acmesmith 2.3.1 → 2.4.0

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