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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c2ccb5560fae2d63385e8460268f5c1fa4ca9e7fa96abb34874fbc2225c4f85
4
- data.tar.gz: 58f74cdbdbb476db11fe8bdde5f6cd8b60e6f05845c47f6293147fb644e29b3c
3
+ metadata.gz: 4089bdb36940e87e2f996a6c820a2f06505024f6ae9721c3da332999c2145998
4
+ data.tar.gz: 1abf984f4f5d82a0d266bb5aee3905708ba1392238439663af0bc8ac9aa700b0
5
5
  SHA512:
6
- metadata.gz: 49099fbee8ea178501cd45fd468c5554c3dfc938ff9d86ff1a5dcbbe3e8385444486ca46bffdadea7f1f70f89e7d5df69ef4191bbd6f6de4de2c94d9ec73c8cf
7
- data.tar.gz: f841483c5c224e73a6e6b95ca7317dc481642b7786899ad15c31c2c97dfae649fc1cf0133ea7ac3afe8b002d10230c73235c77d90eca85c2103789fc5aed3e7d
6
+ metadata.gz: d1147c0742b2bd14205b89ca76f4b011453d2eeadf0df2aaad094b7e3c32907de322aed48c0d4fce40a4120ddd782c5c67e6da1504644fce3db679b4452ef774
7
+ data.tar.gz: e8bc441a9cb9fff624f338fb33946bab80bf742848e695130b17bd807ba8da98d79e5999459ce824cf6ebcdac6206f156026a17adc754aa6bde8356cc3a915f1
@@ -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
@@ -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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- acmesmith (2.3.1)
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.6)
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.312.0)
18
- aws-sdk-acm (1.30.0)
19
- aws-sdk-core (~> 3, >= 3.71.0)
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.95.0)
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.31.0)
27
- aws-sdk-core (~> 3, >= 3.71.0)
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.34.0)
30
- aws-sdk-core (~> 3, >= 3.71.0)
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.64.0)
33
- aws-sdk-core (~> 3, >= 3.83.0)
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.1.3)
37
- aws-eventstream (~> 1.0, >= 1.0.2)
36
+ aws-sigv4 (1.2.2)
37
+ aws-eventstream (~> 1, >= 1.0.2)
38
38
  diff-lcs (1.3)
39
- faraday (1.0.1)
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
- print " * #{process.domain} (#{process.challenge.challenge_type}) ..."
73
- process.challenge.request_validation()
74
- puts " [ ok ]"
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
- [zone_id, change_batch_for_challenges(dcs, action: 'UPSERT')]
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
- [zone_id, change_batch_for_challenges(dcs, action: 'DELETE', comment: '(cleanup)')]
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
@@ -1,3 +1,3 @@
1
1
  module Acmesmith
2
- VERSION = "2.3.1"
2
+ VERSION = "2.4.0"
3
3
  end
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.3.1
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-05-12 00:00:00.000000000 Z
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)