apartment_acme_client 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -16
- data/Rakefile +0 -1
- data/lib/apartment_acme_client.rb +6 -0
- data/lib/apartment_acme_client/acme_client/real_client.rb +24 -18
- data/lib/apartment_acme_client/certificate_storage/s3.rb +32 -17
- data/lib/apartment_acme_client/dns_api/check_dns.rb +65 -0
- data/lib/apartment_acme_client/dns_api/fake.rb +6 -0
- data/lib/apartment_acme_client/dns_api/route53.rb +90 -0
- data/lib/apartment_acme_client/encryption.rb +121 -29
- data/lib/apartment_acme_client/nginx_configuration/real.rb +38 -40
- data/lib/apartment_acme_client/renewal_service.rb +14 -9
- data/lib/apartment_acme_client/version.rb +1 -1
- metadata +44 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66eaebf9ae761526e149a5bedd237deee499a95107edb9ce3c1e3916e29c058b
|
4
|
+
data.tar.gz: 4ab90728f136c38b453a24f54b760c84539c4333ae0cb24ba4122dcb54c124b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
272
|
+
straight invocation:
|
258
273
|
```ruby
|
259
|
-
|
274
|
+
ApartmentAcmeClient::RenewalService.run!
|
260
275
|
```
|
261
276
|
|
262
|
-
|
263
|
-
|
277
|
+
we provide a helper rake task:
|
264
278
|
```ruby
|
265
|
-
|
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
@@ -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
|
-
|
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:
|
9
|
-
|
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
|
-
|
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:,
|
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:
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
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-
|
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
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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'
|
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
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
24
|
-
|
25
|
-
store_s3_file(derived_filename(
|
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(
|
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,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
|
-
# -
|
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(
|
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
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
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
|
55
|
-
|
111
|
+
def authorize_domain_with_http(domain_authorization)
|
112
|
+
challenge = domain_authorization.http
|
56
113
|
|
57
|
-
#
|
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.
|
147
|
+
challenge.request_validation # => true
|
94
148
|
|
95
|
-
|
96
|
-
|
97
|
-
if challenge.
|
98
|
-
|
99
|
-
|
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(
|
156
|
+
sleep(2)
|
157
|
+
challenge.reload
|
104
158
|
end
|
105
159
|
File.delete(full_challenge_filename)
|
106
160
|
|
107
|
-
|
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
|
-
|
111
|
-
|
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
|
-
|
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
|
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
|
-
|
64
|
-
|
62
|
+
#
|
63
|
+
# A virtual host using mix of IP-, name-, and port-based configuration
|
64
|
+
#
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
71
|
+
server {
|
72
72
|
|
73
|
-
|
74
|
-
|
73
|
+
# FOR HTTP
|
74
|
+
listen 80;
|
75
75
|
|
76
|
-
|
76
|
+
gzip on;
|
77
77
|
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
# Application root, as defined previously
|
79
|
+
root <%= options[:public_folder] %>;
|
80
|
+
server_name <%= options[:base_domain] %> *.<%= options[:base_domain] %>;
|
81
81
|
|
82
|
-
|
82
|
+
try_files $uri/index.html $uri @app;
|
83
83
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
92
|
+
error_page 500 502 503 504 /500.html;
|
93
|
+
client_max_body_size 4G;
|
94
|
+
keepalive_timeout 10;
|
95
95
|
|
96
|
-
|
97
|
-
|
98
|
-
|
96
|
+
# BELOW THIS LINE FOR HTTPS
|
97
|
+
<% if options[:include_ssl] %>
|
98
|
+
listen 443 default_server ssl;
|
99
99
|
|
100
|
-
|
101
|
-
|
100
|
+
# The following should be enabled once everything is SSL
|
101
|
+
# ssl on;
|
102
102
|
|
103
|
-
|
104
|
-
|
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
|
-
|
107
|
-
|
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
|
-
|
111
|
-
|
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
|
-
|
9
|
-
puts "authorized-domains list: #{domains}"
|
9
|
+
common_name = ApartmentAcmeClient.common_name || good_domains.first
|
10
10
|
|
11
|
-
|
12
|
-
certificate =
|
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.
|
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
|
17
|
-
ApartmentAcmeClient::FileManipulation::Proxy.singleton.restart_service(
|
21
|
+
puts 'Restarting nginx with new certificate'
|
22
|
+
ApartmentAcmeClient::FileManipulation::Proxy.singleton.restart_service('nginx')
|
18
23
|
|
19
|
-
puts
|
24
|
+
puts 'done.'
|
20
25
|
end
|
21
26
|
end
|
22
27
|
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.
|
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:
|
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.
|
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.
|
46
|
+
version: 2.0.0
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name: aws-sdk-
|
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:
|
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: '
|
68
|
-
type: :
|
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: '
|
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:
|
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.
|
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)
|