diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f46440f99971c70e3258e04bcbac90379f431e47..b4fffe8144a786c8ec85a3c63028d73f2fa28177 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,6 @@
 stages:
   - check-code
-  - compile
+  - tests
 
 check-code:
   image: ninfra/puppet-checker:0.0.1
@@ -17,7 +17,7 @@ compile-profiles:
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
     USE_PUPPETDB: 'false'
-  stage: compile
+  stage: tests
   script:
     - 'echo "127.0.1.1 $( facter fqdn )" >> /etc/hosts'
     - 'service puppet-master start'
@@ -29,3 +29,9 @@ compile-profiles:
        done < <( find ./profile/manifests/ -name "*.pp" -a -exec grep ^class "{}" \; | awk "{ print \$2 }" );'
   tags:
     - docker
+
+enc-test:
+  image: debian:stable
+  stage: tests
+  script:
+    - '${CI_PROJECT_DIR}/profile/files/puppet/puppet_node_classifier --run-tests'
diff --git a/profile/files/puppet/puppet_node_classifier b/profile/files/puppet/puppet_node_classifier
new file mode 100755
index 0000000000000000000000000000000000000000..6dbe8dbd3858050fa07d34eeb73d78a22d057462
--- /dev/null
+++ b/profile/files/puppet/puppet_node_classifier
@@ -0,0 +1,133 @@
+#!/bin/bash
+#
+# Puppet External Node Classifier
+# -------------------------------
+#
+# This script parses FQDNs in the following format:
+#
+#     ROLE-ENV-TAG.DOMAIN
+#
+# And returns the proper classes and environment accordingly:
+#
+#     - ROLE should be alpha-num + '_' and the class returned will be
+#       role::ROLE.
+#     
+#     - ENV: should be alpha-num + '_' and, if it is a prefix of one of
+#       "production", "staging" or "development", then the corresponding one is
+#       returned as an environment for the node. Otherwise, ENV itself is
+#       returned.
+#
+#     - TAG: should be alpha-num + '_' + '+' and should add enough info to make
+#       sure the FQDN is unique in your infrastructure.
+#
+# If a FQDN doesn't match as above, no special information is returned.
+#
+# See: https://puppet.com/docs/puppet/5.5/nodes_external.html
+
+if [ ${#} -ne 1 ]; then
+	echo "Usage: ${0} NODE"
+	exit 1
+fi
+
+PREFIXED_ENVS='production staging development'
+
+get_environment() {
+
+	# Return one of the PREFIXED_ENVS defined above if ENV is a prefix of
+	# one of them, otherwise just return ENV itself.
+
+	prefix=${1}
+	environment=""
+	for env in ${PREFIXED_ENVS}; do
+		if [[ ${env} == ${prefix}* ]]; then
+			environment=${env}
+			break
+		fi
+	done
+	if [ -z "${environment}" ]; then
+		environment="${prefix}"
+	fi
+	echo ${environment}
+}
+
+main() {
+
+	# Test whether the given FQDN matches ROLE-ENV-TAG.DOMAIN, and act
+	# accordingly.
+
+	#         1. role     2. environment      3. tag/id
+	regex="([[:alnum:]_]+)-([[:alnum:]_]+)-([[:alnum:]_-]+)((\.[[:alnum:]_-]+)?)+$"
+	fqdn=${1}
+
+	# we're only interested in matching FQDNs
+	if [[ ! ${fqdn} =~ ${regex} ]]; then
+		echo "classes:"
+		exit 0
+	fi
+
+	echo "classes:"
+	echo "  - role::${BASH_REMATCH[1]}"
+	echo "environment: $( get_environment ${BASH_REMATCH[2]} )"
+}
+
+run_tests() {
+
+	# If FQDN = ROLE-ENV-TAG.DOMAIN, we expect `main` to output:
+	#
+	#   ----------8<----------
+	#   classes:
+	#     - role::${ROLE}
+	#   environment: ${REAL_ENV}
+	#   ----------8<----------
+	#
+	# where REAL_ENV is either one of PREFIXED_ENVS as defined above (if
+	# ENV is a prefix of one of them) or just ENV otherwise.
+        #
+	# If FQDN does not fit the model, we expect just an empty `classes`
+	# hash:
+	#
+	#   ----------8<----------
+	#   classes:
+	#   ----------8<----------
+
+	set -ex
+
+	# Test uses of environment prefixes.
+
+	output=$( main otherrole-prod-some-tag.example.com )
+        [ "${output}" == $'classes:\n  - role::otherrole\nenvironment: production' ] || exit 1
+
+	output=$( main thirdrole-stag-some-tag.example.com )
+        [ "${output}" == $'classes:\n  - role::thirdrole\nenvironment: staging' ] || exit 1
+
+	output=$( main yetanotherrole-dev-some-tag.example.com )
+        [ "${output}" == $'classes:\n  - role::yetanotherrole\nenvironment: development' ] || exit 1
+
+	# Test uses of full environment names.
+
+	output=$( main otherrole-prod-some-tag.example.com )
+        [ "${output}" == $'classes:\n  - role::otherrole\nenvironment: production' ] || exit 1
+
+	output=$( main thirdrole-stag-some-tag.example.com )
+        [ "${output}" == $'classes:\n  - role::thirdrole\nenvironment: staging' ] || exit 1
+
+	output=$( main yetanotherrole-dev-some-tag.example.com )
+        [ "${output}" == $'classes:\n  - role::yetanotherrole\nenvironment: development' ] || exit 1
+
+	# Test using an arbitrary environment
+
+	output=$( main somerole-someenv-some-tag.example.com )
+        [ "${output}" == $'classes:\n  - role::somerole\nenvironment: someenv' ] || exit 1
+
+	# Test using something that doesn't match ROLE-ENV-TAG.DOMAIN
+
+	output=$( main arbitrary-fqdn.example.com )
+        [ "${output}" == $'classes:' ] || exit 1
+
+}
+
+if [ ${1} == '--run-tests' ]; then
+	run_tests
+else
+	main ${1}
+fi
diff --git a/profile/manifests/puppet/master.pp b/profile/manifests/puppet/master.pp
index 473f3326fb2e55788312a7d0c198964f719d20de..e062a2080ccc3bdbde78e6a0a3411453403c979c 100644
--- a/profile/manifests/puppet/master.pp
+++ b/profile/manifests/puppet/master.pp
@@ -62,7 +62,7 @@ class profile::puppet::master (
     server                     => true,
     server_reports             => 'store,puppetdb',
     server_foreman             => false,
-    server_external_nodes      => '',
+    server_external_nodes      => '/usr/local/bin/puppet_node_classifier',
     server_git_repo            => true,
     server_git_repo_path       => '/var/lib/gitolite3/repositories/puppet.git',
     server_git_repo_user       => 'gitolite3',
@@ -183,4 +183,13 @@ class profile::puppet::master (
     require => Class['puppet'],
   }
 
+  # External Node Classifier
+  file { '/usr/local/bin/puppet_node_classifier':
+    ensure  => file,
+    source  => "puppet://modules/profile/puppet/puppet_node_classifier",
+    owner   => root,
+    group   => root,
+    mode    => '0755';
+  }
+
 }