Monday, December 28, 2020

How to deploy a single instance SSL-enabled Tomcat application on AWS Elastic Beanstalk

The Problem

I run a simple low-traffic website hosted on Elastic Beanstalk and I did not want to pay for the $15-$20 / month for an auto-scaled environment, which requires an elastic load balancer -- hence the higher cost. Instead, I can run a single EC2 instance and cut my costs in half from ~$30 to ~$15. 

The catch is that by dropping the elastic load balancer, you also lose the ability to terminate the SSL (HTTPS) connection and the ability to use Amazon's free SSL certificate service. So what do you do? 


The Solution

Summary

We can extend our Elastic Beanstalk application hosting using the EB Extensions features. The following changes will tell Elastic Beanstalk to:

1. get a free SSL certificate from Letsencrypt.org using one of their tools (certbot-auto) which is downloaded and executed after the instance is set up, but before Apache HTTP server is started. The SSL certificates will be stored in /etc/letsencrypt/live folder. The SSL certificate will be valid for 3 months, but you can renew it as often as you need (see step 4). 

2. Configure Apache to use those SSL certificates and start listening on port 443 for SSL connections. 

3. Updated the security group for the EC2 instance is updated to allow traffic from the internet to port 443. 

4. Schedule a cron job to run every month to renew the certificate


Step 1. Create .ebextensions Folder 

By including a new folder in the top level of the WAR file called .ebextensions with the following files:




If you are using maven, you need to add the following bit to your pom.xml to get the .ebextension directory to be bundled in the right place in the war:


Step 2. Configure Maven war plugin


<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-war-plugin</artifactId>

<version>3.3.1</version>

<configuration>

<webResources>

<resource>

<directory>src/main/ebextensions</directory>

<targetPath>.ebextensions</targetPath>

<filtering>true</filtering>

</resource>

</webResources>

</configuration>

</plugin>


Step 3. Add ssl.conf File

This file will update the Apache configuration to look at the SSL certificates generated by letsencrypt.org. Update the domain (in red) for your domain name. 

Listen 443

<VirtualHost *:443> 

  ServerName mydomain.com

  SSLEngine on 

  SSLCertificateFile    "/etc/letsencrypt/live/www.mydomain.com/fullchain.pem"

  SSLCertificateKeyFile "/etc/letsencrypt/live/www.mydomain.com/privkey.pem"


  <Proxy *> 

    Require all granted 

  </Proxy> 

  ProxyPass / http://localhost:8080/ retry=0 

  ProxyPassReverse / http://localhost:8080/ 

  ProxyPreserveHost on 


  ErrorLog /var/log/httpd/elasticbeanstalk-ssl-error_log 


</VirtualHost>



Step 4. Add http-instance-single.config File

This file will update the security group resource to allow traffic in port 443 (HTTPS Port) and it will also configure the necessary command to run to get the SSL certificates from letsencrypt.org. Make sure you update the configuration in red for your own email and domain name. 


Resources:

  sslSecurityGroupIngress: 

    Type: AWS::EC2::SecurityGroupIngress

    Properties:

      GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}

      IpProtocol: tcp

      ToPort: 443

      FromPort: 443

      CidrIp: 0.0.0.0/0

      

commands:

  00_getSSLcertificate: 

    command: | 

      sudo service httpd stop

      mkdir -p /opt/certbot

      wget https://dl.eff.org/certbot-auto -O /opt/certbot/certbot-auto;chmod a+x /opt/certbot/certbot-auto

      sudo mkdir -p /var/www/challenge

      sudo /opt/certbot/certbot-auto certonly --debug --non-interactive --email myEmail@gmail.com --agree-tos --standalone --domains www.mydomain.com --keep-until-expiring -w /var/www/challenge

  10_renewSSLCertificate:

    command: '(crontab -l ; echo ''0 6 * * * root /opt/certbot/certbot-auto renew --standalone --pre-hook "service httpd stop" --post-hook "service httpd start" --force-renew'') | crontab -'

    



Troubleshooting

1. Make sure your domain name is pointing to the Elastic beanstalk environment that you are changing with this single instance. If you don't want to experiment on your live website, you can create a test subdomain like (testssl.mydomain.com) and use that to experiment on until you know you have the right configuration in place to update your real website. 

2. View the logs in AWS Console or SSH into the instance and view the logs in /var/log/eb-activity.log
tail -f /var/log/eb-activity.log
That log should contain the errors for both letsencrypt as well as anything specific to apache. 

3. Execute the commands on your own. If the automated solution does not work, you can SSH insto the instance and execute the command on your own to see what may not work. If you find that you need to add a new command, add it to the commands block in the http-instance-single.config file. Then build and deploy your application again. 

4. In order for letsencrypt utility to work, it needs to start its own HTTP web server to validate that you own the domain for which it is issuing a certificate. To do this certbot-auto will need to bind itself to port 80 and it cannot do this if something else is running on that port. In this case you will see an error like "Cannot bind to port 80 ...". The solution is to stop any services that run on that port. Hence, the first command above is sudo service httpd stop which stops the Apache http service. If you have another type of service (e.g. nginx) on your Elastic Beanstalk configuration, you need to make sure that service is stopped before certbot-auto is executed.