diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index ed86de819eb7c5c8add2031cfc6a06facdf597c3..c52d4d6338209d5c1cb05f6ad247b929649e7072 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -2,7 +2,7 @@ module Ci
   class GitlabCiYamlProcessor
     class ValidationError < StandardError; end
 
-    include Gitlab::Ci::Config::Node::ValidationHelpers
+    include Gitlab::Ci::Config::Node::LegacyValidationHelpers
 
     DEFAULT_STAGES = %w(build test deploy)
     DEFAULT_STAGE = 'test'
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index b48d3592f1699c6473700703b299b3ccd3e6968c..adfd097736e3e0347dd6dd889ed13b4aa38b658e 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -4,8 +4,6 @@ module Gitlab
     # Base GitLab CI Configuration facade
     #
     class Config
-      delegate :valid?, :errors, to: :@global
-
       ##
       # Temporary delegations that should be removed after refactoring
       #
@@ -18,6 +16,14 @@ module Gitlab
         @global.process!
       end
 
+      def valid?
+        @global.valid?
+      end
+
+      def errors
+        @global.errors
+      end
+
       def to_hash
         @config
       end
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
index d60f87f3f94d5e127932c410600aa2183c89a3bc..374ff71d0f5544e40bc0ac9eeb04a164d000da57 100644
--- a/lib/gitlab/ci/config/node/configurable.rb
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -15,27 +15,24 @@ module Gitlab
         #
         module Configurable
           extend ActiveSupport::Concern
+          include Validatable
 
-          def allowed_nodes
-            self.class.allowed_nodes || {}
+          included do
+            validations do
+              validates :config, hash: true
+            end
           end
 
           private
 
-          def prevalidate!
-            unless @value.is_a?(Hash)
-              @errors << 'should be a configuration entry with hash value'
-            end
-          end
-
           def create_node(key, factory)
-            factory.with(value: @value[key])
-            factory.nullify! unless @value.has_key?(key)
+            factory.with(value: @config[key], key: key)
+            factory.nullify! unless @config.has_key?(key)
             factory.create!
           end
 
           class_methods do
-            def allowed_nodes
+            def nodes
               Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }]
             end
 
@@ -47,7 +44,6 @@ module Gitlab
 
               define_method(symbol) do
                 raise Entry::InvalidError unless valid?
-
                 @nodes[symbol].try(:value)
               end
 
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
index 52758a962f3c79989cd36582be5dc86114049853..f044ef965e9c1a19b100a2f9bd9d55caf96a724a 100644
--- a/lib/gitlab/ci/config/node/entry.rb
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -8,14 +8,14 @@ module Gitlab
         class Entry
           class InvalidError < StandardError; end
 
-          attr_accessor :description
+          attr_reader :config
+          attr_accessor :key, :description
 
-          def initialize(value)
-            @value = value
+          def initialize(config)
+            @config = config
             @nodes = {}
-            @errors = []
-
-            prevalidate!
+            @validator = self.class.validator.new(self)
+            @validator.validate
           end
 
           def process!
@@ -23,50 +23,54 @@ module Gitlab
             return unless valid?
 
             compose!
-
-            nodes.each(&:process!)
-            nodes.each(&:validate!)
+            process_nodes!
           end
 
           def nodes
             @nodes.values
           end
 
-          def valid?
-            errors.none?
-          end
-
           def leaf?
-            allowed_nodes.none?
+            self.class.nodes.none?
           end
 
-          def errors
-            @errors + nodes.map(&:errors).flatten
+          def key
+            @key || self.class.name.demodulize.underscore
           end
 
-          def allowed_nodes
-            {}
+          def valid?
+            errors.none?
           end
 
-          def validate!
-            raise NotImplementedError
+          def errors
+            @validator.full_errors +
+              nodes.map(&:errors).flatten
           end
 
           def value
             raise NotImplementedError
           end
 
-          private
+          def self.nodes
+            {}
+          end
 
-          def prevalidate!
+          def self.validator
+            Validator
           end
 
+          private
+
           def compose!
-            allowed_nodes.each do |key, essence|
+            self.class.nodes.each do |key, essence|
               @nodes[key] = create_node(key, essence)
             end
           end
 
+          def process_nodes!
+            nodes.each(&:process!)
+          end
+
           def create_node(key, essence)
             raise NotImplementedError
           end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
index 787ca006f5abb8291ee4a034a56348db121aa06f..025ae40ef944ebb5a8ea4ccbe5d8a7949f11142a 100644
--- a/lib/gitlab/ci/config/node/factory.rb
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -30,6 +30,7 @@ module Gitlab
 
             @entry_class.new(@attributes[:value]).tap do |entry|
               entry.description = @attributes[:description]
+              entry.key = @attributes[:key]
             end
           end
         end
diff --git a/lib/gitlab/ci/config/node/validation_helpers.rb b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
similarity index 97%
rename from lib/gitlab/ci/config/node/validation_helpers.rb
rename to lib/gitlab/ci/config/node/legacy_validation_helpers.rb
index 72f648975dcf1c3ee2f6249ef31499ad535309ae..4d9a508796abff70c3731d0a2248998e7d5a83ab 100644
--- a/lib/gitlab/ci/config/node/validation_helpers.rb
+++ b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
@@ -2,7 +2,7 @@ module Gitlab
   module Ci
     class Config
       module Node
-        module ValidationHelpers
+        module LegacyValidationHelpers
           private
 
           def validate_duration(value)
diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb
index 5072bf0db7d56ef4016629cfb229ffa8a6b0fb5c..c044f5c5e717b38edb77c22b8fcee2f9978065c0 100644
--- a/lib/gitlab/ci/config/node/script.rb
+++ b/lib/gitlab/ci/config/node/script.rb
@@ -11,16 +11,14 @@ module Gitlab
         # implementation in Runner.
         #
         class Script < Entry
-          include ValidationHelpers
+          include Validatable
 
-          def value
-            @value.join("\n")
+          validations do
+            validates :config, array_of_strings: true
           end
 
-          def validate!
-            unless validate_array_of_strings(@value)
-              @errors << 'before_script should be an array of strings'
-            end
+          def value
+            @config.join("\n")
           end
         end
       end
diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f6e2896dfb23ce2760a62aa46f50a1dbc5ba15e6
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validatable.rb
@@ -0,0 +1,29 @@
+module Gitlab
+  module Ci
+    class Config
+      module Node
+        module Validatable
+          extend ActiveSupport::Concern
+
+          class_methods do
+            def validator
+              validator = Class.new(Node::Validator)
+
+              if defined?(@validations)
+                @validations.each { |rules| validator.class_eval(&rules) }
+              end
+
+              validator
+            end
+
+            private
+
+            def validations(&block)
+              (@validations ||= []).append(block)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..02edc9219c395330ee2fc6eac9bfbceb6373858c
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validator.rb
@@ -0,0 +1,27 @@
+module Gitlab
+  module Ci
+    class Config
+      module Node
+        class Validator < SimpleDelegator
+          include ActiveModel::Validations
+          include Node::Validators
+
+          def initialize(node)
+            super(node)
+            @node = node
+          end
+
+          def full_errors
+            errors.full_messages.map do |error|
+              "#{@node.key} #{error}".humanize
+            end
+          end
+
+          def self.name
+            'Validator'
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dc9cdb9a2205038fc928b8a8f06662e55f649476
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validators.rb
@@ -0,0 +1,27 @@
+module Gitlab
+  module Ci
+    class Config
+      module Node
+        module Validators
+          class ArrayOfStringsValidator < ActiveModel::EachValidator
+            include LegacyValidationHelpers
+
+            def validate_each(record, attribute, value)
+              unless validate_array_of_strings(value)
+                record.errors.add(attribute, 'should be an array of strings')
+              end
+            end
+          end
+
+          class HashValidator < ActiveModel::EachValidator
+            def validate_each(record, attribute, value)
+              unless value.is_a?(Hash)
+                record.errors.add(attribute, 'should be a configuration entry hash')
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index d562d8b25ea4f8ed00a13e9ce866cab2afaae142..200ca6aeeea32c059ad3082dc90a117ba02dc689 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -951,7 +951,7 @@ EOT
         config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
         expect do
           GitlabCiYamlProcessor.new(config, path)
-        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings")
+        end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Before script config should be an array of strings")
       end
 
       it "returns errors if job before_script parameter is not an array of strings" do
diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
index 47c68f96dc8ab603743f67ee38aa0e0b8dff4742..9bbda6e73967ba358f44808a5cf1bca7784210f5 100644
--- a/spec/lib/gitlab/ci/config/node/configurable_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
@@ -7,26 +7,26 @@ describe Gitlab::Ci::Config::Node::Configurable do
     node.include(described_class)
   end
 
-  describe 'allowed nodes' do
+  describe 'configured nodes' do
     before do
       node.class_eval do
         allow_node :object, Object, description: 'test object'
       end
     end
 
-    describe '#allowed_nodes' do
-      it 'has valid allowed nodes' do
-        expect(node.allowed_nodes).to include :object
+    describe '.nodes' do
+      it 'has valid nodes' do
+        expect(node.nodes).to include :object
       end
 
       it 'creates a node factory' do
-        expect(node.allowed_nodes[:object])
+        expect(node.nodes[:object])
           .to be_an_instance_of Gitlab::Ci::Config::Node::Factory
       end
 
       it 'returns a duplicated factory object' do
-        first_factory = node.allowed_nodes[:object]
-        second_factory = node.allowed_nodes[:object]
+        first_factory = node.nodes[:object]
+        second_factory = node.nodes[:object]
 
         expect(first_factory).not_to be_equal(second_factory)
       end
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
index d681aa32456fbf43a1f8ebb84876525260a8cc1d..01a707a6bd49846bd865ada732d6b02c294cd33f 100644
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -25,6 +25,16 @@ describe Gitlab::Ci::Config::Node::Factory do
           expect(entry.description).to eq 'test description'
         end
       end
+
+      context 'when setting key' do
+        it 'creates entry with custom key' do
+          entry = factory
+            .with(value: ['ls', 'pwd'], key: 'test key')
+            .create!
+
+          expect(entry.key).to eq 'test key'
+        end
+      end
     end
 
     context 'when not setting value' do
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
index b1972172435a29f0950d665f63138860438a2b7b..fddd53a2b57508c713f14072eb21611f555bc96e 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -3,13 +3,19 @@ require 'spec_helper'
 describe Gitlab::Ci::Config::Node::Global do
   let(:global) { described_class.new(hash) }
 
-  describe '#allowed_nodes' do
+  describe '.nodes' do
     it 'can contain global config keys' do
-      expect(global.allowed_nodes).to include :before_script
+      expect(described_class.nodes).to include :before_script
     end
 
     it 'returns a hash' do
-      expect(global.allowed_nodes).to be_a Hash
+      expect(described_class.nodes).to be_a Hash
+    end
+  end
+
+  describe '#key' do
+    it 'returns underscored class name' do
+      expect(global.key).to eq 'global'
     end
   end
 
@@ -79,7 +85,7 @@ describe Gitlab::Ci::Config::Node::Global do
     describe '#errors' do
       it 'reports errors from child nodes' do
         expect(global.errors)
-          .to include 'before_script should be an array of strings'
+          .to include 'Before script config should be an array of strings'
       end
     end
 
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
index e4d6481f8a5711bacfe6705aa082d50d7b369c1d..6af6aa15eef2dbe618bc20bdda64b9cfb3531d1b 100644
--- a/spec/lib/gitlab/ci/config/node/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -1,13 +1,13 @@
 require 'spec_helper'
 
 describe Gitlab::Ci::Config::Node::Script do
-  let(:entry) { described_class.new(value) }
+  let(:entry) { described_class.new(config) }
 
-  describe '#validate!' do
-    before { entry.validate! }
+  describe '#process!' do
+    before { entry.process! }
 
-    context 'when entry value is correct' do
-      let(:value) { ['ls', 'pwd'] }
+    context 'when entry config value is correct' do
+      let(:config) { ['ls', 'pwd'] }
 
       describe '#value' do
         it 'returns concatenated command' do
@@ -29,12 +29,12 @@ describe Gitlab::Ci::Config::Node::Script do
     end
 
     context 'when entry value is not correct' do
-      let(:value) { 'ls' }
+      let(:config) { 'ls' }
 
       describe '#errors' do
         it 'saves errors' do
           expect(entry.errors)
-            .to include /should be an array of strings/
+            .to include 'Script config should be an array of strings'
         end
       end
 
diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..10cd01afcd1da4e16314af3ab8a0ca7f34a493ff
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Validatable do
+  let(:node) { Class.new }
+
+  before do
+    node.include(described_class)
+  end
+
+  describe '.validator' do
+    before do
+      node.class_eval do
+        attr_accessor :test_attribute
+
+        validations do
+          validates :test_attribute, presence: true
+        end
+      end
+    end
+
+    it 'returns validator' do
+      expect(node.validator.superclass)
+        .to be Gitlab::Ci::Config::Node::Validator
+    end
+
+    context 'when validating node instance' do
+      let(:node_instance) { node.new }
+
+      context 'when attribute is valid' do
+        before do
+          node_instance.test_attribute = 'valid'
+        end
+
+        it 'instance of validator is valid' do
+          expect(node.validator.new(node_instance)).to be_valid
+        end
+      end
+
+      context 'when attribute is not valid' do
+        before do
+          node_instance.test_attribute = nil
+        end
+
+        it 'instance of validator is invalid' do
+          expect(node.validator.new(node_instance)).to be_invalid
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/config/node/validator_spec.rb b/spec/lib/gitlab/ci/config/node/validator_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ad875d553840bf6b0b0797e8d2c645fd8ae35edd
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/validator_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Validator do
+  let(:validator) { Class.new(described_class) }
+  let(:validator_instance) { validator.new(node) }
+  let(:node) { spy('node') }
+
+  shared_examples 'delegated validator' do
+    context 'when node is valid' do
+      before do
+        allow(node).to receive(:test_attribute).and_return('valid value')
+      end
+
+      it 'validates attribute in node' do
+        expect(node).to receive(:test_attribute)
+        expect(validator_instance).to be_valid
+      end
+
+      it 'returns no errors' do
+        validator_instance.validate
+
+        expect(validator_instance.full_errors).to be_empty
+      end
+    end
+
+    context 'when node is invalid' do
+      before do
+        allow(node).to receive(:test_attribute).and_return(nil)
+      end
+
+      it 'validates attribute in node' do
+        expect(node).to receive(:test_attribute)
+        expect(validator_instance).to be_invalid
+      end
+
+      it 'returns errors' do
+        validator_instance.validate
+
+        expect(validator_instance.full_errors).not_to be_empty
+      end
+    end
+  end
+
+  describe 'attributes validations' do
+    before do
+      validator.class_eval do
+        validates :test_attribute, presence: true
+      end
+    end
+
+    it_behaves_like 'delegated validator'
+  end
+
+  describe 'interface validations' do
+    before do
+      validator.class_eval do
+        validate do
+          unless @node.test_attribute == 'valid value'
+            errors.add(:test_attribute, 'invalid value')
+          end
+        end
+      end
+    end
+
+    it_behaves_like 'delegated validator'
+  end
+end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 3871d939feb62c16ebfaef7d32c06aef82524a2a..2a5d132db7bb6f218df734d21f3227e1d7e84afd 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -67,6 +67,12 @@ describe Gitlab::Ci::Config do
             expect(config.errors).not_to be_empty
           end
         end
+
+        describe '#errors' do
+          it 'returns an array of strings' do
+            expect(config.errors).to all(be_an_instance_of(String))
+          end
+        end
       end
     end
   end