diff --git a/Gemfile b/Gemfile
index 19432758b34e0597015c1678a8c06e335bc16726..d20dbe7c2fd71e81aaf7b3fc7ca970198545d21d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -60,6 +60,8 @@ gem 'u2f', '~> 0.2.1'
 # GitLab Pages
 gem 'validates_hostname', '~> 1.0.6'
 gem 'rubyzip', '~> 1.2.2', require: 'zip'
+# GitLab Pages letsencrypt support
+gem 'acme-client', '~> 2.0.2'
 
 # Browser detection
 gem 'browser', '~> 2.5'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1bd88b651247989d2d819b7b352f094aa0454d3c..482352e59d53a2f69f2b3a0fb7a2cb09602e77fd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,6 +4,8 @@ GEM
     RedCloth (4.3.2)
     abstract_type (0.0.7)
     ace-rails-ap (4.1.2)
+    acme-client (2.0.2)
+      faraday (~> 0.9, >= 0.9.1)
     actioncable (5.1.7)
       actionpack (= 5.1.7)
       nio4r (~> 2.0)
@@ -998,6 +1000,7 @@ PLATFORMS
 DEPENDENCIES
   RedCloth (~> 4.3.2)
   ace-rails-ap (~> 4.1.0)
+  acme-client (~> 2.0.2)
   activerecord_sane_schema_dumper (= 1.0)
   acts-as-taggable-on (~> 6.0)
   addressable (~> 2.5.2)
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index d445be0eb19002b551e18851a2d32c2b57cb361c..d5bc723aa8cd14149ae896d58176f792628f299b 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -89,6 +89,13 @@ def clear_repository_check_states
     )
   end
 
+  # Getting ToS url requires `directory` api call to Let's Encrypt
+  # which could result in 500 error/slow rendering on settings page
+  # Because of that we use separate controller action
+  def lets_encrypt_terms_of_service
+    redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
+  end
+
   private
 
   def set_application_setting
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 64e01fa2d0004755777cd65b3c441a0d4002c02f..77795dbf913dfd161085da8ef374cdb8f652c681 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -30,8 +30,7 @@
         .form-check
           = f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
           = f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
-            // Terms of Service should actually be a link, but the best way to get the url is using API
-            // So it will be done in later MR
-            = _("I have read and agree to the Let's Encrypt Terms of Service")
+            - terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path }
+            = _("I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe }
 
   = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 90d7f4a04d46be9e150d9b8d722aedfb65b28e4b..bc19219a0b8b8e14032c4f43dfb9d360a98f875d 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -111,6 +111,7 @@
     put :reset_health_check_token
     put :clear_repository_check_states
     get :integrations, :repository, :templates, :ci_cd, :reporting, :metrics_and_profiling, :network, :geo, :preferences
+    get :lets_encrypt_terms_of_service
   end
 
   resources :labels
diff --git a/lib/gitlab/lets_encrypt/challenge.rb b/lib/gitlab/lets_encrypt/challenge.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6a7f5e965c549e2a97b811b9d5d15a8f9f83b640
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/challenge.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module LetsEncrypt
+    class Challenge
+      def initialize(acme_challenge)
+        @acme_challenge = acme_challenge
+      end
+
+      delegate :url, :token, :file_content, :status, :request_validation, to: :acme_challenge
+
+      private
+
+      attr_reader :acme_challenge
+    end
+  end
+end
diff --git a/lib/gitlab/lets_encrypt/client.rb b/lib/gitlab/lets_encrypt/client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d7468b067675ddbcac4053a891e4cf9ffc4e5ebf
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/client.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module LetsEncrypt
+    class Client
+      PRODUCTION_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory'
+      STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory'
+
+      def new_order(domain_name)
+        ensure_account
+
+        acme_order = acme_client.new_order(identifiers: [domain_name])
+
+        ::Gitlab::LetsEncrypt::Order.new(acme_order)
+      end
+
+      def load_order(url)
+        ensure_account
+
+        # rubocop: disable CodeReuse/ActiveRecord
+        ::Gitlab::LetsEncrypt::Order.new(acme_client.order(url: url))
+        # rubocop: enable CodeReuse/ActiveRecord
+      end
+
+      def load_challenge(url)
+        ensure_account
+
+        ::Gitlab::LetsEncrypt::Challenge.new(acme_client.challenge(url: url))
+      end
+
+      def terms_of_service_url
+        acme_client.terms_of_service
+      end
+
+      def enabled?
+        return false unless Feature.enabled?(:pages_auto_ssl)
+
+        Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted
+      end
+
+      private
+
+      def acme_client
+        @acme_client ||= ::Acme::Client.new(private_key: private_key, directory: acme_api_directory_url)
+      end
+
+      def private_key
+        @private_key ||= OpenSSL::PKey.read(Gitlab::Application.secrets.lets_encrypt_private_key)
+      end
+
+      def admin_email
+        Gitlab::CurrentSettings.lets_encrypt_notification_email
+      end
+
+      def contact
+        "mailto:#{admin_email}"
+      end
+
+      def ensure_account
+        raise 'Acme integration is disabled' unless enabled?
+
+        @acme_account ||= acme_client.new_account(contact: contact, terms_of_service_agreed: true)
+      end
+
+      def acme_api_directory_url
+        if Rails.env.production?
+          PRODUCTION_DIRECTORY_URL
+        else
+          STAGING_DIRECTORY_URL
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/lets_encrypt/order.rb b/lib/gitlab/lets_encrypt/order.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5109b5e9843a23c9d07d58528f6b430411cb6ffd
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/order.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module LetsEncrypt
+    class Order
+      def initialize(acme_order)
+        @acme_order = acme_order
+      end
+
+      def new_challenge
+        authorization = @acme_order.authorizations.first
+        challenge = authorization.http
+        ::Gitlab::LetsEncrypt::Challenge.new(challenge)
+      end
+
+      delegate :url, :status, to: :acme_order
+
+      private
+
+      attr_reader :acme_order
+    end
+  end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 078a398045d7bdb99aa087f98df5c03e81c60ef0..68397c38700ff8dd3c2f3afbd9c4f5fdf979012f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4905,7 +4905,7 @@ msgstr ""
 msgid "I accept the|Terms of Service and Privacy Policy"
 msgstr ""
 
-msgid "I have read and agree to the Let's Encrypt Terms of Service"
+msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}"
 msgstr ""
 
 msgid "ID"
diff --git a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..74622f356de88871dfb9516b025b325b2dd22ef8
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Challenge do
+  delegated_methods = {
+    url: 'https://example.com/',
+    status: 'pending',
+    token: 'tokenvalue',
+    file_content: 'hereisfilecontent',
+    request_validation: true
+  }
+
+  let(:acme_challenge) do
+    acme_challenge = instance_double('Acme::Client::Resources::Challenge')
+    allow(acme_challenge).to receive_messages(delegated_methods)
+    acme_challenge
+  end
+
+  let(:challenge) { described_class.new(acme_challenge) }
+
+  delegated_methods.each do |method, value|
+    describe "##{method}" do
+      it 'delegates to Acme::Client::Resources::Challenge' do
+        expect(challenge.public_send(method)).to eq(value)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..16a16acfd25750488da9a07c974d19e905e4a7ee
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Client do
+  include LetsEncryptHelpers
+
+  let(:client) { described_class.new }
+
+  before do
+    stub_application_setting(
+      lets_encrypt_notification_email: 'myemail@test.example.com',
+      lets_encrypt_terms_of_service_accepted: true
+    )
+  end
+
+  let!(:stub_client) { stub_lets_encrypt_client }
+
+  shared_examples 'ensures account registration' do
+    it 'ensures account registration' do
+      subject
+
+      expect(stub_client).to have_received(:new_account).with(
+        contact: 'mailto:myemail@test.example.com',
+        terms_of_service_agreed: true
+      )
+    end
+
+    context 'when acme integration is disabled' do
+      before do
+        stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+      end
+
+      it 'raises error' do
+        expect do
+          subject
+        end.to raise_error('Acme integration is disabled')
+      end
+    end
+  end
+
+  describe '#new_order' do
+    subject(:new_order) { client.new_order('example.com') }
+
+    before do
+      order_double = instance_double('Acme::Order')
+      allow(stub_client).to receive(:new_order).and_return(order_double)
+    end
+
+    include_examples 'ensures account registration'
+
+    it 'returns order' do
+      is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+    end
+  end
+
+  describe '#load_order' do
+    let(:url) { 'https://example.com/order' }
+    subject { client.load_order(url) }
+
+    before do
+      acme_order = instance_double('Acme::Client::Resources::Order')
+      allow(stub_client).to receive(:order).with(url: url).and_return(acme_order)
+    end
+
+    include_examples 'ensures account registration'
+
+    it 'loads order' do
+      is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+    end
+  end
+
+  describe '#load_challenge' do
+    let(:url) { 'https://example.com/challenge' }
+    subject { client.load_challenge(url) }
+
+    before do
+      acme_challenge = instance_double('Acme::Client::Resources::Challenge')
+      allow(stub_client).to receive(:challenge).with(url: url).and_return(acme_challenge)
+    end
+
+    include_examples 'ensures account registration'
+
+    it 'loads challenge' do
+      is_expected.to be_a(::Gitlab::LetsEncrypt::Challenge)
+    end
+  end
+
+  describe '#enabled?' do
+    subject { client.enabled? }
+
+    context 'when terms of service are accepted' do
+      it { is_expected.to eq(true) }
+
+      context 'when feature flag is disabled' do
+        before do
+          stub_feature_flags(pages_auto_ssl: false)
+        end
+
+        it { is_expected.to eq(false) }
+      end
+    end
+
+    context 'when terms of service are not accepted' do
+      before do
+        stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+      end
+
+      it { is_expected.to eq(false) }
+    end
+  end
+
+  describe '#terms_of_service_url' do
+    subject { client.terms_of_service_url }
+
+    it 'returns valid url' do
+      is_expected.to eq("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
+    end
+  end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/order_spec.rb b/spec/lib/gitlab/lets_encrypt/order_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee7058baf8dfa33f8052ef4c110db5a2c175831d
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/order_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Order do
+  delegated_methods = {
+    url: 'https://example.com/',
+    status: 'valid'
+  }
+
+  let(:acme_order) do
+    acme_order = instance_double('Acme::Client::Resources::Order')
+    allow(acme_order).to receive_messages(delegated_methods)
+    acme_order
+  end
+
+  let(:order) { described_class.new(acme_order) }
+
+  delegated_methods.each do |method, value|
+    describe "##{method}" do
+      it 'delegates to Acme::Client::Resources::Order' do
+        expect(order.public_send(method)).to eq(value)
+      end
+    end
+  end
+
+  describe '#new_challenge' do
+    before do
+      challenge = instance_double('Acme::Client::Resources::Challenges::HTTP01')
+      authorization = instance_double('Acme::Client::Resources::Authorization')
+      allow(authorization).to receive(:http).and_return(challenge)
+      allow(acme_order).to receive(:authorizations).and_return([authorization])
+    end
+
+    it 'returns challenge' do
+      expect(order.new_challenge).to be_a(::Gitlab::LetsEncrypt::Challenge)
+    end
+  end
+end
diff --git a/spec/support/helpers/lets_encrypt_helpers.rb b/spec/support/helpers/lets_encrypt_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7f0886b451c1d8722847ac65a962fe921fbcc14e
--- /dev/null
+++ b/spec/support/helpers/lets_encrypt_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module LetsEncryptHelpers
+  def stub_lets_encrypt_client
+    client = instance_double('Acme::Client')
+
+    allow(client).to receive(:new_account)
+    allow(client).to receive(:terms_of_service).and_return(
+      "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf"
+    )
+
+    allow(Acme::Client).to receive(:new).with(
+      private_key: kind_of(OpenSSL::PKey::RSA),
+      directory: ::Gitlab::LetsEncrypt::Client::STAGING_DIRECTORY_URL
+    ).and_return(client)
+
+    client
+  end
+end