diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 34858a522041363a4e0ed03109b1400d874a83b0..73ae623b81b1ce6ac95aefb1ae1eacb4ac34578d 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -111,3 +111,7 @@
   = render_if_exists "layouts/frontend_monitor"
   %meta{ name: "description", content: page_description }
   %meta{ name: 'theme-color', content: user_theme_primary_color }
+
+  - if Gitlab.config.gitlab.respond_to?(:custom_html_header_tags)
+    - unless Gitlab.config.gitlab.custom_html_header_tags.empty?
+      = Gitlab.config.gitlab.custom_html_header_tags.html_safe
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 86457f339b82c1ebab946abe0df0d6e9626977a3..afb7697472e38633944441b2ce46f2fda6228a7e 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -139,6 +139,15 @@ production: &base
     ##   11 - Dark Mode (alpha)
     # default_theme: 1 # default: 1
 
+    ## Custom html header tags
+    # In some cases some custom header tags are needed
+    # e.g., to add the EU cookie consent
+    # Tip: you must add the externals source to the content_security_policy as
+    #      well, typically the script_src and style_src.
+    # custom_html_header_tags: |
+    #   <script src="https://example.com/cookie-consent.js"></script>
+    #   <link rel="stylesheet" href="https://example.com/cookie-consent.css"/>
+
     ## Automatic issue closing
     # If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
     # This happens when the commit is pushed or merged into the default branch of a project.
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index feda8a86af68568196f186ca057f3afa61f616bc..1a37895853c4befe6a426c32823bc4d13159cc95 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -187,6 +187,7 @@
 # `default_can_create_group` is deprecated since GitLab 15.5 in favour of the `can_create_group` column on `ApplicationSetting`.
 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
 Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
+Settings.gitlab['custom_html_header_tags'] ||= Settings.gitlab['custom_html_header_tags'] || ''
 Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
 Settings.gitlab['cdn_host'] ||= ENV['GITLAB_CDN_HOST'].presence
 Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
diff --git a/doc/administration/configure.md b/doc/administration/configure.md
index 7ea3b19ea33cc222c313d31022006f8ad2e85827..df6f280e11d01910cc32a8115303c4fdd0e210e9 100644
--- a/doc/administration/configure.md
+++ b/doc/administration/configure.md
@@ -47,3 +47,4 @@ Customize and configure your self-managed GitLab installation.
 - [Issue closing pattern](../administration/issue_closing_pattern.md)
 - [Snippets](../administration/snippets/index.md)
 - [Host the product documentation](../administration/docs_self_host.md)
+- [Custom HTML header tags](../administration/custom_html_header_tags.md)
diff --git a/doc/administration/custom_html_header_tags.md b/doc/administration/custom_html_header_tags.md
new file mode 100644
index 0000000000000000000000000000000000000000..6a8b509411fa90abf1f75535676933b44a2c8ba1
--- /dev/null
+++ b/doc/administration/custom_html_header_tags.md
@@ -0,0 +1,70 @@
+---
+stage: Govern
+group: Compliance
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+description: Learn how to modify the HTML header tags of your GitLab instance.
+---
+
+# Custom HTML header tags
+
+DETAILS:
+**Tier:** Free, Premium, Ultimate
+**Offering:** Self-managed
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/153877) in GitLab 17.1.
+
+If you self-manage a GitLab instance in the EU, or any jurisdiction that
+requires a cookie consent banner, additional HTML header tags are needed to
+add scripts and stylesheets.
+
+## Security implications
+
+Before enabling this feature, you should understand the security implications this might have.
+
+A previously legit external resource could end up being compromised and then used to extract
+pretty much any data from any user in the GitLab instance. For that reason,
+you should never add resources from untrusted external sources. If possible, you should always
+use integrity checks like [Subresource Integrity](https://www.w3.org/TR/SRI/) with third-party
+resources to confirm the authenticity of the resources that are loaded.
+
+Limit the functionality you are adding by using HTML header tags to the minimum.
+Otherwise, it could cause also stability or functionality issues if you, for example,
+interact with other application code from GitLab.
+
+## Add a custom HTML header tag
+
+You must add the externals sources to the Content Security Policy which is
+available in the `content_security_policy` option. For the following example, you
+must extend the `script_src` and `style_src`.
+
+To add a custom HTML header tag:
+
+::Tabs
+
+:::TabTitle Self-compiled
+
+1. Edit `/home/git/gitlab/config/gitlab.yml`:
+
+   ```yaml
+   production: &base
+     gitlab:
+       custom_html_header_tags: |
+         <script src="https://example.com/cookie-consent.js" integrity="sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"         crossorigin="anonymous"></script>
+         <link rel="stylesheet" href="https://example.com/cookie-consent.css" integrity="sha384-+/M6kredJcxdsqkczBUjMLvqyHb1K/JThDXWsBVxMEeZHEaMKEOEct339VItX1zB"        crossorigin="anonymous">
+       content_security_policy:
+         directives:
+           script_src: "'self' 'unsafe-eval' https://example.com http://localhost:* https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.gstatic.com/recaptcha/ https://apis.google.com"
+           style_src: "'self' 'unsafe-inline' https://example.com"
+   ```
+
+1. Save the file and restart GitLab:
+
+   ```shell
+   # For systems running systemd
+   sudo systemctl restart gitlab.target
+
+   # For systems running SysV init
+   sudo service gitlab restart
+   ```
+
+::EndTabs
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 5ef25bdbde42e0812055072083a969c03b20ea10..992edcf95f3212bd04d9fad34b0f177bd6a6764d 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -95,6 +95,17 @@
     end
   end
 
+  context 'when custom_html_header_tags are set' do
+    before do
+      allow(Gitlab.config.gitlab).to receive(:custom_html_header_tags).and_return('<script src="https://example.com/cookie-consent.js"></script>')
+    end
+
+    it 'adds the custom html header tag' do
+      render
+      expect(rendered).to match('<script src="https://example.com/cookie-consent.js"></script>')
+    end
+  end
+
   context 'when an asset_host is set and snowplow url is set', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346542' do
     let(:asset_host) { 'http://test.host' }
     let(:snowplow_collector_hostname) { 'www.snow.plow' }