apartment_acme_client 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9e32d511e21e812a21bae30429bb7d11a0cdc62c878df8a73ffad85d0f38821
4
- data.tar.gz: 1b33b1bf1e99fdc674198994b76acec4fe302f625a59a712051c28b2c6a0ab50
3
+ metadata.gz: 66eaebf9ae761526e149a5bedd237deee499a95107edb9ce3c1e3916e29c058b
4
+ data.tar.gz: 4ab90728f136c38b453a24f54b760c84539c4333ae0cb24ba4122dcb54c124b6
5
5
  SHA512:
6
- metadata.gz: 52706bcc1e28859eeb2ce40391a31512d3382c4cc00726f4deb9cd9d974f464e7aca378b0a68974dadccc3f660ede923740c87938dd54a7b2200962c89f44db8
7
- data.tar.gz: 12dc42f69d30fba1d42bdb5415249730b9e57c61a5159a25249a1db519266c3f702a53fea6c6f93fcb1136a1071ff8818ffa34e6b0acf4ae34e3c2f1541b7e63
6
+ metadata.gz: ff22698eadfef2b8b2e6d0c25793ad5bb1228ff66262c039ef01b969aabb42458caf4d3e6f0f8485a4eca5273b878328661437fe4591f6d809317a44811f1ae1
7
+ data.tar.gz: 870ed58bb9befb87b788fa1976f4fd73b052982cc1b57be889c2783a1ec55372ce24d155b286fd5937c24bb910bbb69ece5cf101b3e7e298d8117adc51d7f100
data/README.md CHANGED
@@ -15,29 +15,27 @@ The goal of this gem is to solve the following problems:
15
15
 
16
16
  1) Make it easy to start using let's encrypt for multiple domains on one server
17
17
  1) Make it easy to periodically refresh a certificate which handles many domains
18
- 1) Make it possible to add a new DNS entry which refers to the server, and request a cert which now also covers that new domain.
18
+ 1) Make it possible to add a new custom DNS entry which refers to the server, and request a cert which now also covers that new domain.
19
+ 1) Make it easy to request a wildcard cert as well as individual domain certs
19
20
  1) Make it resilient, if a DNS record is removed, handle that by removing that domain from list requested for the cert.
20
21
 
21
22
  **Example Situation**:
22
23
 
23
- - Your application is known as example.com
24
+ - Your application is known as site.example.com
24
25
  - You allow users to create new accounts, and assign each account a separate subdomain,
25
- - e.g. alice.example.com, bob.example.com, charlie.example.com
26
+ - e.g. alice.site.example.com, bob.site.example.com, charlie.site.example.com
26
27
  - You allow users to also whitelabel the service by buying their own domains, and setting up CNAME records:
27
- - e.g. www.alice.com -> CNAME: alice.example.com
28
- - e.g. bobrocks.com -> CNAME: bob.example.com
28
+ - e.g. www.alice.com -> CNAME: alice.site.example.com
29
+ - e.g. bobrocks.com -> CNAME: bob.site.example.com
29
30
 
30
31
  **What can ApartmentAcmeClient do?**
31
32
 
32
33
  - Create a single Let's Encrypt SSL Certificate which covers all of:
33
- - example.com
34
- - alice.example.com
35
- - bob.example.com
36
- - charlie.example.com
34
+ - site.example.com
35
+ - *.site.example.com (which covers alice.site.example.com, bob.site.example.com, charlie.site.example.com)
37
36
  - www.alice.com
38
37
  - bobrocks.com
39
38
 
40
-
41
39
  SSL Certificates
42
40
  ----------------
43
41
 
@@ -70,6 +68,8 @@ See below for a detailed explanation of "First Time Setup"
70
68
 
71
69
  When setting this up the first time, it is recommended that you enable test-mode:
72
70
  ```ruby
71
+ # in config/initializers/apartment_acme_client.rb
72
+
73
73
  ApartmentAcmeClient.lets_encrypt_test_server_enabled = true
74
74
  ```
75
75
 
@@ -131,12 +131,27 @@ Define the code which will list the domains to check.
131
131
  # Should return an array of domains (without http/https prefixes)
132
132
 
133
133
  # It can be a straight array, or a callable object
134
- ApartmentAcmeClient.domains_to_check = -> { SomeModel.all.map(&:subdomain) }
134
+ # These should be all of the domains which are NOT
135
+ # covered by the wildcard settings
136
+ ApartmentAcmeClient.domains_to_check = -> { SomeModel.all.map(&:custom_domain) }
137
+ ApartmentAcmeClient.wildcard_domain = "site.example.com" # optional element
135
138
 
136
139
  # e.g.
137
140
  # ApartmentAcmeClient.domains_to_check = ["example.com", "alice.example.com", "alice.com"]
138
141
  ```
139
142
 
143
+ #### Wildcard domain
144
+
145
+ You can request a wildcard certificate for a domain (or a subdomain). In order to do this, the system must be able to write to the DNS provider.
146
+
147
+ Currently, only Route53 is supported as a DNS provider, and we use an `upsert` to write a TXT record to the system, in order to prove that we control the DNS for the domain.
148
+
149
+ If you specify `wildcard_domain` (the domain on which to request a wildcard cert), we will request a wilcard cert for `*.<wildcard_domain>`, and use AWS Route53 API to perform the domain-authorization.
150
+
151
+ The necessary permissions to be able to update the Route53 records for wildcard-cert update are:
152
+ - route53:ListHostedZones
153
+ - route53:ChangeResourceRecordSets
154
+
140
155
  #### Specify the common-name domain
141
156
 
142
157
  This is used to identify the certificate requested, and should be the same from week-to-week.
@@ -254,15 +269,14 @@ At this point, the only thing necessary is to run `rake encryption:renew_and_upd
254
269
 
255
270
  Each week, the certificates should be renewed. We have provided 2 ways to do this.
256
271
 
257
- a rake task:
272
+ straight invocation:
258
273
  ```ruby
259
- rake "encryption:renew_and_update_certificate"
274
+ ApartmentAcmeClient::RenewalService.run!
260
275
  ```
261
276
 
262
- straight invocation:
263
-
277
+ we provide a helper rake task:
264
278
  ```ruby
265
- ApartmentAcmeClient::RenewalService.run!
279
+ rake "encryption:renew_and_update_certificate"
266
280
  ```
267
281
 
268
282
  Please use whatever scheduling service you wish in order to ensure that this runs periodically.
data/Rakefile CHANGED
@@ -17,7 +17,6 @@ end
17
17
  APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
18
  load 'rails/tasks/engine.rake'
19
19
 
20
-
21
20
  load 'rails/tasks/statistics.rake'
22
21
 
23
22
  require 'bundler/gem_tasks'
@@ -7,6 +7,9 @@ require "apartment_acme_client/acme_client/proxy"
7
7
  require "apartment_acme_client/acme_client/real_client"
8
8
  require "apartment_acme_client/certificate_storage/proxy"
9
9
  require "apartment_acme_client/certificate_storage/s3"
10
+ require "apartment_acme_client/dns_api/check_dns"
11
+ require "apartment_acme_client/dns_api/fake"
12
+ require "apartment_acme_client/dns_api/route53"
10
13
  require "apartment_acme_client/nginx_configuration/proxy"
11
14
  require "apartment_acme_client/nginx_configuration/real"
12
15
  require "apartment_acme_client/file_manipulation/proxy"
@@ -27,6 +30,9 @@ module ApartmentAcmeClient
27
30
  end
28
31
  end
29
32
 
33
+ # An optional domain which will we request a wildcard certificate for
34
+ mattr_accessor :wildcard_domain
35
+
30
36
  # The base domain, a domain which is always going to be accessible.
31
37
  # because we need a common domain to be used on each request.
32
38
  # if not defined, the first 'domain_to_check' which succeeds will be used
@@ -1,21 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'acme-client'
2
4
 
3
5
  module ApartmentAcmeClient
4
6
  module AcmeClient
5
7
  class RealClient
6
- def initialize(private_key:)
8
+ attr_reader :csr_private_key
9
+ def initialize(acme_client_private_key:, csr_private_key:)
7
10
  @client = Acme::Client.new(
8
- private_key: private_key,
9
- endpoint: server_endpoint,
11
+ private_key: acme_client_private_key,
12
+ directory: server_directory
10
13
  )
14
+ @csr_private_key = csr_private_key
11
15
  end
12
16
 
13
17
  def register(email)
14
18
  # If the private key is not known to the server, we need to register it for the first time.
15
- registration = @client.register(contact: "mailto:#{email}")
16
-
17
- # You may need to agree to the terms of service (that's up the to the server to require it or not but boulder does by default)
18
- registration.agree_terms
19
+ account = @client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
20
+ Rollbar.info("New Let's Encrypt Account created with KID: #{account.kid}")
19
21
 
20
22
  true
21
23
  end
@@ -24,31 +26,35 @@ module ApartmentAcmeClient
24
26
  @client.authorize(domain: domain)
25
27
  end
26
28
 
29
+ def new_order(identifiers:)
30
+ @client.new_order(identifiers: identifiers)
31
+ end
32
+
27
33
  # Create a Certificate for our new set of domain names
28
34
  # returns that certificate
29
- def request_certificate(common_name:, domains:)
35
+ def request_certificate(common_name:, names:, order:)
30
36
  # We're going to need a certificate signing request. If not explicitly
31
37
  # specified, the first name listed becomes the common name.
32
- csr = Acme::Client::CertificateRequest.new(common_name: common_name, names: domains)
33
-
34
- # We can now request a certificate. You can pass anything that returns
35
- # a valid DER encoded CSR when calling to_der on it. For example an
36
- # OpenSSL::X509::Request should work too.
37
- certificate = @client.new_certificate(csr) # => #<Acme::Client::Certificate ....>
38
+ csr = Acme::Client::CertificateRequest.new(private_key: csr_private_key, common_name: common_name, names: names)
39
+ order.finalize(csr: csr)
40
+ while order.status == 'processing'
41
+ sleep(1)
42
+ order.reload
43
+ end
38
44
 
39
- certificate
45
+ order.certificate
40
46
  end
41
47
 
42
48
  private
43
49
 
44
- def server_endpoint
50
+ def server_directory
45
51
  # We need an ACME server to talk to, see github.com/letsencrypt/boulder
46
52
  # WARNING: This endpoint is the production endpoint, which is rate limited and will produce valid certificates.
47
53
  # You should probably use the staging endpoint for all your experimentation:
48
54
  if ApartmentAcmeClient.lets_encrypt_test_server_enabled
49
- 'https://acme-staging.api.letsencrypt.org/'
55
+ 'https://acme-staging-v02.api.letsencrypt.org/directory'
50
56
  else
51
- 'https://acme-v01.api.letsencrypt.org/'
57
+ 'https://acme-v02.api.letsencrypt.org/directory'
52
58
  end
53
59
  end
54
60
  end
@@ -1,36 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'aws-sdk-s3'
2
4
 
3
5
  module ApartmentAcmeClient
4
6
  module CertificateStorage
5
7
  class S3
6
8
  def initialize
7
- @base_prefix = if ApartmentAcmeClient::lets_encrypt_test_server_enabled
8
- TEST_PREFIX
9
- else
10
- ""
11
- end
9
+ @base_prefix = if ApartmentAcmeClient.lets_encrypt_test_server_enabled
10
+ TEST_PREFIX
11
+ else
12
+ ''
13
+ end
12
14
  end
13
15
 
14
- ENCRYPTION_S3_NAME = 'server_encryption_client_private_key.der'.freeze
16
+ ENCRYPTION_S3_NAME = 'server_encryption_client_private_key.der'
17
+ CSR_ENCRYPTION_S3_NAME = 'csr_server_encryption_client_private_key.der'
15
18
 
16
- def store_certificate(certificate)
17
- # Save the certificate and the private key to files
18
- File.write(cert_path("privkey.pem"), certificate.request.private_key.to_pem)
19
- File.write(cert_path("cert.pem"), certificate.to_pem)
20
- File.write(cert_path("chain.pem"), certificate.chain_to_pem)
21
- File.write(cert_path("fullchain.pem"), certificate.fullchain_to_pem)
19
+ def store_certificate_string(certificate_string)
20
+ File.write(cert_path('cert.pem'), certificate_string)
21
+ store_s3_file(derived_filename('cert.pem'), certificate_string)
22
+ end
22
23
 
23
- store_s3_file(derived_filename("privkey.pem"), certificate.request.private_key.to_pem)
24
- store_s3_file(derived_filename("cert.pem"), certificate.to_pem)
25
- store_s3_file(derived_filename("chain.pem"), certificate.chain_to_pem)
26
- store_s3_file(derived_filename("fullchain.pem"), certificate.fullchain_to_pem)
24
+ def store_csr_private_key_string(csr_private_key_string)
25
+ File.write(cert_path('privkey.pem'), csr_private_key_string)
26
+ store_s3_file(derived_filename('privkey.pem'), csr_private_key_string)
27
27
  end
28
28
 
29
29
  # do we have a certificate on this server?
30
30
  # We cannot start nginx when it is pointing at a non-existing certificate,
31
31
  # so we need to check
32
32
  def cert_exists?
33
- File.exist?(cert_path("privkey.pem"))
33
+ File.exist?(cert_path('privkey.pem'))
34
34
  end
35
35
 
36
36
  def private_key
@@ -40,17 +40,32 @@ module ApartmentAcmeClient
40
40
  s3_object.get.body.read
41
41
  end
42
42
 
43
+ def csr_private_key
44
+ s3_object = s3_file(csr_private_key_s3_filename)
45
+ return nil unless s3_object.exists?
46
+
47
+ s3_object.get.body.read
48
+ end
49
+
43
50
  # saves a private key to s3
44
51
  def save_private_key(private_key)
45
52
  store_s3_file(private_key_s3_filename, private_key.to_der)
46
53
  end
47
54
 
55
+ def save_csr_private_key(private_key)
56
+ store_s3_file(csr_private_key_s3_filename, private_key.to_der)
57
+ end
58
+
48
59
  private
49
60
 
50
61
  def private_key_s3_filename
51
62
  derived_filename(ENCRYPTION_S3_NAME)
52
63
  end
53
64
 
65
+ def csr_private_key_s3_filename
66
+ derived_filename(CSR_ENCRYPTION_S3_NAME)
67
+ end
68
+
54
69
  def derived_filename(filename)
55
70
  "#{@base_prefix}#{filename}"
56
71
  end
@@ -0,0 +1,65 @@
1
+ module ApartmentAcmeClient
2
+ module DnsApi
3
+ # Check to see if a particular DNS record is
4
+ # present.
5
+ class CheckDns
6
+ attr_reader :root_domain, :dns_record
7
+
8
+ def initialize(root_domain, dns_record)
9
+ # ensure we only have the TLD, not a subdomain
10
+ @root_domain = root_domain.split(".").last(2).join(".")
11
+ @dns_record = dns_record
12
+ end
13
+
14
+ # Search DNS recodrs for any entries which are TXT and include
15
+ # the text 'value'
16
+ def check_dns(value)
17
+ valid = true
18
+
19
+ nameservers.each do |nameserver|
20
+ begin
21
+ records = Resolv::DNS.open(nameserver: nameserver) do |dns|
22
+ dns.getresources(
23
+ dns_record,
24
+ Resolv::DNS::Resource::IN::TXT
25
+ )
26
+ end
27
+ records = records.map(&:strings).flatten
28
+ valid = records.include?(value)
29
+ rescue Resolv::ResolvError
30
+ return false
31
+ end
32
+ return false unless valid
33
+ end
34
+
35
+ valid
36
+ end
37
+
38
+ def nameservers
39
+ return @nameservers if defined?(@nameservers)
40
+
41
+ @nameservers = []
42
+ Resolv::DNS.open(nameserver: '8.8.8.8') do |dns|
43
+ while nameservers.empty?
44
+ @nameservers = dns.getresources(
45
+ root_domain,
46
+ Resolv::DNS::Resource::IN::NS
47
+ ).map(&:name).map(&:to_s)
48
+ end
49
+ end
50
+
51
+ @nameservers
52
+ end
53
+
54
+ def wait_for_present(value, timeout_seconds: 60)
55
+ time = 1
56
+ until check_dns(value)
57
+ puts "Waiting for DNS to update"
58
+ sleep 1
59
+ time += 1
60
+ break if time > timeout_seconds
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,6 @@
1
+ module ApartmentAcmeClient
2
+ module DnsApi
3
+ class Fake
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,90 @@
1
+ require 'aws-sdk-route53'
2
+
3
+ module ApartmentAcmeClient
4
+ module DnsApi
5
+ # based on https://www.petekeen.net/lets-encrypt-without-certbot
6
+ class Route53
7
+ # The domain being requested for DNS update
8
+ # e.g. "site.example.com"
9
+ attr_reader :requested_domain
10
+
11
+ # the DNS TXT record label (full label, including domain)
12
+ attr_reader :label
13
+
14
+ # will be TXT
15
+ attr_reader :record_type
16
+
17
+ # array of value keys to be written
18
+ # (for wildcard certs, you'll have one for *.example.com, and one for example.com)
19
+ # e.g. ["One", "Two"]
20
+ attr_reader :values
21
+
22
+ def initialize(requested_domain:, dns_record_label:, record_type:, values:)
23
+ @requested_domain = requested_domain
24
+ @label = dns_record_label
25
+ @record_type = record_type
26
+ @values = values
27
+ end
28
+
29
+ # NOTE:
30
+ # if you get error like:
31
+ #
32
+ # "Invalid Resource Record: FATAL problem:
33
+ # InvalidCharacterString
34
+ # (Value should be enclosed in quotation marks) encountered with <value>"
35
+ #
36
+ # this means that the "Value" should include escape quotes.
37
+ # e.g. values: ["\"Something\"", "\"Other Thing\""]
38
+ def write_record
39
+ route53.change_resource_record_sets(options)
40
+ end
41
+
42
+ private
43
+
44
+ def options
45
+ change = {
46
+ action: 'UPSERT',
47
+ resource_record_set: {
48
+ name: label,
49
+ type: record_type,
50
+ ttl: 1,
51
+ resource_records: resource_record_values
52
+ }
53
+ }
54
+
55
+ {
56
+ hosted_zone_id: zone.id,
57
+ change_batch: {
58
+ changes: [change]
59
+ }
60
+ }
61
+ end
62
+
63
+ def root_domain
64
+ requested_domain.split(".").last(2).join(".")
65
+ end
66
+
67
+ def zone
68
+ @zone = route53.list_hosted_zones(max_items: 100)
69
+ .hosted_zones
70
+ .detect { |z| z.name = "#{root_domain}." }
71
+ end
72
+
73
+ def route53
74
+ # Note: The `region` doesn't matter, because Route53 is global.
75
+ @route53 ||= Aws::Route53::Client.new(region: 'us-east-1')
76
+ end
77
+
78
+ # createt an AwsRoute53 upsert with multiple value entries
79
+ def resource_record_values
80
+ values.map do |value|
81
+ if value.include?("\"")
82
+ { value: value }
83
+ else
84
+ { value: "\"#{value}\"" }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
 
3
5
  # Initially, the system is only accessible via subdomain.example.com
4
6
  # But, as we add more Conventions, we want to be able to access those also,
5
7
  # thus we will need:
6
- # - a.subdomain.example.com
7
- # - b.subdomain.example.com
8
- # - c.subdomain.example.com
8
+ # - *.subdomain.example.com
9
9
  #
10
10
  # Also, each convention may add an "alias" for their convention, like:
11
11
  # - www.naucc.com
@@ -21,20 +21,21 @@ require 'openssl'
21
21
  #
22
22
  # Manage the encryption of the website (https).
23
23
  module ApartmentAcmeClient
24
- class Encryption
24
+ class Encryption # rubocop:disable Metrics/ClassLength
25
25
  def initialize
26
26
  @certificate_storage = ApartmentAcmeClient::CertificateStorage::Proxy.singleton
27
27
  end
28
28
 
29
29
  # Largely based on https://github.com/unixcharles/acme-client documentation
30
30
  def register_new(email)
31
- raise StandardError.new("Private key already exists") unless @certificate_storage.private_key.nil?
31
+ raise StandardError.new('Private key already exists') unless @certificate_storage.private_key.nil?
32
32
 
33
33
  private_key = create_private_key
34
34
 
35
35
  # Initialize the client
36
36
  new_client = ApartmentAcmeClient::AcmeClient::Proxy.singleton(
37
- private_key: private_key,
37
+ acme_client_private_key: private_key,
38
+ csr_private_key: nil, # not needed for 'register' call
38
39
  )
39
40
 
40
41
  new_client.register(email)
@@ -42,22 +43,75 @@ module ApartmentAcmeClient
42
43
  @certificate_storage.save_private_key(private_key)
43
44
  end
44
45
 
45
- def authorize_domains(domains)
46
- successful_domains = domains.select {|domain| authorize_domain(domain) }
47
- successful_domains
46
+ # Authorize a wildcard cert domain.
47
+ # to do this, we have to write to the Amazon Route53 DNS entry
48
+ # params:
49
+ # - authorizations - a list of authorizations, which may be http or dns based (ignore the non-wildcard ones)
50
+ # - wildcard_domain - the url of the wildcard's base domain (e.g. "site.example.com")
51
+ def authorize_domains_with_dns(authorizations, wildcard_domain:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
52
+ label = nil
53
+ record_type = nil
54
+ values = []
55
+
56
+ dns_authorizations = []
57
+ authorizations.each do |domain_authorization|
58
+ next unless domain_authorization.wildcard || domain_authorization.http.nil?
59
+
60
+ dns_authorizations << domain_authorization.dns
61
+ end
62
+
63
+ dns_authorizations.each do |authorization|
64
+ label = "#{authorization.record_name}.#{wildcard_domain}"
65
+ record_type = authorization.record_type
66
+ value = authorization.record_content
67
+ values << value
68
+ end
69
+
70
+ return unless values.any?
71
+
72
+ route53 = ApartmentAcmeClient::DnsApi::Route53.new(
73
+ requested_domain: wildcard_domain,
74
+ dns_record_label: label,
75
+ record_type: record_type,
76
+ values: values
77
+ )
78
+
79
+ route53.write_record
80
+
81
+ check_dns = ApartmentAcmeClient::DnsApi::CheckDns.new(wildcard_domain, label)
82
+
83
+ check_dns.wait_for_present(values.first)
84
+
85
+ if check_dns.check_dns(values.first)
86
+ # DNS is updated, proceed with cert request
87
+ dns_authorizations.each do |domain_authorization|
88
+ domain_authorization.request_validation
89
+
90
+ 30.times do
91
+ # may be 'pending' initially
92
+ break if domain_authorization.status == 'valid'
93
+
94
+ puts "Waiting for LetsEncrypt to authorize the domain. Status #{domain_authorization.status}"
95
+
96
+ # Wait a bit for the server to make the request, or just blink. It should be fast.
97
+ sleep(2)
98
+ domain_authorization.reload
99
+ end
100
+ end
101
+ else
102
+ # ERROR, DNS not updated in time
103
+ Rollbar.error("DNS Entry not found in timeout")
104
+ end
48
105
  end
49
106
 
50
- # authorizes a domain with letsencrypt server
107
+ # authorizes a single domain with letsencrypt server
51
108
  # returns true on success, false otherwise.
52
109
  #
53
110
  # from https://github.com/unixcharles/acme-client/tree/master#authorize-for-domain
54
- def authorize_domain(domain)
55
- authorization = client.authorize(domain: domain)
111
+ def authorize_domain_with_http(domain_authorization)
112
+ challenge = domain_authorization.http
56
113
 
57
- # This example is using the http-01 challenge type. Other challenges are dns-01 or tls-sni-01.
58
- challenge = authorization.http01
59
-
60
- # The http-01 method will require you to respond to a HTTP request.
114
+ # The http method will require you to respond to a HTTP request.
61
115
 
62
116
  # You can retrieve the challenge token
63
117
  challenge.token # => "some_token"
@@ -90,43 +144,81 @@ module ApartmentAcmeClient
90
144
  # challenge = client.challenge_from_hash(JSON.parse(File.read('challenge')))
91
145
 
92
146
  # Once you are ready to serve the confirmation request you can proceed.
93
- challenge.request_verification # => true
147
+ challenge.request_validation # => true
94
148
 
95
- success = false
96
- 10.times do
97
- if challenge.verify_status == 'valid' # may be 'pending' initially
98
- success = true
99
- break
100
- end
149
+ 30.times do
150
+ # may be 'pending' initially
151
+ break if challenge.status == 'valid'
152
+
153
+ puts "Waiting for letsencrypt to authorize the single domain. Status: #{challenge.status}"
101
154
 
102
155
  # Wait a bit for the server to make the request, or just blink. It should be fast.
103
- sleep(1)
156
+ sleep(2)
157
+ challenge.reload
104
158
  end
105
159
  File.delete(full_challenge_filename)
106
160
 
107
- success
161
+ challenge.status == 'valid'
162
+ end
163
+
164
+ # Create an order, perform authorization for each domain, and then
165
+ # request the certificate.
166
+ # - common name is used so that there is continuity of requests over time
167
+ # - domains are the list of individual http-based domains to be authorized
168
+ # - wildcard_domain is an optional wildcard domain to be authorized via DNS Record
169
+ #
170
+ # Returns the certificate
171
+ def request_certificate(common_name:, domains:, wildcard_domain: nil)
172
+ domain_names_requested = domains
173
+ domain_names_requested += [wildcard_domain, "*.#{wildcard_domain}"] if wildcard_domain.present?
174
+ order = client.new_order(identifiers: domain_names_requested)
175
+
176
+ # Do the HTTP authorizations
177
+ order.authorizations.each do |authorization|
178
+ next if authorization.wildcard || authorization.http.nil?
179
+
180
+ authorize_domain_with_http(authorization)
181
+ end
182
+ # Do the DNS (wildcard) authorizations
183
+ authorize_domains_with_dns(order.authorizations, wildcard_domain: wildcard_domain)
184
+
185
+ client.request_certificate(common_name: common_name, names: domain_names_requested, order: order)
108
186
  end
109
187
 
110
- def request_certificate(common_name:, domains:)
111
- client.request_certificate(common_name: common_name, domains: domains)
188
+ # for use in order to store this on the machine for NGINX use
189
+ def csr_private_key_string
190
+ csr_private_key.to_s
112
191
  end
113
192
 
114
193
  private
115
194
 
116
195
  def client
117
196
  @client ||= ApartmentAcmeClient::AcmeClient::Proxy.singleton(
118
- private_key: private_key,
197
+ acme_client_private_key: acme_client_private_key,
198
+ csr_private_key: csr_private_key,
119
199
  )
120
200
  end
121
201
 
122
202
  # Returns a private key
123
- def private_key
203
+ def acme_client_private_key
124
204
  private_key = @certificate_storage.private_key
125
205
  return nil unless private_key
126
206
 
127
207
  OpenSSL::PKey::RSA.new(private_key)
128
208
  end
129
209
 
210
+ def csr_private_key
211
+ private_key = @certificate_storage.csr_private_key
212
+
213
+ # create a new private key if one is not found
214
+ if private_key.nil?
215
+ private_key = create_private_key
216
+ @certificate_storage.save_csr_private_key(private_key)
217
+ end
218
+
219
+ OpenSSL::PKey::RSA.new(private_key)
220
+ end
221
+
130
222
  def create_private_key
131
223
  OpenSSL::PKey::RSA.new(4096)
132
224
  end
@@ -30,10 +30,10 @@ module ApartmentAcmeClient
30
30
 
31
31
  def filled_template
32
32
  return nil unless check_configuration
33
+
33
34
  fill_template(read_template, @options)
34
35
  end
35
36
 
36
-
37
37
  def default_options
38
38
  result = {}
39
39
  result[:public_folder] = ApartmentAcmeClient.public_folder
@@ -59,62 +59,60 @@ module ApartmentAcmeClient
59
59
 
60
60
  def default_template
61
61
  <<~THE_END
62
- #
63
- # A virtual host using mix of IP-, name-, and port-based configuration
64
- #
62
+ #
63
+ # A virtual host using mix of IP-, name-, and port-based configuration
64
+ #
65
65
 
66
- upstream app {
67
- # Path to Unicorn SOCK file, as defined previously
68
- server unix:<%= options[:socket_path] %> fail_timeout=0;
69
- }
66
+ upstream app {
67
+ # Path to Unicorn SOCK file, as defined previously
68
+ server unix:<%= options[:socket_path] %> fail_timeout=0;
69
+ }
70
70
 
71
- server {
71
+ server {
72
72
 
73
- # FOR HTTP
74
- listen 80;
73
+ # FOR HTTP
74
+ listen 80;
75
75
 
76
- gzip on;
76
+ gzip on;
77
77
 
78
- # Application root, as defined previously
79
- root <%= options[:public_folder] %>;
80
- server_name <%= options[:base_domain] %> *.<%= options[:base_domain] %>;
78
+ # Application root, as defined previously
79
+ root <%= options[:public_folder] %>;
80
+ server_name <%= options[:base_domain] %> *.<%= options[:base_domain] %>;
81
81
 
82
- try_files $uri/index.html $uri @app;
82
+ try_files $uri/index.html $uri @app;
83
83
 
84
- location @app {
85
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
86
- proxy_set_header X-FORWARDED-PROTO $scheme;
87
- proxy_set_header Host $http_host;
88
- proxy_redirect off;
89
- proxy_pass http://app;
90
- }
84
+ location @app {
85
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
86
+ proxy_set_header X-FORWARDED-PROTO $scheme;
87
+ proxy_set_header Host $http_host;
88
+ proxy_redirect off;
89
+ proxy_pass http://app;
90
+ }
91
91
 
92
- error_page 500 502 503 504 /500.html;
93
- client_max_body_size 4G;
94
- keepalive_timeout 10;
92
+ error_page 500 502 503 504 /500.html;
93
+ client_max_body_size 4G;
94
+ keepalive_timeout 10;
95
95
 
96
- # BELOW THIS LINE FOR HTTPS
97
- <% if options[:include_ssl] %>
98
- listen 443 default_server ssl;
96
+ # BELOW THIS LINE FOR HTTPS
97
+ <% if options[:include_ssl] %>
98
+ listen 443 default_server ssl;
99
99
 
100
- # The following should be enabled once everything is SSL
101
- # ssl on;
100
+ # The following should be enabled once everything is SSL
101
+ # ssl on;
102
102
 
103
- ssl_certificate <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>cert.pem;
104
- ssl_certificate_key <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>privkey.pem;
103
+ ssl_certificate <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>cert.pem;
104
+ ssl_certificate_key <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>privkey.pem;
105
105
 
106
- ssl_stapling on;
107
- ssl_stapling_verify on;
108
- ssl_trusted_certificate <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>fullchain.pem;
106
+ ssl_stapling on;
107
+ ssl_stapling_verify on;
109
108
 
110
- ssl_session_timeout 5m;
111
- <% end %>
112
- }
109
+ ssl_session_timeout 5m;
110
+ <% end %>
111
+ }
113
112
  THE_END
114
113
  end
115
114
 
116
115
  def fill_template(template, options)
117
-
118
116
  # scope defined for use in binding to ERB
119
117
  def opts(options)
120
118
  options
@@ -1,22 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApartmentAcmeClient
2
4
  class RenewalService
3
5
  def self.run!
4
-
5
6
  good_domains, rejected_domains = ApartmentAcmeClient::DomainChecker.new.accessible_domains
6
7
  puts "All domains to be requested: #{good_domains}, invalid domains: #{rejected_domains}"
7
8
 
8
- domains = ApartmentAcmeClient::Encryption.new.authorize_domains(good_domains)
9
- puts "authorized-domains list: #{domains}"
9
+ common_name = ApartmentAcmeClient.common_name || good_domains.first
10
10
 
11
- common_name = ApartmentAcmeClient.common_name ? ApartmentAcmeClient.common_name : good_domains.first
12
- certificate = ApartmentAcmeClient::Encryption.new.request_certificate(common_name: common_name, domains: domains)
11
+ encryptor = ApartmentAcmeClient::Encryption.new
12
+ certificate = encryptor.request_certificate(
13
+ common_name: common_name,
14
+ domains: good_domains,
15
+ wildcard_domain: ApartmentAcmeClient.wildcard_domain
16
+ )
13
17
 
14
- ApartmentAcmeClient::CertificateStorage::Proxy.singleton.store_certificate(certificate)
18
+ ApartmentAcmeClient::CertificateStorage::Proxy.singleton.store_certificate_string(certificate)
19
+ ApartmentAcmeClient::CertificateStorage::Proxy.singleton.store_csr_private_key_string(encryptor.csr_private_key_string)
15
20
 
16
- puts "Restarting nginx with new certificate"
17
- ApartmentAcmeClient::FileManipulation::Proxy.singleton.restart_service("nginx")
21
+ puts 'Restarting nginx with new certificate'
22
+ ApartmentAcmeClient::FileManipulation::Proxy.singleton.restart_service('nginx')
18
23
 
19
- puts "done."
24
+ puts 'done.'
20
25
  end
21
26
  end
22
27
  end
@@ -1,3 +1,3 @@
1
1
  module ApartmentAcmeClient
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.5'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apartment_acme_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Dunlop
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-13 00:00:00.000000000 Z
11
+ date: 2020-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -36,16 +36,16 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.3.1
39
+ version: 2.0.0
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.3.1
46
+ version: 2.0.0
47
47
  - !ruby/object:Gem::Dependency
48
- name: aws-sdk-s3
48
+ name: aws-sdk-route53
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
@@ -59,19 +59,19 @@ dependencies:
59
59
  - !ruby/object:Gem::Version
60
60
  version: '1'
61
61
  - !ruby/object:Gem::Dependency
62
- name: sqlite3
62
+ name: aws-sdk-s3
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - ">="
65
+ - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0'
68
- type: :development
67
+ version: '1'
68
+ type: :runtime
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
- - - ">="
72
+ - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '0'
74
+ version: '1'
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: bundler
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,20 @@ dependencies:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
88
  version: '1.15'
89
+ - !ruby/object:Gem::Dependency
90
+ name: pry
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
89
103
  - !ruby/object:Gem::Dependency
90
104
  name: rake
91
105
  requirement: !ruby/object:Gem::Requirement
@@ -129,7 +143,21 @@ dependencies:
129
143
  - !ruby/object:Gem::Version
130
144
  version: '0'
131
145
  - !ruby/object:Gem::Dependency
132
- name: pry
146
+ name: rubocop
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ - !ruby/object:Gem::Dependency
160
+ name: sqlite3
133
161
  requirement: !ruby/object:Gem::Requirement
134
162
  requirements:
135
163
  - - ">="
@@ -170,6 +198,9 @@ files:
170
198
  - lib/apartment_acme_client/acme_client/real_client.rb
171
199
  - lib/apartment_acme_client/certificate_storage/proxy.rb
172
200
  - lib/apartment_acme_client/certificate_storage/s3.rb
201
+ - lib/apartment_acme_client/dns_api/check_dns.rb
202
+ - lib/apartment_acme_client/dns_api/fake.rb
203
+ - lib/apartment_acme_client/dns_api/route53.rb
173
204
  - lib/apartment_acme_client/domain_checker.rb
174
205
  - lib/apartment_acme_client/encryption.rb
175
206
  - lib/apartment_acme_client/engine.rb
@@ -201,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
232
  version: '0'
202
233
  requirements: []
203
234
  rubyforge_project:
204
- rubygems_version: 2.7.8
235
+ rubygems_version: 2.7.6
205
236
  signing_key:
206
237
  specification_version: 4
207
238
  summary: Let's Encrypt interface for Multi-tenancy applications (like Apartment)