diff --git a/Gemfile b/Gemfile
index 2ba2d18b947d10346011cf2ac6c90a358032a526_R2VtZmlsZQ==..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_R2VtZmlsZQ== 100644
--- a/Gemfile
+++ b/Gemfile
@@ -41,6 +41,7 @@
 gem 'yard'
 gem 'toml-rb'
 gem 'retriable'
+gem "tomlib", "~> 0.5.0"
 
 group :packagecloud, optional: true do
   gem 'package_cloud'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2ba2d18b947d10346011cf2ac6c90a358032a526_R2VtZmlsZS5sb2Nr..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_R2VtZmlsZS5sb2Nr 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -479,6 +479,7 @@
     thread_safe (0.3.6)
     toml-rb (2.2.0)
       citrus (~> 3.0, > 3.0)
+    tomlib (0.5.0)
     tomlrb (1.3.0)
     train-core (3.9.2)
       addressable (~> 2.5)
@@ -584,6 +585,7 @@
   simplecov-cobertura
   thor (~> 1.2)
   toml-rb
+  tomlib (~> 0.5.0)
   yard
 
 BUNDLED WITH
diff --git a/config/patches/tomlib/license/add-license-file.patch b/config/patches/tomlib/license/add-license-file.patch
new file mode 100644
index 0000000000000000000000000000000000000000..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_Y29uZmlnL3BhdGNoZXMvdG9tbGliL2xpY2Vuc2UvYWRkLWxpY2Vuc2UtZmlsZS5wYXRjaA==
--- /dev/null
+++ b/config/patches/tomlib/license/add-license-file.patch
@@ -0,0 +1,27 @@
+diff --git a/LICENSE.txt b/LICENSE.txt
+new file mode 100644
+index 000000000..82cf5177b
+--- /dev/null
++++ b/LICENSE.txt
+@@ -0,0 +1,21 @@
++The MIT License (MIT)
++
++Copyright (c) 2022 Kamil Giszczak
++
++Permission is hereby granted, free of charge, to any person obtaining a copy
++of this software and associated documentation files (the "Software"), to deal
++in the Software without restriction, including without limitation the rights
++to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++copies of the Software, and to permit persons to whom the Software is
++furnished to do so, subject to the following conditions:
++
++The above copyright notice and this permission notice shall be included in
++all copies or substantial portions of the Software.
++
++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
++THE SOFTWARE.
diff --git a/config/software/gitaly.rb b/config/software/gitaly.rb
index 2ba2d18b947d10346011cf2ac6c90a358032a526_Y29uZmlnL3NvZnR3YXJlL2dpdGFseS5yYg==..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_Y29uZmlnL3NvZnR3YXJlL2dpdGFseS5yYg== 100644
--- a/config/software/gitaly.rb
+++ b/config/software/gitaly.rb
@@ -30,6 +30,7 @@
 dependency 'ruby'
 dependency 'bundler'
 dependency 'libicu'
+dependency 'tomlib'
 
 # Technically, gitaly depends on git also. But because of how omnibus arranges
 # components to be built, this causes git to be built early in the process. But
diff --git a/config/software/tomlib.rb b/config/software/tomlib.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_Y29uZmlnL3NvZnR3YXJlL3RvbWxpYi5yYg==
--- /dev/null
+++ b/config/software/tomlib.rb
@@ -0,0 +1,36 @@
+#
+# Copyright 2016-2023 GitLab Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+name 'tomlib'
+
+# The version here should be in agreement with /Gemfile.lock so that our rspec
+# testing stays consistent with the package contents.
+default_version '0.5.0'
+
+license 'MIT'
+license_file 'LICENSE.txt'
+
+skip_transitive_dependency_licensing true
+
+dependency 'ruby'
+
+build do
+  patch source: "license/add-license-file.patch"
+  env = with_standard_compiler_flags(with_embedded_path)
+
+  gem 'install tomlib' \
+      " --version '#{version}'", env: env
+end
diff --git a/files/gitlab-cookbooks/gitaly/attributes/default.rb b/files/gitlab-cookbooks/gitaly/attributes/default.rb
index 2ba2d18b947d10346011cf2ac6c90a358032a526_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvYXR0cmlidXRlcy9kZWZhdWx0LnJi..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvYXR0cmlidXRlcy9kZWZhdWx0LnJi 100644
--- a/files/gitlab-cookbooks/gitaly/attributes/default.rb
+++ b/files/gitlab-cookbooks/gitaly/attributes/default.rb
@@ -1,6 +1,4 @@
 default['gitaly']['enable'] = false
 default['gitaly']['ha'] = false
 default['gitaly']['dir'] = "/var/opt/gitlab/gitaly"
-default['gitaly']['runtime_dir'] = "#{node['gitaly']['dir']}/run"
-default['gitaly']['log_directory'] = "/var/log/gitlab/gitaly"
 default['gitaly']['env_directory'] = "/opt/gitlab/etc/gitaly/env"
@@ -6,4 +4,3 @@
 default['gitaly']['env_directory'] = "/opt/gitlab/etc/gitaly/env"
-default['gitaly']['graceful_restart_timeout'] = nil
 # default['gitaly']['env'] is set in ../recipes/enable.rb
 default['gitaly']['bin_path'] = "/opt/gitlab/embedded/bin/gitaly"
@@ -8,16 +5,3 @@
 # default['gitaly']['env'] is set in ../recipes/enable.rb
 default['gitaly']['bin_path'] = "/opt/gitlab/embedded/bin/gitaly"
-default['gitaly']['socket_path'] = "#{node['gitaly']['dir']}/gitaly.socket"
-default['gitaly']['listen_addr'] = nil
-default['gitaly']['tls_listen_addr'] = nil
-default['gitaly']['certificate_path'] = nil
-default['gitaly']['key_path'] = nil
-default['gitaly']['gpg_signing_key_path'] = nil
-default['gitaly']['prometheus_listen_addr'] = "localhost:9236"
-default['gitaly']['logging_level'] = nil
-default['gitaly']['logging_format'] = "json"
-default['gitaly']['logging_sentry_dsn'] = nil
-default['gitaly']['logging_ruby_sentry_dsn'] = nil
-default['gitaly']['logging_sentry_environment'] = nil
-default['gitaly']['prometheus_grpc_latency_buckets'] = nil
 default['gitaly']['storage'] = []
@@ -23,7 +7,2 @@
 default['gitaly']['storage'] = []
-default['gitaly']['auth_token'] = nil
-default['gitaly']['auth_transitioning'] = false
-default['gitaly']['git_catfile_cache_size'] = nil
-default['gitaly']['git_bin_path'] = "#{node['package']['install-dir']}/embedded/bin/git"
-default['gitaly']['use_bundled_git'] = true
 default['gitaly']['open_files_ulimit'] = 15000
@@ -29,23 +8,2 @@
 default['gitaly']['open_files_ulimit'] = 15000
-default['gitaly']['ruby_max_rss'] = nil
-default['gitaly']['ruby_graceful_restart_timeout'] = nil
-default['gitaly']['ruby_restart_delay'] = nil
-default['gitaly']['ruby_num_workers'] = nil
-default['gitaly']['concurrency'] = nil
-default['gitaly']['rate_limiting'] = nil
-default['gitaly']['daily_maintenance_start_hour'] = nil
-default['gitaly']['daily_maintenance_start_minute'] = nil
-default['gitaly']['daily_maintenance_duration'] = nil
-default['gitaly']['daily_maintenance_storages'] = nil
-default['gitaly']['daily_maintenance_disabled'] = false
-default['gitaly']['cgroups_mountpoint'] = nil
-default['gitaly']['cgroups_hierarchy_root'] = nil
-default['gitaly']['cgroups_memory_bytes'] = nil
-default['gitaly']['cgroups_cpu_shares'] = nil
-default['gitaly']['cgroups_repositories_count'] = nil
-default['gitaly']['cgroups_repositories_memory_bytes'] = nil
-default['gitaly']['cgroups_repositories_cpu_shares'] = nil
-default['gitaly']['pack_objects_cache_enabled'] = false
-default['gitaly']['pack_objects_cache_dir'] = nil
-default['gitaly']['pack_objects_cache_max_age'] = nil
 default['gitaly']['consul_service_name'] = 'gitaly'
@@ -51,3 +9,2 @@
 default['gitaly']['consul_service_name'] = 'gitaly'
-default['gitaly']['custom_hooks_dir'] = nil
 default['gitaly']['consul_service_meta'] = nil
@@ -53,1 +10,15 @@
 default['gitaly']['consul_service_meta'] = nil
+
+default['gitaly']['configuration'] = {
+  runtime_dir: "#{node['gitaly']['dir']}/run",
+  socket_path: "#{node['gitaly']['dir']}/gitaly.socket",
+  prometheus_listen_addr: 'localhost:9236',
+  logging: {
+    dir: '/var/log/gitlab/gitaly',
+    format: 'json'
+  },
+  git: {
+    use_bundled_binaries: true,
+    bin_path: "#{node['package']['install-dir']}/embedded/bin/git"
+  }
+}
diff --git a/files/gitlab-cookbooks/gitaly/libraries/gitaly.rb b/files/gitlab-cookbooks/gitaly/libraries/gitaly.rb
index 2ba2d18b947d10346011cf2ac6c90a358032a526_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvbGlicmFyaWVzL2dpdGFseS5yYg==..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvbGlicmFyaWVzL2dpdGFseS5yYg== 100644
--- a/files/gitlab-cookbooks/gitaly/libraries/gitaly.rb
+++ b/files/gitlab-cookbooks/gitaly/libraries/gitaly.rb
@@ -16,6 +16,7 @@
 #
 
 require 'chef/mash'
+require 'tomlib'
 require_relative '../../package/libraries/helpers/output_helper.rb'
 
 module Gitaly
@@ -27,6 +28,9 @@
       parse_gitaly_storages
       parse_gitconfig
       parse_legacy_cgroup_variables
+
+      remap_legacy_values
+      check_array_entries
     end
 
     def parse_legacy_cgroup_variables
@@ -30,6 +34,10 @@
     end
 
     def parse_legacy_cgroup_variables
+      # Don't map these if the new configuraton is set. Doing so would set the old keys
+      # and raise an error as both keys are set.
+      return unless Gitlab['gitaly'].dig('configuration', 'cgroups').nil?
+
       # maintain backwards compatibility with pre 15.0 Gitaly cgroups config
       cgroups_repositories_memory_bytes = Gitlab['gitaly']['cgroups_repositories_memory_bytes'] || (Gitlab['gitaly']['cgroups_memory_limit'] if Gitlab['gitaly']['cgroups_memory_enabled'])
       cgroups_repositories_cpu_shares = Gitlab['gitaly']['cgroups_repositories_cpu_shares'] || (Gitlab['gitaly']['cgroups_cpu_shares'] if Gitlab['gitaly']['cgroups_cpu_enabled'])
@@ -43,9 +51,9 @@
     end
 
     def gitaly_address
-      socket_path     = user_config['socket_path']     || package_default['socket_path']
-      listen_addr     = user_config['listen_addr']     || package_default['listen_addr']
-      tls_listen_addr = user_config['tls_listen_addr'] || package_default['tls_listen_addr']
+      socket_path = user_config.dig('configuration', 'socket_path')         || user_config['socket_path']     || package_default.dig('configuration', 'socket_path')
+      listen_addr = user_config.dig('configuration', 'listen_addr')         || user_config['listen_addr']     || package_default.dig('configuration', 'listen_addr')
+      tls_listen_addr = user_config.dig('configuration', 'tls_listen_addr') || user_config['tls_listen_addr'] || package_default.dig('configuration', 'tls_listen_addr')
 
       # Default to using socket path if available
       if tls_listen_addr && !tls_listen_addr.empty?
@@ -76,7 +84,7 @@
     end
 
     def parse_gitaly_storages
-      return unless Gitlab['gitaly']['storage'].nil?
+      return unless Gitlab['gitaly']['storage'].nil? && Gitlab['gitaly'].dig('configuration', 'storage').nil?
 
       storages = []
       Gitlab['gitlab_rails']['repositories_storages'].each do |key, value|
@@ -168,5 +176,170 @@
     def package_default
       Gitlab['node']['gitaly'].to_hash
     end
+
+    # remap_legacy_values moves configuration values from their legacy locations to where they are
+    # in Gitaly's own configuration. All of the configuration was previously grouped under Gitlab['gitaly']
+    # but now Gitaly's own config is under Gitlab['gitaly']['configuration']. This then allows us to
+    # simply encode the map as TOML to get the resulting Gitaly configuration file without having to manually
+    # template every key. As existing configuration files may can still have the configuration in its old place,
+    # this method provides backwards compatibility by moving the old values to their new locations. This can
+    # compatibility wrapper can be removed in 16.0
+    def remap_legacy_values
+      Gitlab['gitaly']['configuration'] = {} unless Gitlab['gitaly']['configuration']
+
+      remap_recursive(
+        {
+          socket_path: 'socket_path',
+          runtime_dir: 'runtime_dir',
+          listen_addr: 'listen_addr',
+          prometheus_listen_addr: 'prometheus_listen_addr',
+          tls_listen_addr: 'tls_listen_addr',
+          tls: {
+            certificate_path: 'certificate_path',
+            key_path: 'key_path'
+          },
+          graceful_restart_timeout: 'graceful_restart_timeout',
+          logging: {
+            level: 'logging_level',
+            format: 'logging_format',
+            sentry_dsn: 'logging_sentry_dsn',
+            ruby_sentry_dsn: 'logging_ruby_sentry_dsn',
+            sentry_environment: 'logging_sentry_environment',
+            dir: 'log_directory'
+          },
+          prometheus: {
+            grpc_latency_buckets: lambda {
+              return [] unless Gitlab['gitaly'].key?('prometheus_grpc_latency_buckets')
+              raise "Legacy configuration key 'prometheus_grpc_latency_buckets' can't be set when its new key 'configuration.prometheus.grpc_latency_buckets' is set." if (Gitlab['gitaly'].dig('configuration', 'prometheus') || {}).key?('grpc_latency_buckets')
+
+              # The legacy value is not actually an array but a string like '[0, 1, 2]'.
+              # The template evaluated Ruby code, so the array string got evaluated to
+              # an array. Parse the array into a Ruby array here.
+              JSON.parse(Gitlab['gitaly']['prometheus_grpc_latency_buckets'])
+            },
+          },
+          auth: {
+            token: 'auth_token',
+            transitioning: 'auth_transitioning'
+          },
+          git: {
+            catfile_cache_size: 'git_catfile_cache_size',
+            bin_path: 'git_bin_path',
+            use_bundled_binaries: 'use_bundled_git',
+            signing_key: 'gpg_signing_key_path',
+            config: lambda {
+              return [] unless Gitlab['gitaly']['gitconfig']
+              raise "Legacy configuration keys 'gitconfig' and 'omnibus_gitconfig' can't be set when its new key 'configuration.git.config' is set." if (Gitlab['gitaly'].dig('configuration', 'git') || {}).key?('config')
+
+              Gitlab['gitaly']['gitconfig'].map do |entry|
+                {
+                  key: [entry['section'], entry['subsection'], entry['key']].compact.join('.'),
+                  value: entry['value']
+                }
+              end
+            }
+          },
+          'gitaly-ruby': {
+            max_rss: 'ruby_max_rss',
+            graceful_restart_timeout: 'ruby_graceful_restart_timeout',
+            restart_delay: 'ruby_restart_delay',
+            num_workers: 'ruby_num_workers'
+          },
+          storage: 'storage',
+          hooks: {
+            custom_hooks_dir: 'custom_hooks_dir'
+          },
+          daily_maintenance: {
+            disabled: 'daily_maintenance_disabled',
+            start_hour: 'daily_maintenance_start_hour',
+            start_minute: 'daily_maintenance_start_minute',
+            duration: 'daily_maintenance_duration',
+            storages: 'daily_maintenance_storages'
+          },
+          cgroups: {
+            mountpoint: 'cgroups_mountpoint',
+            hierarchy_root: 'cgroups_hierarchy_root',
+            memory_bytes: 'cgroups_memory_bytes',
+            cpu_shares: 'cgroups_cpu_shares',
+            repositories: {
+              count: 'cgroups_repositories_count',
+              memory_bytes: 'cgroups_repositories_memory_bytes',
+              cpu_shares: 'cgroups_repositories_cpu_shares'
+            }
+          },
+          concurrency: 'concurrency',
+          rate_limiting: 'rate_limiting',
+          pack_objects_cache: {
+            enabled: 'pack_objects_cache_enabled',
+            dir: 'pack_objects_cache_dir',
+            max_age: 'pack_objects_cache_max_age'
+          }
+        },
+        Gitlab['gitaly']['configuration'],
+        ['configuration']
+      )
+    end
+
+    # remap_recursive goes over the mappings to remap the configuration from the old format into the new
+    # format:
+    #   - Hash values indicate a subsection in the destination configuration. Hashes are recursed into to
+    #     build the expected configuration structure.
+    #   - Proc values are mapping functions that return the new value when executed.
+    #   - String values indicate an old configuration key that should be copied into the new configuration
+    #     under the new key.
+    #
+    # new_parent contains the parent key path on each level of recursion.
+    def remap_recursive(mappings, new_configuration, new_parent)
+      mappings.each do |new_key, mapping|
+        # If this is a hash, recurse the tree to create the correct structure.
+        if mapping.is_a?(Hash)
+          new_value = remap_recursive(
+            mappings[new_key],
+            # If there is already a section in the new configuration under the key, use that. If not,
+            # initialize and empty hash for the section.
+            Gitlab['gitaly'].dig(*[new_parent, new_key].flatten) || {},
+            [new_parent, new_key].flatten
+          )
+
+          new_configuration[new_key] = new_value unless new_value.empty?
+          next
+        end
+
+        # If this is a Proc, it's a mapping function that returns the value for the
+        # new key.
+        if mapping.is_a?(Proc)
+          new_value = mapping.call
+          new_configuration[new_key] = new_value unless new_value.empty?
+          next
+        end
+
+        # If the mapping is not a hash nor a lambda, then it is a String. The value gets copied from the
+        # mapping to the new_key as is.
+        #
+        # If there is no old key, then there's nothing to map. Proceed to the next key.
+        next unless Gitlab['gitaly'].key?(mapping)
+
+        # Raise an error if both the old key and the new key are present in the configuration as it would be
+        # ambigious which key is ued in the final configuration.
+        raise "Legacy configuration key '#{mapping}' can't be set when its new key '#{[new_parent, new_key].flatten.join('.')}' is set." if new_configuration.key?(new_key)
+
+        new_configuration[new_key] = Gitlab['gitaly'][mapping]
+      end
+
+      new_configuration
+    end
+
+    # check_array_entries checks that array values in the new configuration are actually arrays.
+    # These values were historically configured as strings. This check guards against copy paster mistakes
+    # users may do while migrating to the new configuration. This check can be removed along the
+    # backwards compatibility code in 16.0.
+    def check_array_entries
+      [
+        [:prometheus, :grpc_latency_buckets]
+      ].each do |key_path|
+        value = Gitlab['gitaly']['configuration'].dig(*key_path)
+        raise "gitaly['configuration']#{key_path.map { |e| "[:#{e}]" }.join('')} must be an array, not a string" unless value.nil? || value.is_a?(Array)
+      end
+    end
   end
 end
diff --git a/files/gitlab-cookbooks/gitaly/recipes/enable.rb b/files/gitlab-cookbooks/gitaly/recipes/enable.rb
index 2ba2d18b947d10346011cf2ac6c90a358032a526_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvcmVjaXBlcy9lbmFibGUucmI=..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvcmVjaXBlcy9lbmFibGUucmI= 100644
--- a/files/gitlab-cookbooks/gitaly/recipes/enable.rb
+++ b/files/gitlab-cookbooks/gitaly/recipes/enable.rb
@@ -18,9 +18,9 @@
 omnibus_helper = OmnibusHelper.new(node)
 
 working_dir = node['gitaly']['dir']
-log_directory = node['gitaly']['log_directory']
+log_directory = node.dig('gitaly', 'configuration', 'logging', 'dir')
 env_directory = node['gitaly']['env_directory']
 config_path = File.join(working_dir, "config.toml")
 gitaly_path = node['gitaly']['bin_path']
 wrapper_path = "#{gitaly_path}-wrapper"
 pid_file = File.join(working_dir, "gitaly.pid")
@@ -22,7 +22,7 @@
 env_directory = node['gitaly']['env_directory']
 config_path = File.join(working_dir, "config.toml")
 gitaly_path = node['gitaly']['bin_path']
 wrapper_path = "#{gitaly_path}-wrapper"
 pid_file = File.join(working_dir, "gitaly.pid")
-json_logging = node['gitaly']['logging_format'].eql?('json')
+json_logging = node.dig('gitaly', 'configuration', 'logging', 'format').eql?('json')
 open_files_ulimit = node['gitaly']['open_files_ulimit']
@@ -28,7 +28,7 @@
 open_files_ulimit = node['gitaly']['open_files_ulimit']
-runtime_dir = node['gitaly']['runtime_dir']
-cgroups_mountpoint = node['gitaly']['cgroups_mountpoint']
-cgroups_hierarchy_root = node['gitaly']['cgroups_hierarchy_root']
+runtime_dir = node.dig('gitaly', 'configuration', 'runtime_dir')
+cgroups_mountpoint = node.dig('gitaly', 'configuration', 'cgroups', 'mountpoint')
+cgroups_hierarchy_root = node.dig('gitaly', 'configuration', 'cgroups', 'hierarchy_root')
 
 directory working_dir do
   owner account_helper.gitlab_user
@@ -81,7 +81,6 @@
 end
 
 gitlab_url, gitlab_relative_path = WebServerHelper.internal_api_url(node)
-custom_hooks_dir = node.dig('gitaly', 'custom_hooks_dir')
 
 template "Create Gitaly config.toml" do
   path config_path
@@ -90,10 +89,41 @@
   group account_helper.gitlab_group
   mode "0640"
   variables node['gitaly'].to_hash.merge(
-    { gitlab_shell: node['gitlab']['gitlab-shell'].to_hash,
-      gitlab_url: gitlab_url,
-      gitlab_relative_path: gitlab_relative_path,
-      custom_hooks_dir: custom_hooks_dir }
+    {
+      configuration: node.dig('gitaly', 'configuration').merge(
+        {
+          # The gitlab section is not configured by the user directly. Its values are derived
+          # from other configuration.
+          gitlab: {
+            url: gitlab_url,
+            relative_url_root: gitlab_relative_path,
+            'http-settings': node.dig('gitlab', 'gitlab-shell', 'http_settings')
+          }.compact,
+
+          # These options below were historically hard coded values in the template. They
+          # are set here to retain the behavior of them not being overridable by the user.
+          bin_dir: '/opt/gitlab/embedded/bin',
+          git: (node.dig('gitaly', 'configuration', 'git') || {}).merge(
+            {
+              # Ignore gitconfig files so that the only source of truth for how Git commands
+              # are configured are Gitaly's own defaults and the Git configuration injected
+              # in this file.
+              ignore_gitconfig: true
+            }
+          ),
+          'gitlab-shell': (node.dig('gitaly', 'configuration', 'gitlab-shell') || {}).merge(
+            {
+              dir: '/opt/gitlab/embedded/service/gitlab-shell'
+            }
+          ),
+          'gitaly-ruby': (node.dig('gitaly', 'configuration', 'gitaly-ruby') || {}).merge(
+            {
+              dir: '/opt/gitlab/embedded/service/gitaly-ruby'
+            }
+          )
+        }
+      )
+    }
   )
   notifies :hup, "runit_service[gitaly]" if omnibus_helper.should_notify?('gitaly')
   sensitive true
@@ -141,6 +171,6 @@
   id 'gitaly'
   meta node['gitaly']['consul_service_meta']
   action Prometheus.service_discovery_action
-  socket_address node['gitaly']['prometheus_listen_addr']
+  socket_address node.dig('gitaly', 'configuration', 'prometheus_listen_addr')
   reload_service false unless Services.enabled?('consul')
 end
diff --git a/files/gitlab-cookbooks/gitaly/templates/default/gitaly-config.toml.erb b/files/gitlab-cookbooks/gitaly/templates/default/gitaly-config.toml.erb
index 2ba2d18b947d10346011cf2ac6c90a358032a526_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvdGVtcGxhdGVzL2RlZmF1bHQvZ2l0YWx5LWNvbmZpZy50b21sLmVyYg==..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9naXRhbHkvdGVtcGxhdGVzL2RlZmF1bHQvZ2l0YWx5LWNvbmZpZy50b21sLmVyYg== 100644
--- a/files/gitlab-cookbooks/gitaly/templates/default/gitaly-config.toml.erb
+++ b/files/gitlab-cookbooks/gitaly/templates/default/gitaly-config.toml.erb
@@ -4,211 +4,4 @@
 # and run:
 # sudo gitlab-ctl reconfigure
 
-socket_path = '<%= @socket_path %>'
-
-runtime_dir = '<%= @runtime_dir %>'
-bin_dir = '/opt/gitlab/embedded/bin'
-<% if @graceful_restart_timeout %>
-graceful_restart_timeout = '<%= @graceful_restart_timeout %>'
-<% end %>
-
-<% if @listen_addr %>
-# Optional: listen on a TCP socket. This is insecure (no authentication)
-listen_addr = '<%= @listen_addr %>'
-<% end %>
-
-<% if @prometheus_listen_addr %>
-# Optional: export metrics via Prometheus
-prometheus_listen_addr = '<%= @prometheus_listen_addr %>'
-<% end %>
-
-<% if @tls_listen_addr %>
-# Optional: listen on a secure TCP socket
-tls_listen_addr = '<%= @tls_listen_addr %>'
-[tls]
-certificate_path = '<%= @certificate_path %>'
-key_path = '<%= @key_path %>'
-<% end %>
-
-<% @storage.each do |shard| %>
-[[storage]]
-<% if shard.has_key?('name') %>
-name = '<%= shard['name'] %>'
-<% end %>
-<% if shard.has_key?('path') %>
-path = '<%= shard['path'] %>'
-<% end %>
-<% end %>
-
-[logging]
-<% if @logging_level %>
-level = '<%= @logging_level %>'
-<% end %>
-<% if @logging_format %>
-format = '<%= @logging_format %>'
-<% end %>
-<% if @logging_sentry_dsn %>
-sentry_dsn = '<%= @logging_sentry_dsn %>'
-<% end %>
-<% if @logging_ruby_sentry_dsn %>
-ruby_sentry_dsn = '<%= @logging_ruby_sentry_dsn %>'
-<% end %>
-<% if @logging_sentry_environment %>
-sentry_environment = '<%= @logging_sentry_environment %>'
-<% end %>
-dir = '<%= @log_directory %>'
-
-<% if @prometheus_grpc_latency_buckets %>
-# # You can optionally configure Gitaly to record histogram latencies on GRPC method calls
-[prometheus]
-grpc_latency_buckets = <%= @prometheus_grpc_latency_buckets %>
-<% end %>
-
-[auth]
-<% if @auth_token %>
-token = '<%= @auth_token %>'
-<% end %>
-<% if @auth_transitioning %>
-transitioning = <%= @auth_transitioning %>
-<% end %>
-
-[git]
-<% if @git_catfile_cache_size %>
-catfile_cache_size = <%= @git_catfile_cache_size %>
-<% end %>
-<% if @git_bin_path %>
-bin_path = '<%= @git_bin_path %>'
-<% end %>
-<% if @use_bundled_git %>
-use_bundled_binaries = <%= @use_bundled_git %>
-<% end %>
-# Ignore gitconfig files so that the only source of truth for how Git commands
-# are configured are Gitaly's own defaults and the Git configuration injected
-# in this file.
-ignore_gitconfig = true
-<% if @gpg_signing_key_path %>
-signing_key = '<%= @gpg_signing_key_path %>'
-<% end %>
-
-<% if @gitconfig %>
-  <% @gitconfig.each do |gitconfig| %>
-[[git.config]]
-key = "<%= [ gitconfig['section'], gitconfig['subsection'], gitconfig['key'] ].compact.join('.') %>"
-value = "<%= gitconfig['value'] %>"
-
-  <% end %>
-<% end %>
-[gitaly-ruby]
-dir = "/opt/gitlab/embedded/service/gitaly-ruby"
-<% if @ruby_max_rss %>
-max_rss = <%= @ruby_max_rss %>
-<% end %>
-<% if @ruby_graceful_restart_timeout %>
-graceful_restart_timeout = '<%= @ruby_graceful_restart_timeout %>'
-<% end %>
-<% if @ruby_restart_delay %>
-restart_delay = '<%= @ruby_restart_delay %>'
-<% end %>
-<% if @ruby_num_workers %>
-num_workers = <%= @ruby_num_workers %>
-<% end %>
-
-[gitlab-shell]
-dir = "/opt/gitlab/embedded/service/gitlab-shell"
-
-[gitlab]
-<% if @gitlab_url %>
-url = '<%= @gitlab_url %>'
-<% end %>
-<% if @gitlab_relative_path %>
-relative_url_root = '<%= @gitlab_relative_path %>'
-<% end %>
-
-<% if @gitlab_shell['http_settings'] %>
-<% http_settings = @gitlab_shell['http_settings'] %>
-[gitlab.http-settings]
-<% if http_settings['read_timeout'] %>
-read_timeout = <%= http_settings['read_timeout'] %>
-<% end %>
-<% if http_settings['user'] %>
-user = '<%= http_settings['user'] %>'
-<% end %>
-<% if http_settings['password'] %>
-password = '<%= http_settings['password'] %>'
-<% end %>
-<% if http_settings['ca_file'] %>
-ca_file = '<%= http_settings['ca_file'] %>'
-<% end %>
-<% if http_settings['ca_path'] %>
-ca_path = '<%= http_settings['ca_path'] %>'
-<% end %>
-<% end %>
-
-[hooks]
-<% if @custom_hooks_dir %>
-custom_hooks_dir = '<%= @custom_hooks_dir %>'
-<% end %>
-
-<% if @concurrency %>
-  <% @concurrency.each do |endpoint| %>
-
-[[concurrency]]
-rpc = "<%= endpoint['rpc'] %>"
-    <% if endpoint['max_per_repo'] %>
-max_per_repo = <%= endpoint['max_per_repo'] %>
-    <% end %>
-    <% if endpoint['max_queue_size'] %>
-max_queue_size = <%= endpoint['max_queue_size'] %>
-    <% end %>
-    <% if endpoint['max_queue_wait'] %>
-max_queue_wait = "<%= endpoint['max_queue_wait'] %>"
-    <% end %>
-  <% end %>
-<% end %>
-
-<% if @rate_limiting %>
-  <% @rate_limiting.each do |endpoint| %>
-[[rate_limiting]]
-rpc = "<%= endpoint['rpc'] %>"
-interval = "<%= endpoint['interval'] %>"
-burst = <%= endpoint['burst'] %>
-  <% end %>
-<% end %>
-
-[daily_maintenance]
-<% if @daily_maintenance_disabled %>
-disabled = <%= @daily_maintenance_disabled %>
-<% else %>
-<%= "start_hour = #{@daily_maintenance_start_hour}" if @daily_maintenance_start_hour %>
-<%= "start_minute = #{@daily_maintenance_start_minute}" if @daily_maintenance_start_minute %>
-<%= "duration = '#{@daily_maintenance_duration}'" if @daily_maintenance_duration %>
-<%= "storages = #{@daily_maintenance_storages}" if @daily_maintenance_storages %>
-<% end %>
-
-<% if @cgroups_repositories_count %>
-[cgroups]
-mountpoint = '<%= @cgroups_mountpoint %>'
-hierarchy_root = '<%= @cgroups_hierarchy_root %>'
-  <% if @cgroups_memory_bytes %>
-memory_bytes = <%= @cgroups_memory_bytes %>
-  <% end %>
-  <% if @cgroups_cpu_shares %>
-cpu_shares = <%= @cgroups_cpu_shares %>
-  <% end %>
-
-[cgroups.repositories]
-count = <%= @cgroups_repositories_count %>
-  <% if @cgroups_repositories_memory_bytes %>
-memory_bytes = <%= @cgroups_repositories_memory_bytes %>
-  <% end %>
-  <% if @cgroups_repositories_cpu_shares %>
-cpu_shares = <%= @cgroups_repositories_cpu_shares %>
-  <% end %>
-<% end %>
-
-<% if @pack_objects_cache_enabled %>
-[pack_objects_cache]
-enabled = true
-<%= "dir = '#{@pack_objects_cache_dir}'" if @pack_objects_cache_dir %>
-<%= "max_age = '#{@pack_objects_cache_max_age}'" if @pack_objects_cache_max_age %>
-<% end %>
+<%= Tomlib.dump(@configuration) %>
diff --git a/files/gitlab-cookbooks/monitoring/libraries/prometheus.rb b/files/gitlab-cookbooks/monitoring/libraries/prometheus.rb
index 2ba2d18b947d10346011cf2ac6c90a358032a526_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9tb25pdG9yaW5nL2xpYnJhcmllcy9wcm9tZXRoZXVzLnJi..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_ZmlsZXMvZ2l0bGFiLWNvb2tib29rcy9tb25pdG9yaW5nL2xpYnJhcmllcy9wcm9tZXRoZXVzLnJi 100644
--- a/files/gitlab-cookbooks/monitoring/libraries/prometheus.rb
+++ b/files/gitlab-cookbooks/monitoring/libraries/prometheus.rb
@@ -241,8 +241,8 @@
       # Don't parse if gitaly is explicitly disabled
       return unless Services.enabled?('gitaly') || service_discovery
 
-      default_config = Gitlab['node']['gitaly'].to_hash
-      user_config = Gitlab['gitaly']
+      default_config = Gitlab['node']['gitaly']['configuration'].to_hash
+      user_config = Gitlab['gitaly']['configuration']
 
       # Don't enable a scrape config if the listen address is empty.
       return if user_config['prometheus_listen_addr'] && user_config['prometheus_listen_addr'].empty?
diff --git a/spec/chef/cookbooks/gitaly/recipes/gitaly_spec.rb b/spec/chef/cookbooks/gitaly/recipes/gitaly_spec.rb
index 2ba2d18b947d10346011cf2ac6c90a358032a526_c3BlYy9jaGVmL2Nvb2tib29rcy9naXRhbHkvcmVjaXBlcy9naXRhbHlfc3BlYy5yYg==..a0f05d22ef782cfc26ae4d2d61a13748d15ba9a4_c3BlYy9jaGVmL2Nvb2tib29rcy9naXRhbHkvcmVjaXBlcy9naXRhbHlfc3BlYy5yYg== 100644
--- a/spec/chef/cookbooks/gitaly/recipes/gitaly_spec.rb
+++ b/spec/chef/cookbooks/gitaly/recipes/gitaly_spec.rb
@@ -100,5 +100,4 @@
     it 'populates gitaly config.toml with defaults' do
       expect(get_rendered_toml(chef_run, '/var/opt/gitlab/gitaly/config.toml')).to eq(
         {
-          auth: {},
           bin_dir: '/opt/gitlab/embedded/bin',
@@ -104,7 +103,6 @@
           bin_dir: '/opt/gitlab/embedded/bin',
-          daily_maintenance: {},
           git: {
             bin_path: '/opt/gitlab/embedded/bin/git',
             ignore_gitconfig: true,
             use_bundled_binaries: true
           },
@@ -106,8 +104,9 @@
           git: {
             bin_path: '/opt/gitlab/embedded/bin/git',
             ignore_gitconfig: true,
             use_bundled_binaries: true
           },
+          cgroups: { repositories: {} },
           'gitaly-ruby': {
             dir: '/opt/gitlab/embedded/service/gitaly-ruby'
           },
@@ -118,7 +117,6 @@
           'gitlab-shell': {
             dir: '/opt/gitlab/embedded/service/gitlab-shell'
           },
-          hooks: {},
           logging: {
             dir: '/var/log/gitlab/gitaly',
             format: 'json'
@@ -173,8 +171,8 @@
     it 'populates gitaly cgroups with correct values' do
       cgroups_section = Regexp.new([
         %r{\[cgroups\]},
-        %r{mountpoint = '#{cgroups_mountpoint}'},
-        %r{hierarchy_root = '#{cgroups_hierarchy_root}'},
+        %r{mountpoint = "#{cgroups_mountpoint}"},
+        %r{hierarchy_root = "#{cgroups_hierarchy_root}"},
         %r{\[cgroups.repositories\]},
         %r{count = 100},
         %r{memory_bytes = #{cgroups_memory_bytes}},
@@ -187,6 +185,44 @@
     end
   end
 
+  context 'with new gitconfig' do
+    it 'raises an error with legacy gitconfig set' do
+      stub_gitlab_rb(
+        {
+          gitaly: {
+            gitconfig: [{ key: 'legacy_key', value: 'legacy_value' }],
+            configuration: {
+              git: {
+                config: [{ key: 'new_key', value: 'new_value' }]
+              }
+            }
+          }
+        }
+      )
+
+      expect { chef_run }.to raise_error "Legacy configuration keys 'gitconfig' and 'omnibus_gitconfig' can't be set when its new key 'configuration.git.config' is set."
+    end
+
+    it 'raises an error if omnibus_gitconfig is set' do
+      stub_gitlab_rb(
+        {
+          omnibus_gitconfig: {
+            system: { nondefault: ['bar = baz'] }
+          },
+          gitaly: {
+            configuration: {
+              git: {
+                config: [{ key: 'new_key', value: 'new_value' }]
+              }
+            }
+          }
+        }
+      )
+
+      expect { chef_run }.to raise_error "Legacy configuration keys 'gitconfig' and 'omnibus_gitconfig' can't be set when its new key 'configuration.git.config' is set."
+    end
+  end
+
   context 'with Omnibus gitconfig' do
     let(:omnibus_gitconfig) { nil }
     let(:gitaly_gitconfig) { nil }
@@ -425,6 +461,134 @@
     end
   end
 
+  context 'with array configured as string' do
+    it 'raises an error with prometheus grpc latency buckets' do
+      stub_gitlab_rb(
+        {
+          gitaly: {
+            enable: true,
+            configuration: {
+              prometheus: {
+                grpc_latency_buckets: '[0, 1, 2]'
+              }
+            }
+          }
+        }
+      )
+
+      expect { chef_run }.to raise_error("gitaly['configuration'][:prometheus][:grpc_latency_buckets] must be an array, not a string")
+    end
+  end
+
+  context 'with some defaults overridden with custom configuration' do
+    before do
+      stub_gitlab_rb(
+        gitaly: {
+          enable: true,
+          configuration: {
+            socket_path: 'overridden_socket_path',
+            logging: {
+              dir: 'overridden_logging_path'
+            },
+            git: {
+              bin_path: 'overridden_git_bin_path'
+            },
+            custom_section: {
+              custom_key: 'custom_value'
+            },
+            storage: [
+              {
+                name: 'custom_storage',
+                path: 'custom_path'
+              },
+            ],
+            cgroups: {
+              cpu_shares: 100,
+            },
+          }
+        }
+      )
+    end
+
+    it 'renders config.toml with' do
+      expect(get_rendered_toml(chef_run, '/var/opt/gitlab/gitaly/config.toml')).to eq(
+        {
+          'gitaly-ruby': {
+            dir: '/opt/gitlab/embedded/service/gitaly-ruby'
+          },
+          'gitlab-shell': {
+            dir: '/opt/gitlab/embedded/service/gitlab-shell'
+          },
+          bin_dir: '/opt/gitlab/embedded/bin',
+          custom_section: { custom_key: 'custom_value' },
+          git: {
+            bin_path: 'overridden_git_bin_path',
+            ignore_gitconfig: true,
+            use_bundled_binaries: true,
+          },
+          gitlab: {
+            url: 'http+unix://%2Fvar%2Fopt%2Fgitlab%2Fgitlab-workhorse%2Fsockets%2Fsocket',
+            relative_url_root: '',
+          },
+          logging: {
+            dir: 'overridden_logging_path',
+            format: 'json',
+          },
+          prometheus_listen_addr: 'localhost:9236',
+          runtime_dir: '/var/opt/gitlab/gitaly/run',
+          socket_path: 'overridden_socket_path',
+          storage: [
+            {
+              name: 'custom_storage',
+              path: 'custom_path',
+            }
+          ],
+          cgroups: {
+            cpu_shares: 100,
+          }
+        }
+      )
+    end
+  end
+
+  context 'with old key and its new key set' do
+    it 'raises an error in generic case' do
+      stub_gitlab_rb(
+        {
+          gitaly: {
+            enable: true,
+            certificate_path: 'legacy_certificate_path_key',
+            configuration: {
+              tls: {
+                certificate_path: 'new_certificate_path_key',
+              }
+            }
+          }
+        }
+      )
+
+      expect { chef_run }.to raise_error "Legacy configuration key 'certificate_path' can't be set when its new key 'configuration.tls.certificate_path' is set."
+    end
+
+    it 'raises an error in generic case' do
+      stub_gitlab_rb(
+        {
+          gitaly: {
+            enable: true,
+            prometheus_grpc_latency_buckets: '[0, 1, 2]',
+            configuration: {
+              prometheus: {
+                grpc_latency_buckets: '[0, 1, 3]',
+              }
+            }
+          }
+        }
+      )
+
+      expect { chef_run }.to raise_error "Legacy configuration key 'prometheus_grpc_latency_buckets' can't be set when its new key 'configuration.prometheus.grpc_latency_buckets' is set."
+    end
+  end
+
   context 'with user settings' do
     before do
       stub_gitlab_rb(
@@ -477,7 +641,7 @@
           ruby_num_workers: ruby_num_workers,
           git_catfile_cache_size: git_catfile_cache_size,
           git_bin_path: git_bin_path,
-          use_bundled_git: true,
+          use_bundled_git: false,
           open_files_ulimit: open_files_ulimit,
           daily_maintenance_start_hour: daily_maintenance_start_hour,
           daily_maintenance_start_minute: daily_maintenance_start_minute,
@@ -494,7 +658,14 @@
           pack_objects_cache_enabled: pack_objects_cache_enabled,
           pack_objects_cache_dir: pack_objects_cache_dir,
           pack_objects_cache_max_age: pack_objects_cache_max_age,
-          custom_hooks_dir: gitaly_custom_hooks_dir
+          custom_hooks_dir: gitaly_custom_hooks_dir,
+          # Sanity check that configuration values get printed out.
+          configuration: {
+            string_value: 'some value',
+            subsection: {
+              array_value: [1, 2, 3]
+            }
+          }
         },
         gitlab_rails: {
           internal_api_url: gitlab_url
@@ -555,6 +726,7 @@
             }
           ],
           daily_maintenance: {
+            disabled: false,
             duration: '45m',
             start_hour: 21,
             start_minute: 9,
@@ -565,7 +737,7 @@
             catfile_cache_size: 50,
             ignore_gitconfig: true,
             signing_key: '/path/to/signing_key.gpg',
-            use_bundled_binaries: true
+            use_bundled_binaries: false
           },
           'gitaly-ruby': {
             dir: '/opt/gitlab/embedded/service/gitaly-ruby',
@@ -633,6 +805,8 @@
               path: '/mnt/nfs1'
             }
           ],
+          string_value: 'some value',
+          subsection: { array_value: [1, 2, 3] },
           tls: {
             certificate_path: '/path/to/cert.pem',
             key_path: '/path/to/key.pem'
@@ -666,7 +840,7 @@
 
       it 'renders daily_maintenance with disabled set to true' do
         expect(chef_run).to render_file(config_path).with_content { |content|
-          expect(content).to include("[daily_maintenance]\ndisabled = true\n\n")
+          expect(content).to include("[daily_maintenance]\ndisabled = true\n")
         }
       end
     end
@@ -688,5 +862,5 @@
 
         it 'populates gitaly config.toml with custom storages' do
           expect(chef_run).to render_file(config_path)
-            .with_content(%r{\[\[storage\]\]\s+name = 'default'\s+path = '/tmp/default/git-data/repositories'})
+            .with_content(%r{\[\[storage\]\]\s+name = "default"\s+path = "/tmp/default/git-data/repositories"})
           expect(chef_run).to render_file(config_path)
@@ -692,5 +866,5 @@
           expect(chef_run).to render_file(config_path)
-            .with_content(%r{\[\[storage\]\]\s+name = 'nfs1'\s+path = '/mnt/nfs1/repositories'})
+            .with_content(%r{\[\[storage\]\]\s+name = "nfs1"\s+path = "/mnt/nfs1/repositories"})
           expect(chef_run).not_to render_file(config_path)
             .with_content('gitaly_address: "/var/opt/gitlab/gitaly/gitaly.socket"')
         end
@@ -711,7 +885,7 @@
 
         it 'populates gitaly config.toml with custom storages' do
           expect(chef_run).to render_file(config_path)
-            .with_content(%r{\[\[storage\]\]\s+name = 'default'\s+path = '/var/opt/gitlab/git-data/repositories'})
+            .with_content(%r{\[\[storage\]\]\s+name = "default"\s+path = "/var/opt/gitlab/git-data/repositories"})
           expect(chef_run).not_to render_file(config_path)
             .with_content('gitaly_address: "tcp://gitaly.internal:8075"')
         end
@@ -903,7 +1077,7 @@
 
       it 'create config file with provided values' do
         expect(chef_run).to render_file(config_path)
-          .with_content(%r{\[gitlab\]\s+url = 'http\+unix://%2Ffake%2Fworkhorse%2Fsocket'\s+relative_url_root = ''})
+          .with_content(%r{\[gitlab\]\s+url = "http\+unix://%2Ffake%2Fworkhorse%2Fsocket"\s+relative_url_root = ""})
       end
     end
 
@@ -914,7 +1088,7 @@
 
       it 'create config file with provided values' do
         expect(chef_run).to render_file(config_path)
-          .with_content(%r{\[gitlab\]\s+url = 'http\+unix://%2Ffake%2Fworkhorse%2Fsockets%2Fsocket'\s+relative_url_root = ''})
+          .with_content(%r{\[gitlab\]\s+url = "http\+unix://%2Ffake%2Fworkhorse%2Fsockets%2Fsocket"\s+relative_url_root = ""})
       end
     end
 
@@ -925,7 +1099,7 @@
 
       it 'create config file with provided values' do
         expect(chef_run).to render_file(config_path)
-          .with_content(%r{\[gitlab\]\s+url = 'http\+unix://%2Fsockets%2Fin%2Fthe%2Fwind'\s+relative_url_root = ''})
+          .with_content(%r{\[gitlab\]\s+url = "http\+unix://%2Fsockets%2Fin%2Fthe%2Fwind"\s+relative_url_root = ""})
       end
     end
   end
@@ -943,7 +1117,7 @@
 
     it 'create config file with only the URL set' do
       expect(chef_run).to render_file(config_path).with_content { |content|
-        expect(content).to match(%r{\[gitlab\]\s+url = 'http://localhost:1234/gitlab'})
+        expect(content).to match(%r{\[gitlab\]\s+url = "http://localhost:1234/gitlab"})
         expect(content).not_to match(/relative_url_root/)
       }
     end
@@ -956,7 +1130,7 @@
 
     it 'create config file with the relative_url_root set' do
       expect(chef_run).to render_file(config_path)
-        .with_content(%r{\[gitlab\]\s+url = 'http\+unix://%2Fvar%2Fopt%2Fgitlab%2Fgitlab-workhorse%2Fsockets%2Fsocket'\s+relative_url_root = '/gitlab'})
+        .with_content(%r{\[gitlab\]\s+url = "http\+unix://%2Fvar%2Fopt%2Fgitlab%2Fgitlab-workhorse%2Fsockets%2Fsocket"\s+relative_url_root = "/gitlab"})
     end
   end