Enable HTTPS and HTTP-Redirect on AWS Elastic Beanstalk


AWS Elastic Beanstalk is powerful deployment tools on AWS. It allows users to create applications and push them to a definable set of AWS services, including Amazon EC2, Amazon RDS, Amazon Simple Notification Service (SNS), Amazon CloudWatch, Auto Scaling Group, and Elastic Load Balancer (ELB). The problem is: Elastic Beanstalk Web Console is not so powerful. But, it can be extended using script (.ebextensions) and CLI.

Problem: HTTP and HTTPS Configuration on Elastic Beanstalk (Web Console)

AWS Elastic Beanstalk Configuration: Load Balancer

From the Elastic Beanstalk Web Console, you can configure a web application to listen HTTP and HTTPS port using Elastic Load Balancer (ELB). But, the ELB will forward/proxy the request into a single HTTP port. It means that HTTP and HTTPS will serve same response from user point-of-view. HTTPS connection is terminated (HTTPS-termination) in ELB.

Some users (at least me and some users in stackoverflow.com/serverfault.com) want:

  • HTTP request is replied with HTTP redirection (3xx status code) to HTTPS.
  • HTTPS request is replied by actual web app.
  • No HTTPS-termination in ELB.

Solution: Configure Elastic Load Balancer using .ebextentions

Some of Elastic Beanstalk resources can be customized using .ebextenstions script (see: Customize Containers and Environment Resources). Now, we will configure the ELB to proxy HTTP and HTTPS request to different EC2 instance’s ports.

  1. Create .ebextensions directory inside your app root path.
  2. Create a file (e.g: 00-load-balancer.config) inside .ebextensions directory.
  3. Write the following configuration into the file (.ebextensions/00-load-balancer.config).
{
  "Resources": {
    "AWSEBSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "Allow HTTP and HTTPS",
        "SecurityGroupIngress": [
          {
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIp": "0.0.0.0/0"
          },
          {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIp": "0.0.0.0/0"
          }
        ]
      }
    },
    "AWSEBLoadBalancerSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "Allow HTTP and HTTPS",
        "SecurityGroupIngress": [
          {
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIp": "0.0.0.0/0"
          },
          {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIp": "0.0.0.0/0"
          }
        ],
        "SecurityGroupEgress": [
          {
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIp": "0.0.0.0/0"
          },
          {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIp": "0.0.0.0/0"
          }
        ]
      }
    },
    "AWSEBLoadBalancer": {
      "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
      "Properties": {
        "HealthCheck": {
          "HealthyThreshold": "3",
          "Interval": "30",
          "Target": "HTTP:80/status.html",
          "Timeout": "5",
          "UnhealthyThreshold": "5"
        },
        "Listeners": [
          {
            "LoadBalancerPort": 80,
            "Protocol": "HTTP",
            "InstancePort": 80,
            "InstanceProtocol": "HTTP"
          },
          {
            "LoadBalancerPort": 443,
            "Protocol": "HTTPS",
            "InstancePort": 443,
            "InstanceProtocol": "HTTPS",
            "SSLCertificateId": "arn:aws:iam::123456789012:server-certificate/YourSSLCertificate"
          }
        ],
        "SecurityGroups": [
          { "Fn::GetAtt": [ "AWSEBLoadBalancerSecurityGroup", "GroupId" ] }
        ]
      }
    }
  }
}

In the above config, we modified 3 resources:

  • EC2 Instance Security Group, allow to listen on port HTTP (80) and HTTPS (443).
  • ELB Security Group, allow to listen on port HTTP (80) and HTTPS (443).
  • ELB, we modified ELB to:
    • Do health check EC2 instances on port 80 by HTTP request to /status.html. So, we need to create a hole in port HTTP to allow access the page (will be described later). Elastic Beanstalk doesn’t allow us to do health check using HTTPS request. If you want to do health check by checking TCP port 80, just remove this config section.
    • Make ELB listen to port 80 and forward it to EC2 instance’s port 80.
    • Make ELB listen to port 443 and forward it to EC2 instance’s port 443.

Now the ELB configuration is ready. But, we need to configure web server inside EC2 instances.

Elastic Beanstalk provides some different type of environment (e.g: Java, Python, Ruby, Docker, etc.). Each environment might have different configuration. You can check it on Supported Platforms. At the time of writing this post, they use some web proxy/server (i.e. Apache 2.2, Apache 2.4, Nginx 1.6.2 and IIS 8.5) to listen at port 80 (HTTP).

In this post, I only tell you how to configure Single Docker Container Elastic Beanstalk, which is using Nginx 1.6.2 as proxy. Basically, Single Docker Container Elastic Beanstalk use Nginx to proxy the request to a Docker container. Each time you deploy a new update, Elastic Beanstalk agent inside EC2 instance will update Docker upstream in /etc/nginx/conf.d/elasticbeanstalk-nginx-docker-upstream.conf. Another environment can be configured slightly same.

  1. Create a file (e.g: 01-nginx-proxy.config) inside .ebextensions directory.
  2. Write the following configuration into the file (.ebextensions/01-nginx-proxy.config). Don’t forget to adjust some config (e.g: domain name, SSL certificate, etc.).
files:
  "/etc/nginx/sites-available/000-default.conf":
    mode: "000644"
    owner: root
    group: root
    content: |
      map $http_upgrade $connection_upgrade {
        default   "upgrade";
        ""        "";
      }
 
      server {
        listen         80;
        server_name    your-domain.com;
 
        location = /status.html {
          proxy_pass          http://docker;
          proxy_http_version  1.1;
 
          proxy_set_header    Connection          $connection_upgrade;
          proxy_set_header    Upgrade             $http_upgrade;
          proxy_set_header    Host                $host;
          proxy_set_header    X-Real-IP           $remote_addr;
          proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
          proxy_set_header    X-Forwarded-Host    $host;
          proxy_set_header    X-Forwarded-Server  $host;
        }
 
        location / {
          return        301 https://$host$request_uri;
        }
      }
 
      server {
        listen 443;
 
        ssl                  on;
        ssl_session_timeout  5m;
        ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;
        ssl_certificate      /opt/ssl/default-ssl.crt;
        ssl_certificate_key  /opt/ssl/default-ssl.pem;
        ssl_session_cache    shared:SSL:10m;
 
        location / {
          proxy_pass          http://docker;
          proxy_http_version  1.1;
 
          proxy_set_header    Connection          $connection_upgrade;
          proxy_set_header    Upgrade             $http_upgrade;
          proxy_set_header    Host                $host;
          proxy_set_header    X-Real-IP           $remote_addr;
          proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
          proxy_set_header    X-Forwarded-Host    $host;
          proxy_set_header    X-Forwarded-Server  $host;
        }
      }
 
  "/opt/ssl/default-ssl.crt":
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN CERTIFICATE-----
      *
      * YOUR-CHAINED-SSL-CERTIFICATE-HERE
      *
      -----END CERTIFICATE-----
 
 
  "/opt/ssl/default-ssl.pem":
    mode: "000400"
    owner: root
    group: root
    content: |
      -----BEGIN RSA PRIVATE KEY-----
      *
      * YOUR-SSL-PRIVATE-KEY-HERE
      *
      -----END RSA PRIVATE KEY-----
 
commands:
   00_enable_site:
    command: 'rm -f /etc/nginx/sites-enabled/* && ln -s /etc/nginx/sites-available/000-default.conf /etc/nginx/sites-enabled/000-default.conf'

In the above config, we:

  • Create SSL certificate and key file.
  • Create Nginx site config:
    • Listen port 80 (HTTP) and redirect all request to HTTPS, except for /status.html. We create a hole here to allow load balancer do health check.
    • Listen port 443 (HTTPS) and proxy the request to actual web server (in this case, Docker container upstream, http://docker).
  • Remove all enabled-sites config and create symlink for the new Nginx config.

After that, you can zip your app directory and deploy it to Elastic Beanstalk via Web Console or CLI.

Advertisements

12 thoughts on “Enable HTTPS and HTTP-Redirect on AWS Elastic Beanstalk

  1. Hey, I’m using AWS Elastic beanstalk and my reqirement is same. But I’m using tomcat which is proxied by httpd. my elasticbeanstalk.conf file (/etc/httpd/conf.d/elastickbeanstalk.conf) looks like this:

    Order deny,allow
    Allow from all

    ProxyPass / http://localhost:8080/ retry=0
    ProxyPassReverse / http://localhost:8080/
    ProxyPreserveHost on

    ErrorLog /var/log/httpd/elasticbeanstalk-error_log

    I have enabled https on elastickbeanstalk from console and it’s working (ie. when i try to reach the site using https, it works) but when I try to reach with http it’s not redirecting me to https.
    I tried following:

    Order deny,allow
    Allow from all

    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteCond %{REQUEST_URI} ^/health
    RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]

    ProxyPass / http://localhost:8080/ retry=0
    ProxyPassReverse / http://localhost:8080/
    ProxyPreserveHost on

    ErrorLog /var/log/httpd/elasticbeanstalk-error_log

    But it didn’t work… can you help me with this? thanks.

    Like

    1. Try to remove:

      RewriteCond %{REQUEST_URI} ^/health

      from your Apache config. It makes RewriteRule is only applied to request which started with /health.

      Or, you can make it:

      RewriteCond %{REQUEST_URI} !^/health

      This one is to apply RewriteRule to all request except started with /health.

      Like

  2. I think it is a bad idea to store ssl certificates in config files like .ebextensions/01-nginx-proxy.config (it is potential security breach if your repo will be compromised).
    There are other ways to set up this stack.

    Like

    1. Hi Dmytro, thanks for your security consideration.

      Usually, I put the SSL certs into a more secure storage, like S3 bucket with tight security for this requirement.

      But, if we don’t need to encrypt the connection between ELB and EC2 instances, we can just put the certificate in ELB, no need to copy those certs into EC2.

      What do you think?

      Like

  3. Thank you for the trick! Does it works on a single instance without the load balancer in front of it ?
    Thanks in advance

    Like

  4. Hey, thanks a lot. I have a question. I am using AWS Certificate Manager to generate my SSL certificate. How do I use it to edit the SSL/TLS configurations that you have in your .ebextensions/01-nginx-proxy.config file?

    Like

  5. Thank you very much for this! Very smart and well explained.

    Could you tell me what to modify in these scripts in order to have the HTTPS termination in ELB?

    Thanks

    Like

  6. I got it working by replacing http://docker by http://docker/ otherwise I had 404 errors.

    I don’t underdstand why you added the line server_name. Can’t we remove it?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s