Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/handle expire date #49

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 29 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ redirects on their verification requests:
```ruby
Rails.application.configure do
# <...>

config.middleware.insert_before ActionDispatch::SSL, Letsencrypt::Middleware

# <...>
Expand All @@ -68,7 +68,7 @@ which you should set.
configured to answer to all these domains, because LetsEncrypt will make a
request to verify ownership.

If you leave this blank, the gem will try and use the Heroku API to get a
If you leave this blank, the gem will try and use the Heroku API to get a
list of configured domains for your app, and verify all of them.
* `ACME_EMAIL`: Your email address, should be valid.
* `HEROKU_TOKEN`: An API token for this app. See below
Expand Down Expand Up @@ -103,8 +103,20 @@ Use the output of that to set the token (`HEROKU_TOKEN`).

## Using for the first time

After deploying, run `heroku run rake letsencrypt:renew`. Ensure that the
output looks good:
After deploy, you can run

on Rails 3.2, Rails 4

```
heroku run bundle exec rake letsencrypt:renew
```
on Rails 5+

```
heroku run bundle exec rails letsencrypt:renew
```

Ensure that the output looks good:

```
$ heroku run rake letsencrypt:renew
Expand All @@ -115,7 +127,7 @@ Setting config vars on Heroku...Done!
Giving config vars time to change...Done!
Testing filename works (to bring up app)...done!
Adding new certificate...Done!
$
$
```

If this is the first time you have used an SNI-based SSL certificate on your
Expand All @@ -124,24 +136,19 @@ app, you may need to alter your DNS configuration as per

You can see these details by typing `heroku domains`.

Sometime you have to add a new domain to the heroku application. In that case you may want
to force generation of a new certificate. In order to perform this, run

```
heroku run bundle exec rails 'letsencrypt:renew[:force_renew]'
```

## Adding a scheduled task

You should add a scheduled task on Heroku to renew the certificate. The
scheduled task should be configured to run `rake letsencrypt:renew` as often
as you want to renew your certificate. Letsencrypt certificates are valid for
90 days, but there's no harm renewing them more frequently than that.

Heroku Scheduler only lets you run a task as infrequently as once a day, but
you don't want to renew your SSL certificate every day (you will hit
[the rate limit](https://letsencrypt.org/docs/rate-limits/)). You can make it
run less frequently using a shell control statement. For example to renew your
certificate on the 1st day of every month:

```
if [ "$(date +%d)" = 01 ]; then rake letsencrypt:renew; fi
```

Source: [blog.dbrgn.ch](https://blog.dbrgn.ch/2013/10/4/heroku-schedule-weekly-monthly-tasks/)
90 days, and usually it will renewed when there are less then 30 from the expire date.

## Security considerations

Expand All @@ -153,7 +160,7 @@ following security considerations:
token to impersonate the account it was created with when accessing
the Heroku API. This is important if your account has access to other apps
that your collaborators don’t. Additionally, if your application environment was
leaked this would give the attacker access to the Heroku API as your user account.
leaked this would give the attacker access to the Heroku API as your user account.
[More information about Heroku’s API and oAuth](https://devcenter.heroku.com/articles/oauth#direct-authorization).

You should create the API token from a suitably locked-down account.
Expand All @@ -165,7 +172,7 @@ following security considerations:

The gem performs some cursory checks to make sure the filename is roughly
what is expected to try and mitigate this.

## Troubleshooting

### Common name invalid errors (security certificate is from *.herokuapp.com)
Expand All @@ -180,7 +187,7 @@ Your domain is still configured as a CNAME or ALIAS to `your-app.herokuapp.com`.
- Stop using a fork of the `platform-api` gem once it supports the SNI endpoint
API calls. [See issue #49 of the platform-api gem](https://github.com/heroku/platform-api/issues/49).

- Provide instructions for running the gem decoupled from the app it is
- Provide instructions for running the gem decoupled from the app it is
securing, for the paranoid.

## Contributing
Expand All @@ -197,7 +204,7 @@ Your domain is still configured as a CNAME or ALIAS to `your-app.herokuapp.com`.
- Please try not to mess with the Rakefile, version, or history. If you want to
have your own version, or is otherwise necessary, that is fine, but please
isolate to its own commit so I can cherry-pick around it.

### Generating a new release

1. Bump the version: `rake version:bump:{major,minor,patch}`.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.0
1.2.1
6 changes: 3 additions & 3 deletions letsencrypt-rails-heroku.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
# -*- encoding: utf-8 -*-
# stub: letsencrypt-rails-heroku 1.2.0 ruby lib
# stub: letsencrypt-rails-heroku 1.2.1 ruby lib

Gem::Specification.new do |s|
s.name = "letsencrypt-rails-heroku"
s.version = "1.2.0"
s.version = "1.2.1"

s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.require_paths = ["lib"]
s.authors = ["Pixie Labs", "David Somers", "Abigail McPhillips"]
s.date = "2017-03-03"
s.date = "2017-03-11"
s.description = "This gem automatically handles creation, renewal, and applying SSL certificates from LetsEncrypt to your Heroku account."
s.email = "team@pixielabs.io"
s.extra_rdoc_files = [
Expand Down
10 changes: 7 additions & 3 deletions lib/letsencrypt-rails-heroku/letsencrypt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ def self.configure
end

def self.challenge_configured?
configuration.acme_challenge_filename &&
configuration.acme_challenge_filename &&
configuration.acme_challenge_filename.start_with?(".well-known/") &&
configuration.acme_challenge_file_content
end

class Configuration
attr_accessor :heroku_token, :heroku_app, :acme_email, :acme_domain,
:acme_endpoint, :ssl_type
:acme_endpoint, :ssl_type, :acme_renew_window, :acme_force_renew

# Not settable by user; part of the gem's behaviour.
attr_reader :acme_challenge_filename, :acme_challenge_file_content

Expand All @@ -35,5 +35,9 @@ def initialize
def valid?
heroku_token && heroku_app && acme_email
end

def need_renew?
(acme_expire_on - acme_renew_window) <= Time.now
end
end
end
87 changes: 60 additions & 27 deletions lib/tasks/letsencrypt.rake
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
require 'open-uri'
require 'openssl'
require 'acme-client'
Expand All @@ -6,14 +7,57 @@ require 'platform-api'
namespace :letsencrypt do

desc 'Renew your LetsEncrypt certificate'
task :renew do
# Check configuration looks OK
abort "letsencrypt-rails-heroku is configured incorrectly. Are you missing an environment variable or other configuration? You should have a heroku_token, heroku_app and acme_email configured either via a `Letsencrypt.configure` block in an initializer or as environment variables." unless Letsencrypt.configuration.valid?
task :renew, [:policy] do |_t, attributes|
policy = attributes.fetch(:policy, :keep_until_expiring).to_sym

unless Letsencrypt.configuration.valid?
abort "letsencrypt-rails-heroku is configured incorrectly. Are you missing an environment variable or other configuration? You should have a heroku_token, heroku_app and acme_email configured either via a 'Letsencrypt.configure' block in an initializer or as environment variables."
end

# Set up Heroku client
heroku = PlatformAPI.connect_oauth Letsencrypt.configuration.heroku_token
heroku_app = Letsencrypt.configuration.heroku_app

domains = []
if Letsencrypt.configuration.acme_domain
puts "Using ACME_DOMAIN configuration variable..."
domains = Letsencrypt.configuration.acme_domain.split(',').map(&:strip)
else
domains = heroku.domain.list(heroku_app).map{|domain| domain['hostname']}
puts "Using #{domains.length} configured Heroku domain(s) for this app..."
end

if policy == :keep_until_expiring
print "Verify if a valid certificate already exists ... "
uri = URI.parse("https://#{domains.first}")
http = Net::HTTP.new(uri.host,uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
begin
http.start do |h|
@cert = h.peer_cert
end
rescue OpenSSL::SSL::SSLError => e
# in this case a certificate doesn't exists, so a do nothing
puts "No valid certicates found!"
else
print "Found "
# if a valid certificate exists check if is still expiring
if @cert.not_after >= Date.today + 30.days
puts " but certificate is not due to expire. Skipping renew!"
exit 0
else
puts " but is due to expire!"
end
end
elsif policy == :force_renew
puts "Forcing renew!"
else
puts "Renew policy not valid"
exit
end


# Create a private key
print "Creating account key..."
private_key = OpenSSL::PKey::RSA.new(4096)
Expand All @@ -27,15 +71,6 @@ namespace :letsencrypt do
registration.agree_terms
puts "Done!"

domains = []
if Letsencrypt.configuration.acme_domain
puts "Using ACME_DOMAIN configuration variable..."
domains = Letsencrypt.configuration.acme_domain.split(',').map(&:strip)
else
domains = heroku.domain.list(heroku_app).map{|domain| domain['hostname']}
puts "Using #{domains.length} configured Heroku domain(s) for this app..."
end

domains.each do |domain|
puts "Performing verification for #{domain}:"

Expand All @@ -44,17 +79,17 @@ namespace :letsencrypt do

print "Setting config vars on Heroku..."
heroku.config_var.update(heroku_app, {
'ACME_CHALLENGE_FILENAME' => challenge.filename,
'ACME_CHALLENGE_FILE_CONTENT' => challenge.file_content
})
'ACME_CHALLENGE_FILENAME' => challenge.filename,
'ACME_CHALLENGE_FILE_CONTENT' => challenge.file_content
})
puts "Done!"

# Wait for app to come up
print "Testing filename works (to bring up app)..."

# Get the domain name from Heroku
hostname = heroku.domain.list(heroku_app).first['hostname']

# Wait at least a little bit, otherwise the first request will almost always fail.
sleep(2)

Expand Down Expand Up @@ -105,9 +140,9 @@ namespace :letsencrypt do
# Unset temporary config vars. We don't care about waiting for this to
# restart
heroku.config_var.update(heroku_app, {
'ACME_CHALLENGE_FILENAME' => nil,
'ACME_CHALLENGE_FILE_CONTENT' => nil
})
'ACME_CHALLENGE_FILENAME' => nil,
'ACME_CHALLENGE_FILE_CONTENT' => nil
})

# Create CSR
csr = Acme::Client::CertificateRequest.new(names: domains)
Expand All @@ -131,23 +166,21 @@ namespace :letsencrypt do
if certificates.any?
print "Updating existing certificate #{certificates[0]['name']}..."
endpoint.update(heroku_app, certificates[0]['name'], {
certificate_chain: certificate.fullchain_to_pem,
private_key: certificate.request.private_key.to_pem
})
certificate_chain: certificate.fullchain_to_pem,
private_key: certificate.request.private_key.to_pem
})
puts "Done!"
else
print "Adding new certificate..."
endpoint.create(heroku_app, {
certificate_chain: certificate.fullchain_to_pem,
private_key: certificate.request.private_key.to_pem
})
certificate_chain: certificate.fullchain_to_pem,
private_key: certificate.request.private_key.to_pem
})
puts "Done!"
end
rescue Excon::Error::UnprocessableEntity => e
warn "Error adding certificate to Heroku. Response from Heroku’s API follows:"
raise Letsencrypt::Error::HerokuCertificateError, e.response.body
end

end

end