diff --git a/README.md b/README.md index 4cc988d..e424fd7 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ redirects on their verification requests: ```ruby Rails.application.configure do # <...> - + config.middleware.insert_before ActionDispatch::SSL, Letsencrypt::Middleware # <...> @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. @@ -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) @@ -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 @@ -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}`. diff --git a/VERSION b/VERSION index 867e524..cb174d5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 \ No newline at end of file +1.2.1 \ No newline at end of file diff --git a/letsencrypt-rails-heroku.gemspec b/letsencrypt-rails-heroku.gemspec index 2d3954d..3efcd33 100644 --- a/letsencrypt-rails-heroku.gemspec +++ b/letsencrypt-rails-heroku.gemspec @@ -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 = [ diff --git a/lib/letsencrypt-rails-heroku/letsencrypt.rb b/lib/letsencrypt-rails-heroku/letsencrypt.rb index 4fa916a..417bb8e 100644 --- a/lib/letsencrypt-rails-heroku/letsencrypt.rb +++ b/lib/letsencrypt-rails-heroku/letsencrypt.rb @@ -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 @@ -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 diff --git a/lib/tasks/letsencrypt.rake b/lib/tasks/letsencrypt.rake index 56db067..ab3b057 100644 --- a/lib/tasks/letsencrypt.rake +++ b/lib/tasks/letsencrypt.rake @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- require 'open-uri' require 'openssl' require 'acme-client' @@ -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) @@ -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}:" @@ -44,9 +79,9 @@ 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 @@ -54,7 +89,7 @@ namespace :letsencrypt do # 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) @@ -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) @@ -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