summaryrefslogtreecommitdiff
path: root/macir.rb
diff options
context:
space:
mode:
authorHendrik Jäger <gitcommit@henk.geekmail.org>2024-02-02 22:49:42 +0100
committerHendrik Jäger <gitcommit@henk.geekmail.org>2024-02-02 22:49:42 +0100
commitb065345f861c1f1523d71d794f3749ef15e07ccd (patch)
treef097b3ed459db3c097eecd17c3fb4018113508dd /macir.rb
parent569f79b0b52e224c22ea725464799a886b40aef0 (diff)
cleanup linter grievances; general tidying; less sleeping
Diffstat (limited to 'macir.rb')
-rw-r--r--macir.rb315
1 files changed, 209 insertions, 106 deletions
diff --git a/macir.rb b/macir.rb
index 3c8ca0c..e950fb7 100644
--- a/macir.rb
+++ b/macir.rb
@@ -1,104 +1,109 @@
#!/usr/bin/ruby
+# frozen_string_literal: true
+
require 'yaml'
require 'openssl'
require 'acme-client'
require 'dnsruby'
require 'time'
+require 'English'
-def read_config( path = 'config.yaml' )
+def read_config(path = 'config.yaml')
p "Reading config from #{path}"
- begin
- config = YAML.load_file( path )
- rescue Psych::SyntaxError
- $stderr.puts "Parsing configfile failed: " + $!.to_s
- raise
- rescue Errno::ENOENT
- $stderr.puts "IO failed: " + $!.to_s
- raise
- end
- return config
+ YAML.load_file(path)
+rescue Psych::SyntaxError => e
+ warn "Parsing configfile failed: #{e}"
+ raise
+rescue Errno::ENOENT => e
+ warn "IO failed: #{e}"
+ raise
end
-def ensure_cert_dir( path = './certs' )
- if not File.exist?( path )
+def ensure_cert_dir(path = './certs')
+ unless File.exist?(path)
puts 'Certificate directory does not exist. Creating with secure permissions.'
- Dir.mkdir( path, 0700 )
- File.chmod( 0700, path )
- end
- if File.world_writable?( path )
- $stderr.puts "WARNING! Certificate directory is world writable! This could be a serious security issue!"
- end
- if File.world_readable?( path )
- $stderr.puts "WARNING! Certificate directory is world readable! This could be a serious security issue!"
- end
- if File.file?( path )
- raise( 'Certificate directory is not a directory but a file. Aborting.' )
- end
- if not File.writable?( path )
- raise( 'Certificate directory is not writable. Aborting.' )
+ Dir.mkdir(path, 0o0700)
end
+ File.world_writable?(path) && warn('WARNING! Certificate directory is world writable! This could be a serious security issue!')
+ File.world_readable?(path) && warn('WARNING! Certificate directory is world readable! This could be a serious security issue!')
+ File.file?(path) && raise('Certificate directory is not a directory but a file. Aborting.')
+ File.writable?(path) || raise('Certificate directory is not writable. Aborting.')
end
-def read_account_key( path = 'pkey.pem' )
+def read_account_key(path = 'pkey.pem')
p "Reading account key from #{path}"
- if File.readable?( path )
+ if File.readable?(path)
p "File #{path} is readable, trying to parse"
- privatekey_string = File.read( path )
- private_key = OpenSSL::PKey::EC.new( privatekey_string )
+ privatekey_string = File.read(path)
+ private_key = OpenSSL::PKey::EC.new(privatekey_string)
+ elsif File.exist?(path)
+ raise("The file #{path} exists but is not readable. Make it readable or specify different path")
else
- if File.exists?( path )
- raise( "The file #{path} exists but is not readable. Make it readable or specify different path" )
- else
- p "File #{path} does not exist, trying to create"
- private_key = OpenSSL::PKey::EC.generate( "prime256v1" )
- File.write( path, private_key.private_to_pem )
- end
+ p "File #{path} does not exist, trying to create"
+ private_key = OpenSSL::PKey::EC.generate('prime256v1')
+ File.write(path, private_key.private_to_pem)
end
return private_key
end
-def read_cert_key( cert_name )
+def read_cert_key(cert_name)
folder = "./certs/#{cert_name}/"
path = "#{folder}/current.key"
p "cert_name #{cert_name}: Reading cert key from #{path}"
- if File.readable?( path )
+ if File.readable?(path)
p "cert_name #{cert_name}: File #{path} is readable, trying to parse"
- privatekey_string = File.read( path )
- private_key = OpenSSL::PKey::EC.new( privatekey_string )
+ privatekey_string = File.read(path)
+ private_key = OpenSSL::PKey::EC.new(privatekey_string)
+ elsif File.exist?(path)
+ raise("cert_name #{cert_name}: The file #{path} exists but is not readable. Make it readable or specify different path")
else
- if File.exists?( path )
- raise( "cert_name #{cert_name}: The file #{path} exists but is not readable. Make it readable or specify different path" )
- else
- p "cert_name #{cert_name}: File #{path} does not exist, trying to create"
- private_key = OpenSSL::PKey::EC.generate( "prime256v1" )
- pkey_file = File.new( folder + Time.now.to_i.to_s + ".key", 'w' )
- pkey_file.write( private_key.private_to_pem )
- File.symlink( File.basename( pkey_file ), File.dirname( pkey_file ) + "/current.key" )
- end
+ p "cert_name #{cert_name}: File #{path} does not exist, trying to create"
+ private_key = OpenSSL::PKey::EC.generate('prime256v1')
+ pkey_file = File.new("#{folder}#{Time.now.to_i}.key", 'w')
+ pkey_file.write(private_key.private_to_pem)
+ File.symlink(File.basename(pkey_file), "#{File.dirname(pkey_file)}/current.key")
end
return private_key
end
-def deploy_dns01_challenge_token( domain, challenge, nameserver, config )
+def lookup_soa(domain)
+ rec = Dnsruby::Recursor.new
+ p "Domain #{domain}: Getting SOA records for #{domain}"
+ rec.query_no_validation_or_recursion(domain, 'SOA')
+rescue StandardError => e
+ warn "Domain #{domain}: SOA lookup during deploy failed: #{e}"
+ raise
+end
+
+def find_apex_domain(domain)
+ domain_soa_resp = lookup_soa(domain)
+ if domain_soa_resp.answer.empty?
+ domain_soa_resp.authority[0].name
+ else
+ domain_soa_resp.answer[0].name
+ end
+end
+
+def deploy_dns01_challenge_token(domain, challenge, nameserver, config)
p "Domain #{domain}: Creating DNS UPDATE packet"
- update = Dnsruby::Update.new( domain )
+
+ apex_domain = find_apex_domain(domain)
+
+ update = Dnsruby::Update.new(apex_domain)
# TODO: delete challenge token record after validation
- update.delete( challenge.record_name + "." + domain, challenge.record_type )
- update.add( challenge.record_name + "." + domain, challenge.record_type, 10, challenge.record_content )
+ update.delete("#{challenge.record_name}.#{domain}", challenge.record_type)
+ update.add("#{challenge.record_name}.#{domain}", challenge.record_type, 10, challenge.record_content)
p "Domain #{domain}: Creating object for contacting nameserver"
- res = Dnsruby::Resolver.new( nameserver )
+ 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 tsig_name
- p tsig_key
- p tsig_alg
+ 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(
@@ -112,22 +117,38 @@ def deploy_dns01_challenge_token( domain, challenge, nameserver, config )
p "Domain #{domain}: Signing DNS UPDATE packet with TSIG object"
tsig.apply(update)
- p update
p "Domain #{domain}: Sending UPDATE to nameserver"
+ res.send_message(update)
+rescue StandardError => e
+ warn "Domain #{domain}: DNS Update failed: #{e}"
+ raise
+end
+
+def wait_for_challenge_propagation(domain, challenge)
+ rec = Dnsruby::Recursor.new
+ p "Domain #{domain}: Getting SOA records for #{domain}"
begin
- response = res.send_message(update)
- rescue Exception
- $stderr.puts "Domain #{domain}: IO failed: " + $!.to_s
+ 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
-end
+ apex_domain = if domain_soa_resp.answer.empty?
+ domain_soa_resp.authority[0].name
+ else
+ domain_soa_resp.answer[0].name
+ end
-def wait_for_challenge_propagation( domain, challenge )
p "Domain #{domain}: Creating recursor object for checking challenge propagation"
rec = Dnsruby::Recursor.new
- p "Domain #{domain}: Getting NS records for #{domain}"
- domain_auth_ns = rec.query_no_validation_or_recursion( domain, "NS" )
+ 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
p "Domain #{domain}: Checking challenge status on all NS"
@@ -137,65 +158,121 @@ def wait_for_challenge_propagation( domain, challenge )
threads << Thread.new(ns) do |my_ns|
nameserver = my_ns.rdata.to_s
p "Domain #{domain}: Creating resolver object for checking propagation on #{nameserver}"
- res = Dnsruby::Resolver.new( nameserver )
+ res = Dnsruby::Resolver.new(nameserver)
res.dnssec = false
res.do_caching = false
begin
p "Domain #{domain}: Querying ACME challenge record"
- result = res.query_no_validation_or_recursion( "_acme-challenge." + domain, "TXT" )
+ begin
+ result = res.query_no_validation_or_recursion("_acme-challenge.#{domain}", 'TXT')
+ rescue Dnsruby::NXDomain
+ p "Domain #{domain}: Not yet propagated, sleeping before checking again"
+ Thread.pass
+ sleep(0.1)
+ retry
+ rescue StandardError => e
+ warn "Domain #{domain}: ACME challenge lookup failed: #{e}"
+ raise
+ end
# p result
propagated = result.answer.any? do |answer|
answer.rdata[0] == challenge.record_content
end
unless propagated
p "Domain #{domain}: Not yet propagated, sleeping before checking again"
- sleep(1)
+ Thread.pass
+ sleep(0.1)
end
end until propagated
end
end
- threads.each { |thread| thread.join }
+ threads.each(&:join)
end
-def wait_for_challenge_validation( challenge )
+def wait_for_challenge_validation(challenge)
p 'Requesting validation of challenge'
- challenge.request_validation
+ begin
+ retries ||= 0
+ challenge.request_validation
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
while challenge.status == 'pending'
p 'Sleeping because challenge validation is pending'
sleep(1)
p 'Checking again'
- challenge.reload
+ begin
+ retries ||= 0
+ challenge.reload
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
end
end
-def get_cert( order, cert_name, domains, domain_key )
+def get_cert(order, cert_name, domains, domain_key)
path = "./certs/#{cert_name}/"
crt_file = "#{path}/cert.pem"
p "Cert #{cert_name}: Creating CSR object"
csr = Acme::Client::CertificateRequest.new(
private_key: domain_key,
names: domains,
- subject: { common_name: "#{domains[0]}" }
+ subject: { common_name: domains[0] }
)
p "Cert #{cert_name}: Finalize cert order"
- order.finalize(csr: csr)
+ begin
+ retries ||= 0
+ order.reload
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
+ begin
+ retries ||= 0
+ order.finalize(csr: csr)
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
while order.status == 'processing'
p "Cert #{cert_name}: Sleep while order is processing"
sleep(1)
p "Cert #{cert_name}: Rechecking order status"
- order.reload
+ begin
+ retries ||= 0
+ order.reload
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
+ end
+ # p "order status: #{order.status}"
+ # pp order
+ begin
+ retries ||= 0
+ cert = order.certificate
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
end
- cert = order.certificate
p "Cert #{cert_name}: Writing cert"
- cert_file = File.new( path + Time.now.to_i.to_s + ".crt", 'w' )
- cert_file.write( cert )
- if File.symlink?( File.dirname( cert_file ) + "/current.crt" )
- File.unlink( File.dirname( cert_file ) + "/current.crt" )
- File.symlink( File.basename( cert_file ), File.dirname( cert_file ) + "/current.crt" )
- elsif File.file?( File.dirname( cert_file ) + "/current.crt" )
+ cert_file = File.new(path + Time.now.to_i.to_s + ".crt", 'w')
+ cert_file.write(cert)
+ if File.symlink?(File.dirname(cert_file) + "/current.crt")
+ File.unlink(File.dirname(cert_file) + "/current.crt")
+ File.symlink(File.basename(cert_file), File.dirname(cert_file) + "/current.crt")
+ elsif File.file?(File.dirname(cert_file) + "/current.crt")
raise 'Could not place symlink for "current.crt" because that is already a normal file.'
end
return cert
@@ -206,63 +283,89 @@ config = read_config
cert_dir = config.dig('global', 'cert_dir') || './certs/'
-ensure_cert_dir( cert_dir )
+ensure_cert_dir(cert_dir)
acme_threads = []
# iterate over configured certs
# TODO: make this one thread per cert
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 )
+ 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 = cert_opts['ca'] || config.dig('defaults', 'certs', 'ca')
cert_ca_name = cert_ca['name']
cert_ca_account = cert_ca['account']
p "Cert #{cert_name}: Finding directory URL for CA"
- acme_directory_url = config.dig( 'CAs', cert_ca_name, 'directory_url' )
+ 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 )
+ account = config.dig('ca_accounts', cert_ca_account)
email = account['email']
- private_key = read_account_key( account['keyfile'] )
+ 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 )
-
- client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
+ client = Acme::Client.new(private_key: private_key, directory: acme_directory_url)
+
+ begin
+ retries ||= 0
+ client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
p "Cert #{cert_name}: Creating order object for cert #{cert_name}"
- order = client.new_order(identifiers: cert_opts['domain_names'] )
+ begin
+ retries ||= 0
+ order = client.new_order(identifiers: cert_opts['domain_names'])
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
+
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"
p "Cert #{cert_name}: Iterating over required authorizations"
- order.authorizations.each do |auth|
+ begin
+ retries ||= 0
+ auths = order.authorizations
+ rescue Acme::Client::Error::BadNonce
+ retries += 1
+ p 'Retrying because of invalid nonce.'
+ retry if retries <= 5
+ end
+ 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 )
+ 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)
end
else
p "Cert #{cert_name}: Order is ready, we don’t need to authorize"
end
- domain_key = read_cert_key( cert_name )
+ domain_key = read_cert_key(cert_name)
- get_cert( order, cert_name, cert_opts['domain_names'], domain_key )
+ get_cert(order, cert_name, cert_opts['domain_names'], domain_key)
end
end
-acme_threads.each { |thread| thread.join }
+acme_threads.each(&:join)