summaryrefslogtreecommitdiff
path: root/macir.rb
diff options
context:
space:
mode:
Diffstat (limited to 'macir.rb')
-rw-r--r--macir.rb487
1 files changed, 393 insertions, 94 deletions
diff --git a/macir.rb b/macir.rb
index f923839..cdb9b61 100644
--- a/macir.rb
+++ b/macir.rb
@@ -68,8 +68,18 @@ def read_cert_key(cert_name)
return private_key
end
+def lookup_ns(domain)
+ p "Domain #{domain}: Creating recursor object for checking challenge propagation"
+ rec = Dnsruby::Resolver.new
+ p "Domain #{domain}: Getting NS records for #{domain}"
+ rec.query_no_validation_or_recursion(domain, 'NS')
+rescue StandardError => e
+ warn "Domain #{domain}: NS lookup during deploy failed: #{e}"
+ raise
+end
+
def lookup_soa(domain)
- rec = Dnsruby::Recursor.new
+ rec = Dnsruby::Resolver.new
p "Domain #{domain}: Getting SOA records for #{domain}"
rec.query_no_validation_or_recursion(domain, 'SOA')
rescue StandardError => e
@@ -86,6 +96,53 @@ def find_apex_domain(domain)
end
end
+def build_dns_update_packet(apex, authzs)
+ update = Dnsruby::Update.new(apex)
+
+ authzs.each do |auth|
+ chal = auth.dns01
+ update.delete("#{chal.record_name}.#{auth.domain}", chal.record_type)
+ update.add("#{chal.record_name}.#{auth.domain}", chal.record_type, 3, chal.record_content)
+ end
+ return update
+end
+
+def build_tsig_object(apex, config)
+ p "Domain #{apex}: Looking up TSIG parameters"
+ tsig_name = config.dig('domains', apex, 'tsig_key') || config.dig('defaults', 'domains', 'tsig_key')
+ tsig_key = config.dig('tsig_keys', tsig_name, 'key')
+ tsig_alg = config.dig('tsig_keys', tsig_name, 'algorithm')
+
+ p "Domain #{apex}: Creating TSIG object"
+ tsig = Dnsruby::RR.create(
+ {
+ name: tsig_name,
+ type: 'TSIG',
+ key: tsig_key,
+ algorithm: tsig_alg,
+ }
+ )
+end
+
+def deploy_dns_tokens_on_apex(apex, authzs, nameserver, config)
+ update_packet = build_dns_update_packet(apex, authzs)
+
+ p "Domain #{apex}: Creating object for contacting nameserver"
+ res = Dnsruby::Resolver.new(nameserver)
+ res.dnssec = false
+
+ tsig = build_tsig_object(apex, config)
+
+ p "Domain #{apex}: Signing DNS UPDATE packet with TSIG object"
+ tsig.apply(update_packet)
+
+ p "Domain #{apex}: Sending UPDATE to nameserver"
+ res.send_message(update_packet)
+rescue StandardError => e
+ warn "Domain #{apex}: DNS Update failed: #{e}"
+ raise
+end
+
def deploy_dns01_challenge_token(domain, challenge, nameserver, config)
p "Domain #{domain}: Creating DNS UPDATE packet"
@@ -100,20 +157,7 @@ def deploy_dns01_challenge_token(domain, challenge, nameserver, config)
res = Dnsruby::Resolver.new(nameserver)
res.dnssec = false
- p "Domain #{domain}: Looking up TSIG parameters"
- tsig_name = config.dig('domains', domain, 'tsig_key') || config.dig('defaults', 'domains', 'tsig_key')
- tsig_key = config.dig('tsig_keys', tsig_name, 'key')
- tsig_alg = config.dig('tsig_keys', tsig_name, 'algorithm')
-
- p "Domain #{domain}: Creating TSIG object"
- tsig = Dnsruby::RR.create(
- {
- name: tsig_name,
- type: 'TSIG',
- key: tsig_key,
- algorithm: tsig_alg,
- }
- )
+ tsig = build_tsig_object(domain, config)
p "Domain #{domain}: Signing DNS UPDATE packet with TSIG object"
tsig.apply(update)
@@ -126,29 +170,8 @@ rescue StandardError => e
end
def wait_for_challenge_propagation(domain, challenge)
- rec = Dnsruby::Recursor.new
- p "Domain #{domain}: Getting SOA records for #{domain}"
- begin
- domain_soa_resp = rec.query_no_validation_or_recursion(domain, 'SOA')
- rescue StandardError => e
- warn "Domain #{domain}: SOA lookup during propagation wait failed: #{e}"
- raise
- end
- apex_domain = if domain_soa_resp.answer.empty?
- domain_soa_resp.authority[0].name
- else
- domain_soa_resp.answer[0].name
- end
-
- p "Domain #{domain}: Creating recursor object for checking challenge propagation"
- rec = Dnsruby::Recursor.new
- p "Domain #{domain}: Getting NS records for #{apex_domain}"
- begin
- domain_auth_ns = rec.query_no_validation_or_recursion(apex_domain, 'NS')
- rescue StandardError => e
- warn "Domain #{domain}: NS lookup failed: #{e}"
- raise
- end
+ apex_domain = find_apex_domain(domain)
+ domain_auth_ns = lookup_ns(apex_domain)
p "Domain #{domain}: Checking challenge status on all NS"
@@ -186,7 +209,7 @@ def wait_for_challenge_propagation(domain, challenge)
end
def acme_request_with_retries(retries: 5, &block)
- p "Retries: #{retries}"
+ # p "Retries: #{retries}"
block.call(self)
rescue Acme::Client::Error::BadNonce
raise unless retries.positive?
@@ -200,11 +223,11 @@ def wait_for_challenge_validation(challenge, cert_name)
acme_request_with_retries { challenge.request_validation }
while challenge.status == 'pending'
- p "Cert #{cert_name}: Sleeping because challenge validation is pending"
+ p "Cert #{cert_name}: challenge validation is pending, sleeping before checking again"
sleep(0.1)
- p 'Checking again'
acme_request_with_retries { challenge.reload }
end
+ # pp challenge
end
def get_cert(order, cert_name, domains, domain_key)
@@ -217,7 +240,9 @@ def get_cert(order, cert_name, domains, domain_key)
subject: { common_name: domains[0] }
)
p "Cert #{cert_name}: Finalize cert order"
- acme_request_with_retries { order.reload }
+ # pp order
+ # TODO: this seems unnecessary?
+ # acme_request_with_retries { order.reload }
acme_request_with_retries { order.finalize(csr: csr) }
while order.status == 'processing'
p "Cert #{cert_name}: Sleep while order is processing"
@@ -241,6 +266,67 @@ def get_cert(order, cert_name, domains, domain_key)
return cert
end
+def find_ca_for_cert(cert_name, cert_opts, config)
+ cert_ca_account = cert_opts['ca_account'] || config.dig('defaults', 'certs', 'ca_account')
+ # cert_ca_name = config.dig('ca_accounts', cert_ca_account, 'ca')
+ # cert_ca_identity = config.dig('ca_accounts', cert_ca_account, 'identity')
+ #
+ # p "Cert #{cert_name}: Finding directory URL for CA"
+ # acme_directory_url = config.dig('CAs', cert_ca_name, 'directory_url')
+ #
+ # p "Cert #{cert_name}: Finding account to use for cert #{cert_name} from CA #{cert_ca_name}"
+ # account = config.dig('identities', cert_ca_account_name)
+ # email = account['email']
+ { cert_name => cert_ca_account }
+ # {
+ # 'ca' => cert_ca_name,
+ # 'account' => cert_ca_account_name,
+ # },
+ # }
+end
+
+def make_client_for_ca_account(ca, id)
+ p "CA #{ca['name']}: Finding directory URL for CA"
+ acme_directory_url = ca['directory_url']
+
+ private_key = read_account_key(id['keyfile'])
+
+ p "CA #{ca['name']}: Creating client object for communication with CA"
+ client = Acme::Client.new(private_key: private_key, directory: acme_directory_url)
+
+ email = id['email']
+
+ account_object = acme_request_with_retries { client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true) }
+ client
+end
+
+def handle_apex_authzs(apex, authzs, config)
+ p "apex: #{apex}"
+ primary_ns = config.dig('domains', authzs[0].domain, 'primary_ns') || config.dig('defaults', 'domains', 'primary_ns')
+ deploy_dns_tokens_on_apex(apex, authzs, primary_ns, config)
+end
+
+
+# domain_to_apex = {}
+#
+# domain_apex_threads = []
+# domains_with_apex = domains.map do |domain|
+# apex_thread = Thread.new(domain) do |d|
+# find_apex_domain(d).to_s
+# end
+# domain_apex_threads << apex_thread
+# domain_to_apex[domain] = apex_thread.value
+# { apex_thread.value => [domain] }
+# end
+# domain_apex_threads.each(&:join)
+#
+# domains_per_apex = domains_with_apex[0].merge(*domains_with_apex[1..]) do |_key, old, new|
+# old + new
+# end
+#
+# p "domains_per_apex"
+# pp domains_per_apex
+
config = read_config
@@ -248,68 +334,281 @@ cert_dir = config.dig('global', 'cert_dir') || './certs/'
ensure_cert_dir(cert_dir)
-acme_threads = []
-# iterate over configured certs
-# TODO: make this one thread per cert
-# TODO: check all domains for apex domain, deploy challenges for one apex_domain all at once
-config['certs'].each_pair do |cert_name, cert_opts|
- acme_threads << Thread.new(cert_name, cert_opts) do |cert_name, cert_opts|
- ensure_cert_dir(cert_dir + cert_name)
+# acme_threads = []
+# # iterate over configured certs
+# config['certs'].each_pair do |cert_name, cert_opts|
+# acme_threads << Thread.new(cert_name, cert_opts) do |cert_name, cert_opts|
+# ensure_cert_dir(cert_dir + cert_name)
+#
+# p "Cert #{cert_name}: Finding CA to use for cert"
+# cert_ca = cert_opts['ca'] || config.dig('defaults', 'certs', 'ca')
+# cert_ca_name = cert_ca['name']
+# cert_ca_account_name = cert_ca['account']
+#
+# p "Cert #{cert_name}: Finding directory URL for CA"
+# acme_directory_url = config.dig('CAs', cert_ca_name, 'directory_url')
+#
+# p "Cert #{cert_name}: Finding account to use for cert #{cert_name} from CA #{cert_ca_name}"
+# account = config.dig('ca_accounts', cert_ca_account_name)
+# email = account['email']
+#
+# private_key = read_account_key(account['keyfile'])
+#
+# p "Cert #{cert_name}: Creating client object for communication with CA"
+# client = Acme::Client.new(private_key: private_key, directory: acme_directory_url)
+#
+# acme_request_with_retries { client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true) }
+#
+# p "Cert #{cert_name}: Creating order object for cert #{cert_name}"
+# order = acme_request_with_retries { client.new_order(identifiers: cert_opts['domain_names']) }
+#
+# p "Cert #{cert_name}: order status"
+# p order.status
+#
+# if order.status != 'ready'
+# p "Cert #{cert_name}: Order is not ready, we need to authorize first"
+#
+# # TODO: collect dns modifications per primary NS, update all at once
+# p "Cert #{cert_name}: Iterating over required authorizations"
+# auths = acme_request_with_retries { order.authorizations }
+# auths.each do |auth|
+# p "Cert #{cert_name}: Processing authorization for #{auth.domain}"
+# p "Cert #{cert_name}: Finding challenge type for #{auth.domain}"
+# # p "Cert #{cert_name}: auth is:"
+# # pp auth
+# if auth.status == 'valid'
+# p "Cert #{cert_name}: Authorization for #{auth.domain} is still valid, skipping"
+# next
+# end
+#
+# challenge = auth.dns01
+# primary_ns = config.dig('domains', auth.domain, 'primary_ns') || config.dig('defaults', 'domains', 'primary_ns')
+# deploy_dns01_challenge_token(auth.domain, challenge, primary_ns, config)
+# wait_for_challenge_propagation(auth.domain, challenge)
+# wait_for_challenge_validation(challenge, cert_name)
+# end
+# else
+# p "Cert #{cert_name}: Order is ready, we don’t need to authorize"
+# end
+# domain_key = read_cert_key(cert_name)
+#
+# get_cert(order, cert_name, cert_opts['domain_names'], domain_key)
+# end
+# end
+#
+# acme_threads.each(&:join)
+
+
+# TODO: restructure process
+# DELAY THIS FOR NOW: check all certs’ lifetimes
+# DELAY THIS FOR NOW: decide which ones to renew
+# collect domain names for certs to be renewed
+# group domains in need of validation by apex_domain
+# collect ca_accounts that need to be used
+# THREAD PER ACCOUNT: create account object
+# hash: ca_account => account object
+# THREAD PER CA_ACCOUNT: create orders
+# THREAD PER DOMAIN: check validation status and collect domains needing validation
+# THREAD PER APEX DOMAIN:
+ # THREAD PER DOMAIN: request challenge
+ # deploy challenges with one DNS UPDATE per apex_domain
+ # THREAD PER NS: check propagation
+ # THREAD PER DOMAIN:
+ # request validation
+ # report back to main
+# THREAD PER CERT: when all domains for any cert are validated, finalize order
+
+
+domains = config['certs'].map { |_certname, cert_opts| cert_opts['domain_names'] }.flatten
+domain_attrs = domains.to_h { |d| [d, {}] }
+
+domain_apex_threads = {}
+domains.map do |d|
+ domain_apex_threads[d] = Thread.new(d) do |d|
+ p "finding apex for domain #{d}"
+ find_apex_domain(d).to_s
+ end
+end
+domain_apex_threads.each(&:join)
+
+domain_attrs.keys.each do |domain|
+ domain_attrs[domain][:apex] = domain_apex_threads[domain].value
+end
- p "Cert #{cert_name}: Finding CA to use for cert"
- cert_ca = cert_opts['ca'] || config.dig('defaults', 'certs', 'ca')
- cert_ca_name = cert_ca['name']
- cert_ca_account = cert_ca['account']
+apex_domains = domain_attrs.map do |_, v|
+ apex = v[:apex]
+ domains_under_apex = domain_attrs.filter { |d,v| v[:apex] == apex }.keys
+ [apex, { :domains => domains_under_apex }]
+end.uniq.to_h
- p "Cert #{cert_name}: Finding directory URL for CA"
- acme_directory_url = config.dig('CAs', cert_ca_name, 'directory_url')
- p "Cert #{cert_name}: Finding account to use for cert #{cert_name} from CA #{cert_ca_name}"
- account = config.dig('ca_accounts', cert_ca_account)
- email = account['email']
+apex_domain_ns_threads = {}
+apex_domains.keys.map do |d|
+ apex_domain_ns_threads[d] = Thread.new(d) do |d|
+ p "finding ns for apex domain #{d}"
+ res = lookup_ns(d)
+ ns_names = res.answer.map do |answer|
+ answer.rdata.to_s
+ end
+ end
+end
+apex_domain_ns_threads.each(&:join)
- private_key = read_account_key(account['keyfile'])
+apex_domains.keys.each do |domain|
+ apex_domains[domain][:ns] = apex_domain_ns_threads[domain].value
+ # p "#{domain}: #{apex_domains[domain][:ns]}"
+end
+# p apex_domains
- p "Cert #{cert_name}: Creating client object for communication with CA"
- client = Acme::Client.new(private_key: private_key, directory: acme_directory_url)
- acme_request_with_retries { client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true) }
+certs = config['certs']
+certs.each_pair do |name, opts|
+ opts['ca_account'] || certs[name]['ca_account'] = config.dig('defaults', 'certs', 'ca_account')
+end
+# # iterate over configured certs to find CA and account to use
+# cert_to_CA_account = config['certs'].map do |cert_name, cert_opts|
+# # ensure_cert_dir(cert_dir + cert_name)
+#
+# p "Cert #{cert_name}: Finding CA to use for cert"
+# find_ca_for_cert(cert_name, cert_opts, config)
+# end
+#
+# # p "cert_to_CA_account"
+# # pp cert_to_CA_account
+#
+# certs_to_CA = cert_to_CA_account[0].merge(*cert_to_CA_account[1..])
+# # p "certs_to_CA"
+# # p certs_to_CA
+
+
+# unique_CA_accounts_to_use = certs_to_CA.values.uniq
+# p "unique_CA_accounts_to_use: #{unique_CA_accounts_to_use}"
+
+ca_accounts = certs.map { |_, opts| opts['ca_account'] }.uniq
+
+
+account_threads = []
+ca_accounts.each do |ca_account|
+ account_threads << Thread.new(ca_account) do |ca_account|
+ ca_name = config.dig('ca_accounts', ca_account, 'ca')
+ ca = config.dig('CAs', ca_name)
+ ca_identity = config.dig('ca_accounts', ca_account, 'identity')
+ identity = config.dig('identities', ca_identity)
+ client = make_client_for_ca_account(ca, identity)
+ {
+ ca_account => client
+ }
+ end
+end
+account_threads.each(&:join)
+
+ca_clients = account_threads.map { |t| t.value }
+ca_clients = ca_clients[0].merge(*ca_clients[1..])
+
+# ca_clients = {}
+# account_threads.each do |t|
+# ca_account_to_client = t.value
+# ca_clients[ca_account_to_client['account']] = ca_account_to_client['client']
+# end
+
+
+# acme_threads = []
+certs.each_pair do |cert_name, cert_opts|
+ # p cert_opts
+ # p cert_opts['ca_account']
+ client = ca_clients[cert_opts['ca_account']]
+ # p "client"
+ # p client
+ domains = cert_opts['domain_names']
+ certs[cert_name]['order_thread'] = Thread.new(cert_name, client, domains) do |cert_name, client, domains|
p "Cert #{cert_name}: Creating order object for cert #{cert_name}"
- order = acme_request_with_retries { client.new_order(identifiers: cert_opts['domain_names']) }
-
- p "Cert #{cert_name}: order status"
- p order.status
-
- if order.status != 'ready'
- p "Cert #{cert_name}: Order is not ready, we need to authorize first"
-
- # TODO: collect dns modifications per primary NS, update all at once
- p "Cert #{cert_name}: Iterating over required authorizations"
- auths = acme_request_with_retries { order.authorizations }
- auths.each do |auth|
- p "Cert #{cert_name}: Processing authorization for #{auth.domain}"
- p "Cert #{cert_name}: Finding challenge type for #{auth.domain}"
- # p "Cert #{cert_name}: auth is:"
- # pp auth
- if auth.status == 'valid'
- p "Cert #{cert_name}: Authorization for #{auth.domain} is still valid, skipping"
- next
- end
+ acme_request_with_retries { client.new_order(identifiers: domains) }
+ end
+end
- challenge = auth.dns01
- primary_ns = config.dig('domains', auth.domain, 'primary_ns') || config.dig('defaults', 'domains', 'primary_ns')
- deploy_dns01_challenge_token(auth.domain, challenge, primary_ns, config)
- wait_for_challenge_propagation(auth.domain, challenge)
- wait_for_challenge_validation(challenge, cert_name)
- end
- else
- p "Cert #{cert_name}: Order is ready, we don’t need to authorize"
- end
- domain_key = read_cert_key(cert_name)
+certs.each_pair do |cert_name, cert_opts|
+ t = cert_opts['order_thread']
+ t.join
+ certs[cert_name]['order'] = t.value
+end
+
+# pp certs
+
+# certs_to_CA.each_pair do |cert_name, ca|
+# # p "ca_clients[ca]: #{ca_clients[ca]}"
+# acme_threads << Thread.new(cert_name, ca_clients[ca], config) do |cert_name, client, config|
+# p "Cert #{cert_name}: Creating order object for cert #{cert_name}"
+# cert_opts = config['certs'][cert_name]
+# order = acme_request_with_retries { client.new_order(identifiers: cert_opts['domain_names']) }
+# { cert_name => order }
+# end
+# end
+#
+# acme_threads.each(&:join)
+# orders = acme_threads.map(&:value)
+# orders = orders[0].merge(*orders[1..])
+# p "orders:"
+# pp orders
+
+# order_threads = []
+# # for each order do
+# # if its ready skip to getting cert
+# # THREADS: get authorizations
+# authorizations = {}
+# orders.each_pair do |cert_name, order|
+# order_authorizations = acme_request_with_retries { order.authorizations }
+# authorizations[cert_name] = order_authorizations
+# end
+
+certs.each_pair do |cert, opts|
+ order_authorizations = acme_request_with_retries { opts['order'].authorizations }
+ order_authorizations.each do |auth|
+ pp auth
+ # pp auth.domain
+ # pp auth.challenges
+ # pp auth.url
+ domain_attrs[auth.domain]['auth'] = auth
+ end
+end
- get_cert(order, cert_name, cert_opts['domain_names'], domain_key)
+all_authorizations = domain_attrs.map do |d, attrs|
+ attrs['auth']
+end
+
+authzs_by_apex = all_authorizations.group_by do |auth|
+ domain_attrs[auth.domain][:apex]
+end
+
+authzs_by_apex.each_pair do |apex, authzs|
+ handle_apex_authzs(apex, authzs, config)
+end
+
+propagation_threads = []
+domain_attrs.each_pair do |d, attrs|
+ domain_attrs[d][:propagation_thread] = Thread.new(attrs['auth']) do |auth|
+ wait_for_challenge_propagation(auth.domain, auth.dns01)
end
+ propagation_threads << domain_attrs[d][:propagation_thread]
end
-acme_threads.each(&:join)
+propagation_threads.each(&:join)
+
+validation_threads = {}
+domain_attrs.each_pair do |d, attrs|
+ # domain_attrs[d][:auth_thread] = Thread.new(attrs) do |attrs|
+ validation_threads[d] = Thread.new(attrs) do |attrs|
+ # wait_for_challenge_validation(auth.domain, auth.dns01)
+ # pp auth.dns01
+ # pp auth.dns01
+ wait_for_challenge_validation(attrs['auth'].dns01, attrs['auth'].domain)
+ end
+ # validation_threads[d] = domain_attrs[d][:auth_thread]
+end
+
+validation_threads.each_pair { |d, t| t.join }
+
+certs.each_pair do |name, opts|
+ cert_key = read_cert_key(name)
+ get_cert(opts['order'], name, opts['domain_names'], cert_key)
+end