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 +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)
|