From 1cd22322f3cc0ab0354386d8b43c7145b87c9b6c Mon Sep 17 00:00:00 2001 From: Sorah Fukumori Date: Sat, 8 Oct 2022 23:43:03 +0900 Subject: [PATCH] route53: implement substitution_map Allow delegation of _acme-challenge by using CNAME and pre-defined substitution record name. --- docs/challenge_responders/route53.md | 11 +++- lib/acmesmith/challenge_responders/route53.rb | 12 +++- spec/challenge_responders/route53_spec.rb | 57 ++++++++++++++++++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/challenge_responders/route53.md b/docs/challenge_responders/route53.md index 1575f4b..e0125dc 100644 --- a/docs/challenge_responders/route53.md +++ b/docs/challenge_responders/route53.md @@ -24,7 +24,16 @@ challenge_responders: # Restore to original records on cleanup (after domain authorization). Default to false. # Useful when you need to keep existing record as long as possible. - restore_to_original_records: true + restore_to_original_records: false + + ### Substitution record names map (optional) + ## This specifies alias for specific _acme-challenge record. For instance the following example + ## updates _acme-challenge.test-example-com.example.org instead of _acme-challenge.test.example.com. + ## + ## This eases using the route53 responder for domains not managed in route53, by registering CNAME record to + ## the alias record name on the original record name in advance. This is called delegation. + substitution_map: + "test.example.com.": "test-example-com.example.org." ``` ## IAM Policy diff --git a/lib/acmesmith/challenge_responders/route53.rb b/lib/acmesmith/challenge_responders/route53.rb index 1878cf8..1c782c4 100644 --- a/lib/acmesmith/challenge_responders/route53.rb +++ b/lib/acmesmith/challenge_responders/route53.rb @@ -17,7 +17,7 @@ def cap_respond_all? true end - def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {}, restore_to_original_records: false) + def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {}, restore_to_original_records: false, substitution_map: {}) aws_options = {region: 'us-east-1'}.tap do |opt| 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 end @@ -37,9 +37,13 @@ def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {}, resto @restore_to_original_records = restore_to_original_records @original_records = {} + + @substitution_map = substitution_map.map { |k,v| [canonical_fqdn(k), v] }.to_h end def respond_all(*domain_and_challenges) + domain_and_challenges = apply_substitution_for_domain_and_challenges(domain_and_challenges) + save_original_records(*domain_and_challenges) if @restore_to_original_records challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) } @@ -60,6 +64,8 @@ def respond_all(*domain_and_challenges) end def cleanup_all(*domain_and_challenges) + domain_and_challenges = apply_substitution_for_domain_and_challenges(domain_and_challenges) + challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) } zone_and_batches = challenges_by_hosted_zone.map do |zone_id, dcs| @@ -264,6 +270,10 @@ def hosted_zone_list end end + def apply_substitution_for_domain_and_challenges(domain_and_challenges) + domain_and_challenges.map { |(domain, challenge)| [@substitution_map.fetch(canonical_fqdn(domain), domain), challenge] } + end + def list_existing_rrsets(hosted_zone_id, name) rrsets = [] start_record_name = name diff --git a/spec/challenge_responders/route53_spec.rb b/spec/challenge_responders/route53_spec.rb index b2c90db..e29b577 100644 --- a/spec/challenge_responders/route53_spec.rb +++ b/spec/challenge_responders/route53_spec.rb @@ -14,6 +14,7 @@ def print(*); end let(:assume_role) { nil } let(:hosted_zone_map) { {} } let(:restore_to_original_records) { false } + let(:substitution_map) { {} } let(:r53) { double(:route53) } @@ -23,6 +24,7 @@ def print(*); end assume_role: assume_role, hosted_zone_map: hosted_zone_map, restore_to_original_records: restore_to_original_records, + substitution_map: substitution_map, ) end @@ -115,7 +117,7 @@ def print(*); end let(:list_hosted_zones) do [ - *%w(example.com corp.example.com).map do |_| + *%w(example.com corp.example.com example.org).map do |_| Aws::Route53::Types::HostedZone.new(name: "#{_}.", id: "/hostedzone/#{_}", config: Aws::Route53::Types::HostedZoneConfig.new(private_zone: false)) end, Aws::Route53::Types::HostedZone.new(name: "example.net.", id: "/hostedzone/example.net-true", config: Aws::Route53::Types::HostedZoneConfig.new(private_zone: false)), @@ -494,6 +496,59 @@ def expect_change_rrset(hosted_zone_id:, changes:, comment:, wait: true) roundtrip end end + + context "when substitution_map is set" do + let(:substitution_map) { {"akane.example.com." => "_akane.example.org"} } + let(:domain_and_challenges) do + [ + ['akane.example.com', double_challenge], + ['yaeka.example.com', double_challenge], + ] + end + + subject(:roundtrip) { + responder.respond_all(*domain_and_challenges) + responder.cleanup_all(*domain_and_challenges) + } + + before do + expect_change_rrset( + hosted_zone_id: '/hostedzone/example.org', + comment: 'ACME challenge response ', + changes: [ + change_object(action: 'UPSERT', name: "_akane.example.org", challenge: domain_and_challenges[0][1]), + ], + ) + expect_change_rrset( + hosted_zone_id: '/hostedzone/example.com', + comment: 'ACME challenge response ', + changes: [ + change_object(action: 'UPSERT', name: domain_and_challenges[1][0], challenge: domain_and_challenges[1][1]), + ], + ) + expect_change_rrset( + hosted_zone_id: '/hostedzone/example.org', + comment: 'ACME challenge response (cleanup)', + changes: [ + change_object(action: 'DELETE', name: "_akane.example.org", challenge: domain_and_challenges[0][1]), + ], + wait: false, + ) + expect_change_rrset( + hosted_zone_id: '/hostedzone/example.com', + comment: 'ACME challenge response (cleanup)', + changes: [ + change_object(action: 'DELETE', name: domain_and_challenges[1][0], challenge: domain_and_challenges[1][1]), + ], + wait: false, + ) + end + + it "uses another name for rrset" do + roundtrip + end + end + end