Generating SSL Certificates

July 16, 2017


Objective

The purpose of this blog post is to explain how to generate a valid SSL certificate issued by Let's Encrypt. The process discussed in this post uses the Let's Encrypt certbot client, and the webiste is a django-powered site deployed on the Heroku platform using a domain name obtained from GoDaddy.

Summary Steps

Generating free SSL certificates using Let's Encrypt is very easy once you know how to do it and the process takes 5-10 minutes once everything is in place. That's a good thing since Let's Encrypt certs need to be renewed every 90 days. This blog assumes a user has never used Let's Encrypt and that the target domain is deployed on Heroku. It's important to note that Heroku provides free SSL certificates for paid dynos which is the case for the CloudStorm website. However, using Let's Encrypt is so easy I haven't bothered switching to a Heroku provided SSL certificate...yet.

Below are the steps you will need to follow in order to generate a valid SSL certificate and upload to Heroku:

  1. Install certbot. I used HomeBrew to install the command-line tool: brew install certbot

  2. Generate a cert manually: sudo certbot certonly --manual -d www.cloudstormllc.com

  3. Return correct string: A person requesting a certificate from Let's Encrypt needs to prove they own the domain by returning a random string generated at the time you are creating a cert.

  4. Upload generated certs to Heroku: If everything works the certbot utility will print a message to the screen stating where the certificate and key has been stored. Certs are added using the following command after logging into the Heroku CLI: heroku certs:add /etc/letsencrypt/live/www.cloudstormllc.com/fullchain.pem /etc/letsencrypt/live/www.cloudstormllc.com/privkey.pem --app cloudstorm-django

Step 5: Add CNAME record: You need to configure your domain provider's records to point to the newly generated SSL certificate.

Detailed Steps

The steps above are sufficient in a perfect world...but things are never that easy are they? I ran into my fair share of issues getting the process to work in an error-free repeatable process. Below are some tips that I hope will be of use to you in case you run into the same issues. We will look at each step in more detail.

Install certbot. I have two MacBook Pro laptop computers that are the same model. Homebrew was able to install certbot without a problem on one of them, but it failed miserably on the second one. I took care of generating my SSL certs on the one computer before turning my attention to my second computer. Even though certbot worked fine on one computer I'm not one to leave a problem unsolved so I set about determining why certbot wouldn't install on my second computer.

I eventually was able to get certbot installed and running on my second MacBook. Below are the steps I followed in case you run into similar issues:

  • Create a virtual environmnet: virtualenv --no-site-packages -p /usr/bin/python venv2
  • Clone Certbot: git clone https://github.com/certbot/certbot
  • Change to the certbot repo: cd certbot
  • Activate the virtual environment: ../venv2/bin/activate
  • Install dependencies:
    • pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx
    • pip install hashin (not sure if this is required)
    • pip install -e .[docs] (not sure if this is required)
  • Run setup.py: python setup.py install
  • Run the certbot client: sudo certbot certonly --manual

Client launches; follow the prompts/commands and your certs will be generated.

Generate a cert manually: I generated a cert for my cloudstormllc.com domain first and that worked. However, GoDaddy doesn't support alias records which are required by Heroku. It was a painful learning experience so keep this in mind if your domain is provided by GoDaddy. I'm tempted to transfer my cloudstormllc.com domain to another provider such as DNSimple, but I have bigger dragons to slay at the moment. Below is a screenshot of executing the certbot utility:



Return correct string: Running the command in step 2 results in the user being prompted to return the random (very long) string from the specified URL. This poses a challenge because both the URL and response string is generated during runtime. It would be terribly inconvenient to write a specific URL and response string every time you had to renew your site's SSL certificate because that would require a new deployment to Heroku. If you're like me you simply don't just push production apps to Heorku no matter how seemingly trivial. In my case that would mean making a change on a dev branch, issuing a pull request to merge into master, deploying to my Heroku dev pipeline, and then finally promoting to Heroku production. That's a lot of work just to return a challenge string!

Fortunately, there is an elegant solution. The CloudStorm website is a django powered site running on Heroku. I created a Heroku configuration variable named LETS_ENCRYPT_RESPONSE so it returns the correct string when the endpoint is hit by the certbot client. The process is very simple. Cut and paste the challenge URL into the config variable and let the power of django do the rest. In order to do this you have to configure your settings to access the Heroku config variable, add a URL pattern to your urlpatterns, write a django view to handle the response, and render the reponse in a template. I know it sounds like a lot but it is rather trivial. Below are the relevant code snippets:

# settings.py
LETS_ENCRYPT_RESPONSE = env('LETS_ENCRYPT_RESPONSE', default='Test')

# urls.py
urlpatterns = [
    ...
    url(r'^.well-known/acme-challenge/(?P<str>[\w.+-:]+)$', lencrypt),
    ...
 ]

# views.py
def lencrypt(request, str):
    return render(request, "lencrypt.html", context={'le_str' : settings.LETS_ENCRYPT_RESPONSE})

# lencrypt.html - The following is the entire template
{{ le_str }}

Below is a brief description of what occurs after the configuration variable is updated in Heroku and the enter key is pressed in the certbot client:

  • The certbot client accesses the challenge URL which points to your site
  • The django app routes the challenge URL to its urlpatterns and it finds a match because any string after .well-known/acme-challenge/ is a match
  • The matched URL gets passed to its view which is lencrypt in this case
  • The lencrypt view sets the context which in this case is simply the updated challenge response and calls the template lencryt.html to render the response.
  • The response is rendered by displaying the challenge response expected by certbot.

The certbot client should generate a new SSl certificate if you have configured your Heroku deployed django app correctly:



Upload generated certs to Heroku: The certs are stored in /etc/letsencrypt/live which is a root owned directory. In order to access the files you may have to run the command sudo -s to switch to the root account before running the command shown in step 4 of the summary or move the generated pem files to another directory.



Add CNAME record: You only need to do this the first time. I logged into my GoDaddy account and added a CNAME record for www.cloudstormllc.com that points to www.cloudstormllc.com.herokudns.com. This record instructs sessions requesting https://www.cloudstormllc.com to use the certificate uploaded to Heroku in step 3.

Congratulations! At this point you have successfully generated an SSL certificate issued by Let's Encrypt and uploaded it to your site deployed on Heroku. You can check the details of your SSL certificate using a site such as https://www.sslshopper.com

Comment Enter a new comment:

On July 29, 2017 david.brady wrote: Reply

Keep in mind you will need to specify certs:update vs certs:add for subsequent renewals when updating new certificates in Heroku. The screen shot shows the update command but it may not be immediately clear.

On Dec. 10, 2017 coyote34 wrote: Reply

Great blog....thanks!