Let’s Encrypt automatic SSL certificate renewal on a single AWS instance

August 14, 2017


How we configured Elastic Beanstalk to play nicely with automatic certificate renewal by Let’s Encrypt.

Everyone it seems is going with Let’s Encrypt to generate their free SSL/TLS ceritficate. Running it once is easy, but getting it configured to work with Elastic Beanstalk and EC2’s lifecycle can send you round in circles. This post is an update of the original January 2017 post with our improved configuration.

The configuration needs to cater for ALL of these scenarios:

  1. The website is deployed and encrypted. - We are replacing the certificate with a Let’s Encrypt certificate before it expires.

  2. The website is deployed and encrypted but with an expired certificate. - The website is no longer accessible.

  3. The website is deployed and encrypted but with a missing certificate. - For example a new EC2 instance has been created.

The events we have at our disposal are:

  1. At Deployment
  2. On a Cron schedule

We are deploying a Rails app to a single EB instance. In the following sustitute ‘mywebsite’ with your website name.

Part 1) Add an Elastic Beanstalk post deploy hook

Configure Elastic Beanstalk to install and run certbot everytime the website is deployed. This ensures NGINX is running and everything is in place.

To do this we hook into the Elastic Beanstalk deployment sequence with a purposely named script that will run after the website is deployed /opt/elasticbeanstalk/hooks/appdeploy/post/99_run_certbot_config.sh

  1. Install certbot:

    mkdir -p /var/log/certbot
    chown ec2-user:ec2-user /var/log/certbot
    cd /home/ec2-user
    unset PYTHON_INSTALL_LAYOUT
    wget https://dl.eff.org/certbot-auto --output-file=/var/log/certbot/install.log
    chmod a+x certbot-auto
    
  2. Run certbot:

    ./certbot-auto certonly --debug --non-interactive --agree-tos --email mail@mywebsite.com --webroot --webroot-path /var/app/current/public --domains 'www.mywebsite.com' --text >> /var/log/certbot/install.log
    
    
  3. Tidyup:

    Currently certbot adds a -0001 folder when a certificate already exists, so there is some directory moving to be done before we reload the NGINX server to pick up changes to its configuration.

    DIRECTORY="/etc/letsencrypt/live/www.mywebsite.com"
    if [ -d "$DIRECTORY-0001" ]; then
      mv $DIRECTORY $DIRECTORY-old
      mv $DIRECTORY-0001 $DIRECTORY
    fi
    service nginx reload
    
  4. Logging:

    We have introduced some new logs to capture output, so we can configure Elastic Beanstalk to include these when we fetch the logs.

Putting it all together:

# .ebextensions/certbot.config

# Include certbot/logs in aws logs fetch
files:
  "/opt/elasticbeanstalk/tasks/taillogs.d/cloud-init.conf" :
    mode: "000755"
    owner: root
    group: root
    content: |
      /var/log/certbot/*.log
      /var/log/letsencrypt/letsencrypt.log

  "/opt/elasticbeanstalk/tasks/bundlelogs.d/applogs.conf" :
    mode: "000755"
    owner: root
    group: root
    content: |
      /var/log/certbot/*.log
      /var/log/letsencrypt/letsencrypt.log

  # Install & run certbot
  "/opt/elasticbeanstalk/hooks/appdeploy/post/99_run_certbot_config.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      mkdir -p /var/log/certbot
      chown ec2-user:ec2-user /var/log/certbot
      cd /home/ec2-user
      echo "=======|| $(date) START wget https://dl.eff.org/certbot-auto ||=======" >> /var/log/certbot/install.log
      unset PYTHON_INSTALL_LAYOUT
      wget https://dl.eff.org/certbot-auto --output-file=/var/log/certbot/install.log
      chmod a+x certbot-auto
      echo "=======|| $(date) START ./certbot-auto certonly ||=======" >> /var/log/certbot/install.log
      ./certbot-auto certonly --debug --non-interactive --agree-tos --email enquiries@mywebsite.com --webroot --webroot-path /var/app/current/public --domains 'www.mywebsite.com' --text >> /var/log/certbot/install.log
      echo "=======|| $(date) END ./certbot-auto certonly ||=======" >> /var/log/certbot/install.log
      DIRECTORY="/etc/letsencrypt/live/www.mywebsite.com"
      if [ -d "$DIRECTORY-0001" ]; then
        mv $DIRECTORY $DIRECTORY-old
        mv $DIRECTORY-0001 $DIRECTORY
      fi
      service nginx reload

Part 2) Configure NGINX

  1. Open up the challenge route on port 80

    In your NGINX config file, add an HTTP server section that opens the challenge route, but redirects for everything else:

    // etc/nginx/conf.d/ssl.conf
    
    ...
    # HTTP server
    server {
      listen 8080;
    
      access_log /var/log/nginx/access.log;
      error_log /var/log/nginx/error.log;
    
      server_name www.mywebsite.com;
    
      # enables Lets encrypt
      location /.well-known {
        allow all;
      }
      location / {
        return 301 https://$server_name$request_uri;
      }
    }
    ...
    
  2. Update the ssl_certificate & ssl_certificate_key locations

    // etc/nginx/conf.d/ssl.conf
    
    ...
    # HTTPS server
    server {
      listen 443;
      ...
      # SSL
      ssl on;
      ssl_certificate /etc/letsencrypt/live/www.mywebsite.com/fullchain.pem;
      ssl_certificate_key /etc/letsencrypt/live/www.mywebsite.com/privkey.pem;
      ...
      add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
      ...
    }
    ...
    
    

Part 3) Configure a Cron schedule

Add a cron schedule that auto renews every so often. This will run a script that includes the tidyup and NGINX reload.

# .ebextensions/cron.config

files:
  "/tmp/cron_certbot_renew.sh" :
    mode: "000777"
    content: |
      /home/ec2-user/certbot-auto renew --text >> /var/log/certbot/cron.log
      DIRECTORY="/etc/letsencrypt/live/www.mywebsite.com"
      if [ -d "$DIRECTORY-0001" ]; then
        mv $DIRECTORY $DIRECTORY-old
        mv $DIRECTORY-0001 $DIRECTORY
      fi
      service nginx reload

  "/tmp/cron_jobs" :
    mode: "000777"
    content: |
      52 17 * * * sudo bash /tmp/cron_certbot_renew.sh >> /var/log/certbot/cron.log

container_commands:
  01_remove_old_cron_jobs:
    command: "crontab -r || exit 0"
  02_cronjobs:
    command: "crontab /tmp/cron_jobs"
    leader_only: true

With this all in place you can deploy or re-deploy to add a certificate to a new instance; and let Cron take care of renewals.

© 2020 Keith P | Follow on Twitter | Git