-
-
Save boof/2465e2b7e59c3206ee17d950f49f3199 to your computer and use it in GitHub Desktop.
Update Heroku SSL Certificate using Let's Encrypt DNS challenge against INWX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require "bundler/inline" | |
gemfile do | |
source "https://rubygems.org" | |
gem "dotenv" | |
gem "inwx-domrobot" | |
gem "acme-client" | |
gem "jwt" | |
gem "platform-api" | |
end | |
require "dotenv/load" | |
# INWX_USERNAME | |
# INWX_PASSWORD | |
# INWX_TLD | |
# HEROKU_TOKEN via heroku plugins:install heroku-cli-oauth, heroku authorizations:create -d "ACME tooling" | |
# HEROKU_APP | |
# ACME_DOMAIN | |
# ACME_DIRECTORY is server from sudo certbot show_account | |
# ACME_KID is Account URL from sudo certbot show_account | |
# ACME_EMAIL is Email contact from sudo certbot show_account | |
# ACME_JWK from certbot, see /etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory/.../private_key.json | |
require "json" | |
require "resolv" | |
module E | |
extend self | |
def const_missing(name) = const_set name, ENV.fetch(name.to_s) | |
def method_missing(name, *) = ENV.fetch name.to_s | |
end | |
VERBOSE = %w[-d --debug].intersection(ARGV).any? | |
# this uses the already existing JWK from previous certbot run exported into ENV | |
jwk = JWT::JWK.new JSON.load(E.ACME_JWK) | |
client = Acme::Client.new private_key: jwk.signing_key, directory: E.ACME_DIRECTORY, kid: E.ACME_KID | |
domrobot = INWX::Domrobot.new | |
login = domrobot.set_language('en') | |
.use_live | |
.use_json | |
.set_debug(VERBOSE) | |
.login(E.INWX_USERNAME, E.INWX_PASSWORD) | |
abort "INWX login failed!" if login['code'] != 1000 | |
# create DNS challenge | |
order = client.new_order identifiers: [E::ACME_DOMAIN] | |
authorization = order.authorizations.first | |
challenge = authorization.dns | |
# create/update DNS record with challenge | |
name = "#{challenge.record_name}.#{E::ACME_DOMAIN}" | |
tld = domrobot.call "nameserver", "info", domain: E.INWX_TLD | |
record = tld.dig("resData", "record")&.find { _1["name"] == name } | |
op = if record | |
domrobot.call "nameserver", "updateRecord", id: record.fetch("id"), content: challenge.record_content | |
else | |
domrobot.call "nameserver", "createRecord", name:, type: challenge.record_type, content: challenge.record_content | |
end | |
domrobot.logout | |
# wait for DNS record to propagate | |
resolver = Resolv::DNS.new nameserver: %w[8.8.8.8 8.8.4.4] | |
begin | |
print "Press ENTER to continue (or type abort to exit) " | |
try_again = gets.chomp != "abort" | |
resource = resolver.getresource name, Resolv::DNS::Resource::IN::TXT | |
pending = resource.data != challenge.record_content | |
end while try_again && pending | |
abort "Challenge couldn't be propagated." if pending | |
# validate challenge | |
challenge.request_validation | |
while challenge.status == "pending" | |
sleep 2 | |
challenge.reload | |
end | |
abort "Challenge couldn't be validated." unless challenge.status == "valid" | |
# request signed certificate | |
pkey = OpenSSL::PKey::RSA.new 4096 | |
if VERBOSE | |
export = JWT::JWK.new(pkey, use: "sig").export include_private: true | |
puts JSON.fast_generate(export) | |
end | |
csr = Acme::Client::CertificateRequest.new private_key: pkey, subject: { common_name: E::ACME_DOMAIN } | |
order.finalize(csr:) | |
while order.status == 'processing' | |
sleep 1 | |
order.reload | |
end | |
# upload signed certificate to heroku | |
heroku = PlatformAPI.connect_oauth E.HEROKU_TOKEN | |
endpoint = heroku.sni_endpoint | |
certificate = endpoint.list(E::HEROKU_APP).find { _1.dig("ssl_cert", "cert_domains").include? E::ACME_DOMAIN } | |
if certificate | |
endpoint.update E::HEROKU_APP, certificate.fetch("name"), certificate_chain: order.certificate, private_key: pkey.to_pem | |
else | |
endpoint.create E::HEROKU_APP, certificate_chain: order.certificate, private_key: pkey.to_pem | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment