The recommended way to enable HTTPS in Elastic Beanstalk is to use one of AWS’s load balancers such as the Application Load Balancer (ALB) which supports autoscaling, fault tolerance, and other things.

This blog is about hosting a web app prototype on a single EC2 instance, using HTTPS via Let’s Encrypt, without a load balancer.

Using an AWS ALB costs a minimum of about $18 per month, on top of any other charges you currently have, such as $5 for the t2.micro instance that you may be running your prototype on.

So, if you’ve only got one EC2 instance in Elastic Beanstalk for your prototype, and don’t currently want the benefits of an ALB (fault tolerance, auto-scaling, etc), but do want the benefits of HTTPS (protection from interception, man-in-the-middle (MITM) attacks, etc), read on.

The Scenario

I’ve been working on an app prototype which is made up of a JS front-end, and a Python Flask backend, all running on Elastic Beanstalk, with Amazon Linux, and Apache server.

Elastic Beanstalk provides a URL which can be used to connect to the app, which looks something like like http://testapp456325.eyiee2b7ip.ap-southeast-2.elasticbeanstalk.com / .. this url is not HTTPS.

The configuration

Create an .ebextensions directory at the root of your project, and within the directory, create the following file called 00_apache_ssl.config

Within the file:

Replace EB_INSTANCE_DOMAIN_NAME with the domain name of your Elastic Beanstalk EC2 instance. i.e. testapp456325.eyiee2b7ip.ap-southeast-2.elasticbeanstalk.com

Replace YOUR_EMAIL_ADDRESS with your email address. i.e. [email protected]

Resources:
    sslSecurityGroupIngress:
        Type: AWS::EC2::SecurityGroupIngress
        Properties:
            GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}
            IpProtocol: tcp
            ToPort: 443
            FromPort: 443
            CidrIp: 0.0.0.0/0

files:
    /etc/httpd/conf.d/ssl.pre:
        mode: "000644"
        owner: root
        group: root
        content: |
            LoadModule ssl_module modules/mod_ssl.so
            Listen 443

            <VirtualHost *:443>
                <Directory /opt/python/current/app/build/static>
                    Order deny,allow
                    Allow from all
                </Directory>
                
                SSLEngine on
                SSLCertificateFile "/etc/letsencrypt/live/EB_INSTANCE_DOMAIN_NAME/fullchain.pem"
                SSLCertificateKeyFile "/etc/letsencrypt/live/EB_INSTANCE_DOMAIN_NAME/privkey.pem"
                SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
                SSLProtocol All -SSLv2 -SSLv3
                SSLHonorCipherOrder On
                SSLSessionTickets Off
                
                Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"
                Header always set X-Frame-Options DENY
                Header always set X-Content-Type-Options nosniff
                
                ProxyPass / http://localhost:80/ retry=0
                ProxyPassReverse / http://localhost:80/
                ProxyPreserveHost on
                RequestHeader set X-Forwarded-Proto "https" early
                # If you have pages that may take awhile to
                # respond, add a ProxyTimeout:
                # ProxyTimeout seconds
            </VirtualHost>
  
    /tmp/renew_cert_cron:
        mode: "000777"
        owner: root
        group: root
        content: |
            # renew Lets encrypt cert with certbot command
            0 1,13 * * * /tmp/certbot-auto renew

packages:
    yum:
        epel-release: []
        mod24_ssl : []

# Steps here
# 1. Install certbot
# 2. Get cert (stop apache before grabbing)
# 3. Link certs where Apache can grab
# 4. Get the Apache config in place
# 5. Move certbot-auto into tmp folder
container_commands:
    10_installcertbot:
        command: "wget https://dl.eff.org/certbot-auto;chmod a+x certbot-auto"
    20_getcert:
        command: "sudo ./certbot-auto certonly --debug --non-interactive --email YOUR_EMAIL_ADDRESS --agree-tos --debug --apache --domains EB_INSTANCE_DOMAIN_NAME --keep-until-expiring"
    30_link:
        command: "sudo ln -sf /etc/letsencrypt/live/EB_INSTANCE_DOMAIN_NAME /etc/letsencrypt/live/ebcert"
    40_config:
        command: "sudo mv /etc/httpd/conf.d/ssl.pre /etc/httpd/conf.d/ssl.conf"
    50_mv_certbot_to_temp_for_cron_renew:
        command: "sudo mv ./certbot-auto /tmp"
    60_create_cert_crontab:
        command: "sudo crontab /tmp/renew_cert_cron"
    70_delete_cronjob_file:
        command: "sudo  rm /tmp/renew_cert_cron"

Deploy to Elastic Beanstalk

Now you should be all good to deploy. If the configuration change worked correctly, it should now be possible to connect to your Elastic Beanstalk via HTTPS.

Keep in mind that the changes made by container_commands in 00_apache_ssl.config will remain, even if you roll back your application code.

If you want to roll back any changes made by container_commands, you’ll need to rebuild the Elastic Beanstalk instance.

Troubleshooting

If you get errors in deployment, you’ll need to log into the Elastic Beanstalk instance and start troubleshooting to figure out what’s causing the error.

To start the process, log into the Elastic Beanstalk instance via eb ssh

Run each of the commands in container_commands individually, to find out if any of the container commands have triggered the error.

Finally, run apachectl configtest to see if it’s the Apache config that’s causing the problem.

Please let me know in the comments below if this blog post needs to be updated.

The certbot –debug flag is currently required on Amazon Linux

If you remove the --debug flag, you might see this error in eb-commandprocessor.log or eb-activity.log:

Amazon Linux support is very experimental at present...
if you would like to work on improving it, please ensure you have backups
and then run this script again with the --debug flag!
Alternatively, you can install OS dependencies yourself and run this script
again with --no-bootstrap.

(ElasticBeanstalk::ExternalInvocationError)

At the time of writing, the --debug flag is required.

YAML can be error-prone

YAML can be hard to edit, which means that it’s easy to get a parse error in 00_apache_ssl.config

Here’s the kind of error you’ll get if the indentation is wrong in 00_apache_ssl.config

The configuration file .ebextensions/00_apache_ssl.config in application version python-v4 contains invalid YAML or JSON. YAML exception: Invalid Yaml: while parsing a block mapping in "<reader>", line 53, column 5: yum: ^ expected <block end>, but found BlockMappingStart in "<reader>", line 55, column 9: mod24_ssl : [] ^ , JSON exception: Invalid JSON: Unexpected character (r) at position 0.. Update the configuration file.

Capitalisation of ‘Resources’ in .ebextensions is important

'.ebextensions/00_apache_ssl.config' - Contains invalid key: 'resources'. For information about valid keys, see http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/ebextensions.html

Make sure you use a capital ‘R’ in Resources

CN was longer than 64 bytes

There’s a limit to how long your domain name can be. For example, while running the certbot command, you may get an error such as:

Error: urn:ietf:params:acme:error:malformed :: The request message was malformed :: Error finalizing order :: issuing precertificate: CN was longer than 64 bytes

i.e your domain name is TestApp456324-env-2.eyiee2b7ip.ap-southeast-2.elasticbeanstalk.com which is 66 characters.

Changing the domain so that it’s i.e. TestApp456324-env.eyiee2b7ip.ap-southeast-2.elasticbeanstalk.com is 64 characters, and works as expected.

Order not allowed in ssl.conf

AH00526: Syntax error on line 4 of /etc/httpd/conf.d/ssl.conf:
order not allowed here

To fix this, I wrapped Order and Allow within a Directory tag, and the SSL config within a VirtualHost tag.

fullchain.pem does not exist or is empty

SSLCertificateFile: file '/etc/letsencrypt/live/TestApp456325.eyiee2b7ip.ap-southeast-2.elasticbeanstalk.com/fullchain.pem' does not exist or is empty

Make sure you use a lowercase URL. i.e. TestApp456325.eyiee2b7ip.ap-southeast-2.elasticbeanstalk.com must be testApp456325.eyiee2b7ip.ap-southeast-2.elasticbeanstalk.com

Credit to Spencer Jones

This blog post was based off of a 2016 post by Spencer Jones: Free, Automated SSL with a Single AWS EC2 Instance

I found his post while trying to get HTTPS set up on my single instance Elastic Beanstalk setup. I had to make a few tweaks to get it work.

Specifically:

  • Fixed up the indentation. The file wasn’t quite valid YAML.
  • Fixed an “Order not allowed in ssl.conf” error. See troubleshooting steps above.
  • On the certbot command, changing the –standalone flag to –apache due to an error when using --standalone while Apache was running.

Thanks for reading!

I hope you found this post useful. Do you have any questions? Are there any follow-up blog posts you’d like me to write relating to this? Feel free to leave any feedback in the comments below.