acmesmith 2.3.1 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/stale.yml +17 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +17 -15
- data/docs/challenge_responders/route53.md +4 -0
- data/lib/acmesmith/authorization_service.rb +24 -3
- data/lib/acmesmith/challenge_responders/route53.rb +122 -6
- data/lib/acmesmith/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4089bdb36940e87e2f996a6c820a2f06505024f6ae9721c3da332999c2145998
|
4
|
+
data.tar.gz: 1abf984f4f5d82a0d266bb5aee3905708ba1392238439663af0bc8ac9aa700b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1147c0742b2bd14205b89ca76f4b011453d2eeadf0df2aaad094b7e3c32907de322aed48c0d4fce40a4120ddd782c5c67e6da1504644fce3db679b4452ef774
|
7
|
+
data.tar.gz: e8bc441a9cb9fff624f338fb33946bab80bf742848e695130b17bd807ba8da98d79e5999459ce824cf6ebcdac6206f156026a17adc754aa6bde8356cc3a915f1
|
data/.github/stale.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Number of days of inactivity before an issue becomes stale
|
2
|
+
daysUntilStale: 30
|
3
|
+
# Number of days of inactivity before a stale issue is closed
|
4
|
+
daysUntilClose: 7
|
5
|
+
# Issues with these labels will never be considered stale
|
6
|
+
exemptLabels:
|
7
|
+
- pinned
|
8
|
+
- security
|
9
|
+
# Label to use when marking an issue as stale
|
10
|
+
staleLabel: rotten
|
11
|
+
# Comment to post when marking an issue as stale. Set to `false` to disable
|
12
|
+
markComment: >
|
13
|
+
This issue has been automatically marked as stale because it has not had
|
14
|
+
recent activity. It will be closed if no further activity occurs. Thank you
|
15
|
+
for your contributions.
|
16
|
+
# Comment to post when closing a stale issue. Set to `false` to disable
|
17
|
+
closeComment: false
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
## v2.4.0 (2020-05-12)
|
2
|
+
|
3
|
+
### Enhancement
|
4
|
+
|
5
|
+
- route53: Gains `restore_to_original_records` option. When enabled, existing record will be restored after authorizing domain names. Useful when other ACME tools or providers using ACME where requires a certain record to remain as long as possible for their renewal process (e.g. Fastly TLS).
|
6
|
+
|
1
7
|
## v2.3.1 (2020-05-12)
|
2
8
|
|
3
9
|
### Fixes
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
acmesmith (2.
|
4
|
+
acmesmith (2.4.0)
|
5
5
|
acme-client (~> 2)
|
6
6
|
aws-sdk-acm
|
7
7
|
aws-sdk-route53
|
@@ -11,33 +11,34 @@ PATH
|
|
11
11
|
GEM
|
12
12
|
remote: https://rubygems.org/
|
13
13
|
specs:
|
14
|
-
acme-client (2.0.
|
14
|
+
acme-client (2.0.7)
|
15
15
|
faraday (>= 0.17, < 2.0.0)
|
16
16
|
aws-eventstream (1.1.0)
|
17
|
-
aws-partitions (1.
|
18
|
-
aws-sdk-acm (1.
|
19
|
-
aws-sdk-core (~> 3, >= 3.
|
17
|
+
aws-partitions (1.402.0)
|
18
|
+
aws-sdk-acm (1.38.0)
|
19
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
20
20
|
aws-sigv4 (~> 1.1)
|
21
|
-
aws-sdk-core (3.
|
21
|
+
aws-sdk-core (3.109.3)
|
22
22
|
aws-eventstream (~> 1, >= 1.0.2)
|
23
23
|
aws-partitions (~> 1, >= 1.239.0)
|
24
24
|
aws-sigv4 (~> 1.1)
|
25
25
|
jmespath (~> 1.0)
|
26
|
-
aws-sdk-kms (1.
|
27
|
-
aws-sdk-core (~> 3, >= 3.
|
26
|
+
aws-sdk-kms (1.39.0)
|
27
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
28
28
|
aws-sigv4 (~> 1.1)
|
29
|
-
aws-sdk-route53 (1.
|
30
|
-
aws-sdk-core (~> 3, >= 3.
|
29
|
+
aws-sdk-route53 (1.44.0)
|
30
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
31
31
|
aws-sigv4 (~> 1.1)
|
32
|
-
aws-sdk-s3 (1.
|
33
|
-
aws-sdk-core (~> 3, >= 3.
|
32
|
+
aws-sdk-s3 (1.86.0)
|
33
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
34
34
|
aws-sdk-kms (~> 1)
|
35
35
|
aws-sigv4 (~> 1.1)
|
36
|
-
aws-sigv4 (1.
|
37
|
-
aws-eventstream (~> 1
|
36
|
+
aws-sigv4 (1.2.2)
|
37
|
+
aws-eventstream (~> 1, >= 1.0.2)
|
38
38
|
diff-lcs (1.3)
|
39
|
-
faraday (1.0
|
39
|
+
faraday (1.1.0)
|
40
40
|
multipart-post (>= 1.2, < 3)
|
41
|
+
ruby2_keywords
|
41
42
|
jmespath (1.4.0)
|
42
43
|
mini_portile2 (2.4.0)
|
43
44
|
multipart-post (2.1.1)
|
@@ -57,6 +58,7 @@ GEM
|
|
57
58
|
diff-lcs (>= 1.2.0, < 2.0)
|
58
59
|
rspec-support (~> 3.9.0)
|
59
60
|
rspec-support (3.9.3)
|
61
|
+
ruby2_keywords (0.0.2)
|
60
62
|
thor (1.0.1)
|
61
63
|
|
62
64
|
PLATFORMS
|
@@ -21,6 +21,10 @@ challenge_responders:
|
|
21
21
|
## Required when you have multiple hosted zones for the same domain name.
|
22
22
|
hosted_zone_map:
|
23
23
|
"example.org.": "/hostedzone/DEADBEEF"
|
24
|
+
|
25
|
+
# Restore to original records on cleanup (after domain authorization). Default to false.
|
26
|
+
# Useful when you need to keep existing record as long as possible.
|
27
|
+
restore_to_original_records: true
|
24
28
|
```
|
25
29
|
|
26
30
|
## IAM Policy
|
@@ -69,9 +69,30 @@ module Acmesmith
|
|
69
69
|
puts "=> Requesting validations..."
|
70
70
|
puts
|
71
71
|
processes.each do |process|
|
72
|
-
|
73
|
-
process.challenge.
|
74
|
-
|
72
|
+
challenge = process.challenge
|
73
|
+
print " * #{process.domain} (#{challenge.challenge_type}) ..."
|
74
|
+
retried = false
|
75
|
+
begin
|
76
|
+
challenge.request_validation()
|
77
|
+
puts " [ ok ]"
|
78
|
+
rescue Acme::Client::Error::Malformed
|
79
|
+
# Rescue in case of requesting validation for a challenge which has already determined valid (asynchronously while we're receiving it).
|
80
|
+
# LE Boulder doesn't take this as an error, but pebble do.
|
81
|
+
# https://github.com/letsencrypt/boulder/blob/ebba443cad233111ee2b769ef09b32a13c3ba57e/wfe2/wfe.go#L1235
|
82
|
+
# https://github.com/letsencrypt/pebble/blob/b60b0b677c280ccbf63de55a26775591935c448b/wfe/wfe.go#L2166
|
83
|
+
challenge.reload
|
84
|
+
if process.valid?
|
85
|
+
puts " [ ok ] (turned valid in background)"
|
86
|
+
next
|
87
|
+
end
|
88
|
+
|
89
|
+
if retried
|
90
|
+
raise
|
91
|
+
else
|
92
|
+
retried = true
|
93
|
+
retry
|
94
|
+
end
|
95
|
+
end
|
75
96
|
end
|
76
97
|
puts
|
77
98
|
|
@@ -17,7 +17,7 @@ module Acmesmith
|
|
17
17
|
true
|
18
18
|
end
|
19
19
|
|
20
|
-
def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {})
|
20
|
+
def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {}, restore_to_original_records: false)
|
21
21
|
aws_options = {region: 'us-east-1'}.tap do |opt|
|
22
22
|
opt[:credentials] = Aws::Credentials.new(aws_access_key['access_key_id'], aws_access_key['secret_access_key'], aws_access_key['session_token']) if aws_access_key
|
23
23
|
end
|
@@ -34,13 +34,25 @@ module Acmesmith
|
|
34
34
|
|
35
35
|
@hosted_zone_map = hosted_zone_map
|
36
36
|
@hosted_zone_cache = {}
|
37
|
+
|
38
|
+
@restore_to_original_records = restore_to_original_records
|
39
|
+
@original_records = {}
|
37
40
|
end
|
38
41
|
|
39
42
|
def respond_all(*domain_and_challenges)
|
43
|
+
save_original_records(*domain_and_challenges) if @restore_to_original_records
|
44
|
+
|
40
45
|
challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) }
|
41
46
|
|
42
47
|
zone_and_batches = challenges_by_hosted_zone.map do |zone_id, dcs|
|
43
|
-
[
|
48
|
+
[
|
49
|
+
zone_id,
|
50
|
+
change_batch_for_challenges(
|
51
|
+
dcs,
|
52
|
+
action: 'UPSERT',
|
53
|
+
pre_changes: changes_to_delete_original_cname(zone_id, *dcs),
|
54
|
+
),
|
55
|
+
]
|
44
56
|
end
|
45
57
|
|
46
58
|
change_ids = request_changing_rrset(zone_and_batches, comment: 'for challenge response')
|
@@ -51,7 +63,15 @@ module Acmesmith
|
|
51
63
|
challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) }
|
52
64
|
|
53
65
|
zone_and_batches = challenges_by_hosted_zone.map do |zone_id, dcs|
|
54
|
-
[
|
66
|
+
[
|
67
|
+
zone_id,
|
68
|
+
change_batch_for_challenges(
|
69
|
+
dcs,
|
70
|
+
action: 'DELETE',
|
71
|
+
comment: '(cleanup)',
|
72
|
+
post_changes: changes_to_restore_original_records(zone_id, *dcs),
|
73
|
+
),
|
74
|
+
]
|
55
75
|
end
|
56
76
|
|
57
77
|
request_changing_rrset(zone_and_batches, comment: 'to remove challenge responses')
|
@@ -59,6 +79,69 @@ module Acmesmith
|
|
59
79
|
|
60
80
|
private
|
61
81
|
|
82
|
+
def save_original_records(*domain_and_challenges)
|
83
|
+
domain_and_challenges.each do |domain, challenge|
|
84
|
+
|
85
|
+
hosted_zone_id = find_hosted_zone(domain)
|
86
|
+
name = "#{challenge.record_name}.#{domain}."
|
87
|
+
|
88
|
+
rrsets = list_existing_rrsets(hosted_zone_id, name)
|
89
|
+
next if rrsets.empty?
|
90
|
+
|
91
|
+
@original_records[hosted_zone_id] ||= {}
|
92
|
+
@original_records[hosted_zone_id][name] = rrsets
|
93
|
+
puts " * original_record: #{domain}(#{hosted_zone_id}): #{rrsets.inspect}"
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def changes_to_delete_original_cname(zone_id, *domain_and_challenges)
|
99
|
+
@original_records[zone_id] ||= {}
|
100
|
+
domain_and_challenges.map do |domain, challenge|
|
101
|
+
name = "#{challenge.record_name}.#{domain}."
|
102
|
+
original_records = @original_records[zone_id][name]
|
103
|
+
next unless original_records
|
104
|
+
original_cname = original_records.find{ |_| _.type == 'CNAME' }
|
105
|
+
next unless original_cname
|
106
|
+
|
107
|
+
# FIXME: support set_identifier?
|
108
|
+
{
|
109
|
+
action: 'DELETE',
|
110
|
+
resource_record_set: {
|
111
|
+
name: original_cname.name,
|
112
|
+
ttl: original_cname.ttl,
|
113
|
+
type: original_cname.type,
|
114
|
+
resource_records: original_cname.resource_records.map(&:to_h),
|
115
|
+
alias_target: original_cname.alias_target&.to_h,
|
116
|
+
},
|
117
|
+
}
|
118
|
+
end.compact
|
119
|
+
end
|
120
|
+
|
121
|
+
def changes_to_restore_original_records(zone_id, *domain_and_challenges)
|
122
|
+
@original_records[zone_id] ||= {}
|
123
|
+
domain_and_challenges.flat_map do |domain, challenge|
|
124
|
+
name = "#{challenge.record_name}.#{domain}."
|
125
|
+
original_records = @original_records[zone_id][name]
|
126
|
+
next unless original_records
|
127
|
+
|
128
|
+
# FIXME: support set_identifier?
|
129
|
+
original_records.map do |original_record|
|
130
|
+
next if original_record.type != challenge.record_type && original_record.type != 'CNAME'
|
131
|
+
{
|
132
|
+
action: 'CREATE',
|
133
|
+
resource_record_set: {
|
134
|
+
name: original_record.name,
|
135
|
+
ttl: original_record.ttl,
|
136
|
+
type: original_record.type,
|
137
|
+
resource_records: original_record.resource_records.map(&:to_h),
|
138
|
+
alias_target: original_record.alias_target&.to_h,
|
139
|
+
},
|
140
|
+
}
|
141
|
+
end
|
142
|
+
end.compact
|
143
|
+
end
|
144
|
+
|
62
145
|
def request_changing_rrset(zone_and_batches, comment: nil)
|
63
146
|
puts "=> Requesting RRSet change #{comment}"
|
64
147
|
puts
|
@@ -107,10 +190,9 @@ module Acmesmith
|
|
107
190
|
end
|
108
191
|
end
|
109
192
|
puts
|
110
|
-
|
111
193
|
end
|
112
194
|
|
113
|
-
def change_batch_for_challenges(domain_and_challenges, comment: nil, action: 'UPSERT')
|
195
|
+
def change_batch_for_challenges(domain_and_challenges, comment: nil, action: 'UPSERT', pre_changes: [], post_changes: [])
|
114
196
|
changes = domain_and_challenges
|
115
197
|
.map do |d, c|
|
116
198
|
rrset_for_challenge(d, c)
|
@@ -133,7 +215,7 @@ module Acmesmith
|
|
133
215
|
|
134
216
|
{
|
135
217
|
comment: "ACME challenge response #{comment}",
|
136
|
-
changes: changes,
|
218
|
+
changes: pre_changes + changes + post_changes,
|
137
219
|
}
|
138
220
|
end
|
139
221
|
|
@@ -181,6 +263,40 @@ module Acmesmith
|
|
181
263
|
end.group_by(&:first).map { |domain, kvs| [domain, kvs.map(&:last)] }.to_h.merge(hosted_zone_map)
|
182
264
|
end
|
183
265
|
end
|
266
|
+
|
267
|
+
def list_existing_rrsets(hosted_zone_id, name)
|
268
|
+
rrsets = []
|
269
|
+
start_record_name = name
|
270
|
+
start_record_type = nil
|
271
|
+
start_record_identifier = nil
|
272
|
+
|
273
|
+
while start_record_name == name
|
274
|
+
begin
|
275
|
+
tries = 0
|
276
|
+
page = @route53.list_resource_record_sets(
|
277
|
+
hosted_zone_id: hosted_zone_id,
|
278
|
+
start_record_name: start_record_name,
|
279
|
+
start_record_type: start_record_type,
|
280
|
+
start_record_identifier: start_record_identifier,
|
281
|
+
max_items: 10,
|
282
|
+
)
|
283
|
+
page.resource_record_sets.each do |rrset|
|
284
|
+
rrsets << rrset if rrset.name == name
|
285
|
+
end
|
286
|
+
|
287
|
+
start_record_name = page.next_record_name
|
288
|
+
start_record_type = page.next_record_type
|
289
|
+
start_record_identifier = page.next_record_identifier
|
290
|
+
rescue Aws::Route53::Errors::Throttling => e
|
291
|
+
interval = (2**tries) * 0.1
|
292
|
+
$stderr.puts " ! #{e.class}: Sleeping #{interval} seconds (#{e.message})"
|
293
|
+
sleep interval
|
294
|
+
tries += 1
|
295
|
+
retry
|
296
|
+
end
|
297
|
+
end
|
298
|
+
rrsets
|
299
|
+
end
|
184
300
|
end
|
185
301
|
end
|
186
302
|
end
|
data/lib/acmesmith/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acmesmith
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sorah Fukumori
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acme-client
|
@@ -136,6 +136,7 @@ extensions: []
|
|
136
136
|
extra_rdoc_files: []
|
137
137
|
files:
|
138
138
|
- ".dockerignore"
|
139
|
+
- ".github/stale.yml"
|
139
140
|
- ".github/workflows/build.yml"
|
140
141
|
- ".gitignore"
|
141
142
|
- ".rspec"
|
@@ -190,7 +191,7 @@ homepage: https://github.com/sorah/acmesmith
|
|
190
191
|
licenses:
|
191
192
|
- MIT
|
192
193
|
metadata: {}
|
193
|
-
post_install_message:
|
194
|
+
post_install_message:
|
194
195
|
rdoc_options: []
|
195
196
|
require_paths:
|
196
197
|
- lib
|
@@ -206,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
207
|
version: '0'
|
207
208
|
requirements: []
|
208
209
|
rubygems_version: 3.1.2
|
209
|
-
signing_key:
|
210
|
+
signing_key:
|
210
211
|
specification_version: 4
|
211
212
|
summary: ACME client (Let's encrypt client) to manage certificate in multi server
|
212
213
|
environment with cloud services (e.g. AWS)
|