From 2204d46cd94862960788100362f6c255db182b5f Mon Sep 17 00:00:00 2001 From: Dylan Jones Date: Mon, 18 Aug 2025 21:31:27 +0100 Subject: [PATCH] Add ACME client to renew SSL certificates --- .gitignore | 1 + BUILD.md | 6 +- app/controllers/static_pages_controller.rb | 1 + app/models/certificate.rb | 78 ++++++++++++++++++++++ app/models/go_daddy_dns.rb | 26 -------- bin/deploy | 2 + config/secrets.yml.example | 2 - config/server/default | 4 +- 8 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 app/models/certificate.rb delete mode 100644 app/models/go_daddy_dns.rb diff --git a/.gitignore b/.gitignore index abd45a0..626d37f 100755 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ config/secrets.yml /public/packs /public/packs-test /public/assets +/public/.well-known/acme-challenge /node_modules /yarn-error.log yarn-debug.log* diff --git a/BUILD.md b/BUILD.md index 83cc858..5e8a967 100644 --- a/BUILD.md +++ b/BUILD.md @@ -121,7 +121,7 @@ certbot certonly \ --agree-tos \ --email dyl@anjon.es \ --manual \ - --preferred-challenges dns \ + --preferred-challenges http \ --expand \ --renew-by-default \ -d dyl.anjon.es \ @@ -129,6 +129,10 @@ certbot certonly \ -d isitaproxyproblem.com \ -d dylanjones.info \ -d alice-jones.co.uk + +# Copy each verification to the public dir +echo "" > /home/rails/public/.well-known/acme-challenge/ + rm dyl.anjon.es.key.old rm dyl.anjon.es.crt.old cp dyl.anjon.es.crt dyl.anjon.es.crt.old diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 40ad195..b1dc727 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -154,6 +154,7 @@ def webfinger # GET /cron.json # GET /cron.xml def cron + Certificate.renew Track.update PringlesPrice.update Gig.update diff --git a/app/models/certificate.rb b/app/models/certificate.rb new file mode 100644 index 0000000..c0f8a38 --- /dev/null +++ b/app/models/certificate.rb @@ -0,0 +1,78 @@ +require 'openssl' +require 'fileutils' + + +class Certificate + + def self.renew + begin + current_cert = OpenSSL::X509::Certificate.new(File.read('/etc/website.crt')) + + if current_cert.not_after > 1.month.from_now + Rails.logger.info "Certificate is valid for at least another month (#{current_cert.not_after})" + return + end + + Rails.logger.info "Renewing certificate" + + private_key = OpenSSL::PKey.read(File.read('/etc/website.key')) + + client = Acme::Client.new(private_key: private_key, directory: 'https://acme-v02.api.letsencrypt.org/directory') + account = client.new_account(contact: 'mailto:dyl@anjon.es', terms_of_service_agreed: true) + + domains = [ + 'dyl.anjon.es', + 'ismytraindelayed.com', + 'isitaproxyproblem.com', + 'dylanjones.info', + 'alice-jones.co.uk' + ] + + order = client.new_order(identifiers: domains) + + Rails.logger.info "Removing old challenges" + FileUtils.rm_r "public/.well-known/acme-challenge" + + order.authorizations.each do |auth| + challenge = auth.http + file_path = "public/#{challenge.filename}" + dir_path = File.dirname file_path + FileUtils.mkdir_p dir_path + + Rails.logger.info "Writing challenge #{file_path}" + File.write(file_path, challenge.file_content) + challenge.request_validation + + timeout = 30 + until challenge.status != 'pending' or timeout == 0 + challenge.reload + sleep(2) + timeout -= 2 + end + end + + csr = Acme::Client::CertificateRequest.new(names: domains, subject: { common_name: domains.first }) + order.finalize(csr: csr) + + timeout = 30 + until order.status != 'processing' or timeout == 0 + order.reload + sleep(1) + timeout -= 1 + end + + Rails.logger.info "Writing new certificate" + File.write('/etc/website.crt', order.certificate) + + Rails.logger.info "Reloading nginx" + if system('sudo systemctl reload nginx') + Rails.logger.info("Reloaded nginx successfully") + else + Rails.logger.error "Reload of nginx failed. Status #{$?.exitstatus}" + end + end + rescue Exception => e + Rails.logger.error "Failed to renew certificate" + Rails.logger.error e.message + end +end diff --git a/app/models/go_daddy_dns.rb b/app/models/go_daddy_dns.rb deleted file mode 100644 index 7758fb1..0000000 --- a/app/models/go_daddy_dns.rb +++ /dev/null @@ -1,26 +0,0 @@ -class GoDaddyDns - - def self.update_txt domain, subdomain, value - auth = "sso-key #{Rails.application.secrets.go_daddy_key}:#{Rails.application.secrets.go_daddy_secret}" - headers = { authorization: auth, content_type: :json, accept: :json } - url = "https://api.godaddy.com/v1/domains/#{domain}/records/TXT" - payload = [ - { - "data" => value, - "name" => subdomain, - "ttl" => 1800, - "type" => "TXT" - } - ] - return RestClient.put url, payload.to_json, headers - end - - def self.update_dev_dylanjones_acme value - return update_txt "anjon.es", "_acme-challenge.dev.dyl", value - end - - def self.update_dylanjones_acme value - return update_txt "anjon.es", "_acme-challenge.dyl", value - end - -end diff --git a/bin/deploy b/bin/deploy index e2b3f2f..570fe89 100755 --- a/bin/deploy +++ b/bin/deploy @@ -13,6 +13,8 @@ yarn install rake assets:precompile RAILS_ENV=production bin/webpack cp config/server/unicorn /etc/default/unicorn +cp config/server/nginx /etc/nginx/sites-enabled/default chown -R rails /home/rails chgrp -R www-data /home/rails service unicorn restart +systemctl reload nginx diff --git a/config/secrets.yml.example b/config/secrets.yml.example index 20f8b09..5f3bf5d 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -21,8 +21,6 @@ defaults: &defaults database_password: redis_password: is_my_train_delayed_page_key: - go_daddy_key: - go_daddy_secret: development: <<: *defaults diff --git a/config/server/default b/config/server/default index 95fc280..7c3ddbf 100644 --- a/config/server/default +++ b/config/server/default @@ -34,8 +34,8 @@ server { listen [::]:443 ssl http2; root /home/rails/public; server_name dyl.anjon.es; - ssl_certificate /root/dyl.anjon.es.crt; - ssl_certificate_key /root/dyl.anjon.es.key; + ssl_certificate /etc/website.crt; + ssl_certificate_key /etc/website.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384; ssl_prefer_server_ciphers on;