Automated generation and renewal of ACME/Letsencrypt SSL certificates for Heroku apps.
There are two parts to the setup:
- Your appplications setup
- Creating a new sabayon app
In step 5 (above) ACME calls a specific, unique URL on your application that allows ownership to be validated. This URL is based upon config vars set by the sabayon app (both during initial create and on an ongoing basis).
There are a couple options to read the config vars and automagically create the appropriate URL endpoint.
For a static app
change the web
process type in your Procfile:
web: bin/start
Add a bin/start
file to your app:
#!/usr/bin/env ruby
data = []
if ENV['ACME_KEY'] && ENV['ACME_TOKEN']
data << {key: ENV['ACME_KEY'], token: ENV['ACME_TOKEN']}
else
ENV.each do |k, v|
if d = k.match(/^ACME_KEY_([0-9]+)/)
index = d[1]
data << {key: v, token: ENV["ACME_TOKEN_#{index}"]}
end
end
end
`mkdir -p dist/.well-known/acme-challenge`
data.each do |e|
`echo #{e[:key]} > dist/.well-known/acme-challenge/#{e[:token]}`
end
`bin/boot`
Make that file executable:
chmod +x bin/start
Commit this code then deploy your main app with those changes.
Add a route to handle the request. Based on schneems's codetriage commit.
There is also a rack example next if you would rather handle this in rack or if you have a non-rails app.
YourAppName::Application.routes.draw do
if ENV['ACME_KEY'] && ENV['ACME_TOKEN']
get ".well-known/acme-challenge/#{ ENV["ACME_TOKEN"] }" => proc { [200, {}, [ ENV["ACME_KEY"] ] ] }
else
ENV.each do |var, _|
next unless var.start_with?("ACME_TOKEN_")
number = var.sub(/ACME_TOKEN_/, '')
get ".well-known/acme-challenge/#{ ENV["ACME_TOKEN_#{number}"] }" => proc { [200, {}, [ ENV["ACME_KEY_#{number}"] ] ] }
end
end
end
Add the following rack middleware to your app:
class SabayonMiddleware
def initialize(app)
@app = app
end
def call(env)
data = []
if ENV['ACME_KEY'] && ENV['ACME_TOKEN']
data << {key: ENV['ACME_KEY'], token: ENV['ACME_TOKEN']}
else
ENV.each do |k, v|
if d = k.match(/^ACME_KEY_([0-9]+)/)
index = d[1]
data << {key: v, token: ENV["ACME_TOKEN_#{index}"]}
end
end
data.each do |e|
if env["PATH_INFO"] == "/.well-known/acme-challenge/#{e[:token]}"
return [200, {"Content-Type" => "text/plain"}, [e[:key]]]
end
end
@app.call(env)
end
end
end
In any other language, you need to be able to respond to requests on the path /.well-known/acme-challenge/$ACME_TOKEN
with $ACME_KEY
as the content.
Please add any other language/framework by opening a Pull Request.
In addition to configuring your application, you will also need to create a new Heroku application which will run sabayon to create and update the certificates for your main application.
To easily create a new Heroku applicaiton with the sabayon code, Click on this deploy button and fill all the required config vars.
You can then generate your first certificate with the following command (this will add configuration to your main application and restart it as well).
heroku run sabayon
Open the scheduler add-on provisioned, and add the following daily command to regenerate your certificate automatically one month before it expires:
sabayon
You can force-reload your app's certificate:
heroku run sabayon --force