diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 50550fc787d1a94ed9ea753d00ab3d83e7a66527..55ff0d97075b7ac39aa7e94f94cc1c3b188cbcbf 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -343,6 +343,8 @@ def visible_attributes
       :commit_email_hostname,
       :protected_ci_variables,
       :local_markdown_version,
+      :mailgun_signing_key,
+      :mailgun_events_enabled,
       :snowplow_collector_hostname,
       :snowplow_cookie_domain,
       :snowplow_enabled,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3804f745520b1d04463b5692d7ab9b698f43043a..cbde2db803a39df6b10182880da68b3e700156a8 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -172,6 +172,11 @@ def self.kroki_formats_attributes
             addressable_url: { enforce_sanitization: true },
             if: :gitpod_enabled
 
+  validates :mailgun_signing_key,
+            presence: true,
+            length: { maximum: 255 },
+            if: :mailgun_events_enabled
+
   validates :snowplow_collector_hostname,
             presence: true,
             hostname: true,
@@ -552,6 +557,7 @@ def self.kroki_formats_attributes
   attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_32_aes_256_gcm
   attr_encrypted :cloud_license_auth_token, encryption_options_base_32_aes_256_gcm
   attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_32_aes_256_gcm
+  attr_encrypted :mailgun_signing_key, encryption_options_base_32_aes_256_gcm.merge(encode: false)
 
   validates :disable_feed_token,
             inclusion: { in: [true, false], message: _('must be a boolean value') }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index a92ae710959992a1553f3f9ef4951f12e0af75bd..5b88d7c4a437c1b886717e31365b6832a3c76b8d 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -104,6 +104,8 @@ def defaults
         issues_create_limit: 300,
         local_markdown_version: 0,
         login_recaptcha_protection_enabled: false,
+        mailgun_signing_key: nil,
+        mailgun_events_enabled: false,
         max_artifacts_size: Settings.artifacts['max_size'],
         max_attachment_size: Settings.gitlab['max_attachment_size'],
         max_import_size: 0,
diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..6204f7df5dcc3444da7364da50d5cd84eb3421fb
--- /dev/null
+++ b/app/views/admin/application_settings/_mailgun.html.haml
@@ -0,0 +1,25 @@
+- return unless Feature.enabled?(:mailgun_events_receiver)
+
+- expanded = integration_expanded?('mailgun_')
+%section.settings.as-mailgun.no-animate#js-mailgun-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Mailgun')
+    %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+      = expanded ? _('Collapse') : _('Expand')
+    %p
+      = _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank') }
+  .settings-content
+    = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-mailgun-settings'), html: { class: 'fieldset-form', id: 'mailgun-settings' } do |f|
+      = form_errors(@application_setting) if expanded
+
+      %fieldset
+        .form-group
+          .form-check
+            = f.check_box :mailgun_events_enabled, class: 'form-check-input'
+            = f.label :mailgun_events_enabled, _('Enable Mailgun event receiver'), class: 'form-check-label'
+        .form-group
+          = f.label :mailgun_signing_key, _('Mailgun HTTP webhook signing key'), class: 'label-light'
+          = f.text_field :mailgun_signing_key, class: 'form-control gl-form-input'
+
+      = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 0fbbef0261382af154b5a3ba0e328c1b03f588b2..53bdbcd713728293a690a4bdd7d4fb06ac3e7ca1 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -107,6 +107,7 @@
 = render_if_exists 'admin/application_settings/maintenance_mode_settings_form'
 = render 'admin/application_settings/gitpod'
 = render 'admin/application_settings/kroki'
+= render 'admin/application_settings/mailgun'
 = render 'admin/application_settings/plantuml'
 = render 'admin/application_settings/sourcegraph'
 = render_if_exists 'admin/application_settings/slack'
diff --git a/config/application.rb b/config/application.rb
index 6da5c872cfacc36134e4d9f0569e98df5c82aa06..6526be15cd4e510a9662c59b5e6df3bbe87e47dc 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -151,6 +151,7 @@ class Application < Rails::Application
       elasticsearch_password
       search
       jwt
+      mailgun_signing_key
       otp_attempt
       sentry_dsn
       trace
diff --git a/config/feature_flags/development/mailgun_events_receiver.yml b/config/feature_flags/development/mailgun_events_receiver.yml
new file mode 100644
index 0000000000000000000000000000000000000000..119d8d34f210e8a49396f45394d8dff60eff685f
--- /dev/null
+++ b/config/feature_flags/development/mailgun_events_receiver.yml
@@ -0,0 +1,8 @@
+---
+name: mailgun_events_receiver
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64249
+rollout_issue_url:
+milestone: '14.1'
+type: development
+group: group::expansion
+default_enabled: false
diff --git a/db/migrate/20210616185947_add_mailgun_settings_to_application_setting.rb b/db/migrate/20210616185947_add_mailgun_settings_to_application_setting.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8447ff79d12cbe783f4d67a071cc75d7ee5ae864
--- /dev/null
+++ b/db/migrate/20210616185947_add_mailgun_settings_to_application_setting.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddMailgunSettingsToApplicationSetting < ActiveRecord::Migration[6.1]
+  def change
+    add_column :application_settings, :encrypted_mailgun_signing_key, :binary
+    add_column :application_settings, :encrypted_mailgun_signing_key_iv, :binary
+
+    add_column :application_settings, :mailgun_events_enabled, :boolean, default: false, null: false
+  end
+end
diff --git a/db/schema_migrations/20210616185947 b/db/schema_migrations/20210616185947
new file mode 100644
index 0000000000000000000000000000000000000000..30275f102dcb52aaff8744420d64867a25f0d445
--- /dev/null
+++ b/db/schema_migrations/20210616185947
@@ -0,0 +1 @@
+8d73f4b4b716176afe5a9b0ee3a4ef28bbbc2fe944a18fb66afa8cf8f763e8ac
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 62c559c1c02621fe79cf0f80a7c91d67ea76cdc8..56a48be6c4b67db6440e7e8b193357054925deae 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9522,6 +9522,9 @@ CREATE TABLE application_settings (
     diff_max_lines integer DEFAULT 50000 NOT NULL,
     diff_max_files integer DEFAULT 1000 NOT NULL,
     valid_runner_registrars character varying[] DEFAULT '{project,group}'::character varying[],
+    encrypted_mailgun_signing_key bytea,
+    encrypted_mailgun_signing_key_iv bytea,
+    mailgun_events_enabled boolean DEFAULT false NOT NULL,
     CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
     CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
     CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
diff --git a/doc/api/settings.md b/doc/api/settings.md
index ce62cd193f7958dc696908e1c4cc58ce6ea62809..d49dca96dfdf33d33f8f4fe60b9345a3fc1fea39 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -328,6 +328,8 @@ listed in the descriptions of the relevant settings.
 | `issues_create_limit`                    | integer          | no                                   | Max number of issue creation requests per minute per user. Disabled by default.|
 | `keep_latest_artifact`                   | boolean          | no                                   | Prevent the deletion of the artifacts from the most recent successful jobs, regardless of the expiry time. Enabled by default. |
 | `local_markdown_version`                 | integer          | no                                   | Increase this value when any cached Markdown should be invalidated. |
+| `mailgun_signing_key`                    | string           | no                                   | The Mailgun HTTP webhook signing key for receiving events from webhook |
+| `mailgun_events_enabled`                 | boolean          | no                                   | Enable Mailgun event receiver. |
 | `maintenance_mode_message`               | string           | no                                   | **(PREMIUM)** Message displayed when instance is in maintenance mode. |
 | `maintenance_mode`                       | boolean          | no                                   | **(PREMIUM)** When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. |
 | `max_artifacts_size`                     | integer          | no                                   | Maximum artifacts size in MB. |
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index b4f8320cb74574eb3390d0dc6737355b5473a9fa..952bf09b1b137f4aaef02937f484f948d269aeeb 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -160,6 +160,10 @@ def filter_attributes_using_license(attrs)
       optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
       optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
       optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
+      optional :mailgun_events_enabled, type: Grape::API::Boolean, desc: 'Enable Mailgun event receiver'
+      given mailgun_events_enabled: ->(val) { val } do
+        requires :mailgun_signing_key, type: String, desc: 'The Mailgun HTTP webhook signing key for receiving events from webhook'
+      end
       optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
       given snowplow_enabled: ->(val) { val } do
         requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname'
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4c7d19131bcb93a6111a615345564bfe077446a9..6419a679fb1f02ca1013839dcdef4c9b1f0292af 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8205,6 +8205,9 @@ msgstr ""
 msgid "Configure storage path settings."
 msgstr ""
 
+msgid "Configure the %{link} integration."
+msgstr ""
+
 msgid "Configure the way a user creates a new account."
 msgstr ""
 
@@ -11935,6 +11938,9 @@ msgstr ""
 msgid "Enable Kroki"
 msgstr ""
 
+msgid "Enable Mailgun event receiver"
+msgstr ""
+
 msgid "Enable PlantUML"
 msgstr ""
 
@@ -19715,6 +19721,15 @@ msgstr ""
 msgid "Made this issue confidential."
 msgstr ""
 
+msgid "Mailgun"
+msgstr ""
+
+msgid "Mailgun HTTP webhook signing key"
+msgstr ""
+
+msgid "Mailgun events"
+msgstr ""
+
 msgid "Maintenance mode"
 msgstr ""
 
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 5a4c47b9dc534b9d61effc704023697e775fe6be..965bda8b4d64a68c14d76b4581e63732eb598e7d 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -269,7 +269,10 @@
     end
 
     context 'Integrations page' do
+      let(:mailgun_events_receiver_enabled) { true }
+
       before do
+        stub_feature_flags(mailgun_events_receiver: mailgun_events_receiver_enabled)
         visit general_admin_application_settings_path
       end
 
@@ -282,6 +285,28 @@
         expect(page).to have_content "Application settings saved successfully"
         expect(current_settings.hide_third_party_offers).to be true
       end
+
+      context 'when mailgun_events_receiver feature flag is enabled' do
+        it 'enabling Mailgun events', :aggregate_failures do
+          page.within('.as-mailgun') do
+            check 'Enable Mailgun event receiver'
+            fill_in 'Mailgun HTTP webhook signing key', with: 'MAILGUN_SIGNING_KEY'
+            click_button 'Save changes'
+          end
+
+          expect(page).to have_content 'Application settings saved successfully'
+          expect(current_settings.mailgun_events_enabled).to be true
+          expect(current_settings.mailgun_signing_key).to eq 'MAILGUN_SIGNING_KEY'
+        end
+      end
+
+      context 'when mailgun_events_receiver feature flag is disabled' do
+        let(:mailgun_events_receiver_enabled) { false }
+
+        it 'does not have mailgun' do
+          expect(page).not_to have_selector('.as-mailgun')
+        end
+      end
     end
 
     context 'when Service Templates are enabled' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index de0027506b9271ff45fb9f6b78142d1f84b67204..80471a09bbd233448c1be93bea478fe03c92f207 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -258,6 +258,19 @@ def many_usernames(num = 100)
       it { is_expected.to allow_value(nil).for(:snowplow_collector_hostname) }
     end
 
+    context 'when mailgun_events_enabled is enabled' do
+      before do
+        setting.mailgun_events_enabled = true
+      end
+
+      it { is_expected.to validate_presence_of(:mailgun_signing_key) }
+      it { is_expected.to validate_length_of(:mailgun_signing_key).is_at_most(255) }
+    end
+
+    context 'when mailgun_events_enabled is not enabled' do
+      it { is_expected.not_to validate_presence_of(:mailgun_signing_key) }
+    end
+
     context "when user accepted let's encrypt terms of service" do
       before do
         expect do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 4a4aeaea7149b194c9b2d16d97eb45887ea680ea..4008b57a1cf228f4e753021b8f31bd8ee541a144 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -127,6 +127,8 @@
             spam_check_endpoint_enabled: true,
             spam_check_endpoint_url: 'grpc://example.com/spam_check',
             spam_check_api_key: 'SPAM_CHECK_API_KEY',
+            mailgun_events_enabled: true,
+            mailgun_signing_key: 'MAILGUN_SIGNING_KEY',
             disabled_oauth_sign_in_sources: 'unknown',
             import_sources: 'github,bitbucket',
             wiki_page_max_content_bytes: 12345,
@@ -175,6 +177,8 @@
         expect(json_response['spam_check_endpoint_enabled']).to be_truthy
         expect(json_response['spam_check_endpoint_url']).to eq('grpc://example.com/spam_check')
         expect(json_response['spam_check_api_key']).to eq('SPAM_CHECK_API_KEY')
+        expect(json_response['mailgun_events_enabled']).to be(true)
+        expect(json_response['mailgun_signing_key']).to eq('MAILGUN_SIGNING_KEY')
         expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
         expect(json_response['import_sources']).to match_array(%w(github bitbucket))
         expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
@@ -493,6 +497,15 @@
       end
     end
 
+    context "missing mailgun_signing_key value when mailgun_events_enabled is true" do
+      it "returns a blank parameter error message" do
+        put api("/application/settings", admin), params: { mailgun_events_enabled: true }
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['error']).to eq('mailgun_signing_key is missing')
+      end
+    end
+
     context "personal access token prefix settings" do
       context "handles validation errors" do
         it "fails to update the settings with too long prefix" do