Form.io/AWS Elastic Beanstalk End-To-End Encrypted Deployment

This tutorial will walk through how to create an example Form.io deployment environment on AWS Elastic Beanstalk that enables TLS/SSL-encrypted communication to the load balancer, from the load balancer to the EC2 instances, and lastly between each container within each instance. This tutorial assumes you have prior knowledge of Form.io and AWS Elastic Beanstalk, that you have an already running Form.io deployment in Elastic Beanstalk, and that your Elastic Beanstalk load balancer has an attached HTTPS listener. Please see AWS Deployment for more details.

"This deployment setup is seldom required and is intended for advanced Form.io configurations, particularly for customers with stringent external security requirements. If you're unsure whether your deployment needs TLS/SSL-encrypted communication between containers, please reach out to support@form.io.

Load Balancer Configuration

First, we'll need to ensure that the HTTPS listener on our load balancer will re-encrypt traffic going to the instances by adding an environment configuration file. Please see the Elastic Beanstalk documentation for more details.

.ebextensions/https-reencrypt-alb.config

option_settings:
  aws:elbv2:listener:443:
    DefaultProcess: https
    ListenerEnabled: 'true'
    Protocol: HTTPS
  aws:elasticbeanstalk:environment:process:https:
    Port: '443'
    Protocol: HTTPS

Second, we'll need to modify our load balancer's security group to allow traffic. This will depend on whether you adopted a default security group or created a custom one during your Elastic Beanstalk environment creation; to cover both scenarios, use the following configuration file to create a security group and attach it to the load balancer.

.ebextensions/https-lbsecuritygroup.config

option_settings:
  # Use the custom security group for the load balancer
  aws:elb:loadbalancer:
    SecurityGroups: '`{ "Ref" : "loadbalancersg" }`'
    ManagedSecurityGroup: '`{ "Ref" : "loadbalancersg" }`'

Resources:
  loadbalancersg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: load balancer security group
      VpcId: vpc-########
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

Be sure to replace ######## with your vpc identification number

Next, we'll add ingress and egress rules that allow communication over port 443 between the load balancer's security group and the instances' security group.

.ebextensions/https-backendsecurity.config

Resources:
  # Add 443-inbound to instance security group (AWSEBSecurityGroup)
  httpsFromLoadBalancerSG: 
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}
      IpProtocol: tcp
      ToPort: 443
      FromPort: 443
      SourceSecurityGroupId: {"Fn::GetAtt" : ["loadbalancersg", "GroupId"]}
  # Add 443-outbound to load balancer security group (loadbalancersg)
  httpsToBackendInstances: 
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: {"Fn::GetAtt" : ["loadbalancersg", "GroupId"]}
      IpProtocol: tcp
      ToPort: 443
      FromPort: 443
      DestinationSecurityGroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}

Now that we've ensured TLS/SSL-encrypted traffic from the load balancer to our instances, we'll move on to configuring our instances to be able to handle HTTPS traffic.

Instance Configuration

In a typical enterprise deployment of the Form.io platform, each server instance behind the load balancer will contain the Enterprise Server Docker container, the PDF Server Docker container, and a reverse proxy (commonly nginx running in its own Docker container) to direct traffic that arrives at the instance to one or both services.

docker-compose.yml

version: "3.8"
services:
  api-server:
    image: formio/formio-enterprise:9.0.0
    mem_limit: 2048m
    restart: always
    links:
      - pdf-server
    volumes:
      - "./certs:/src/certs:ro"
    environment:
      MONGO_CA: /src/certs/rds-combined-ca-bundle.pem
      PORTAL_ENABLED: 1
      PORT: 3000
    ports:
      - "3000:3000"
    env_file:
      - .api.env
  pdf-server:
    image: formio/uswds-viewer:4.0.0-rc.1
    restart: always
    mem_limit: 2048m
    volumes:
      - "./certs:/src/certs:ro"
    environment:
      MONGO_CA: /src/certs/rds-combined-ca-bundle.pem
      FORMIO_PDF_PORT: 4005
    ports:
      - "4005:4005"
    env_file:
      - .pdf.env
  nginx-proxy:
    image: nginx
    restart: always
    mem_limit: 128m
    ports:
      - "443:443"
    volumes:
      - "./certs:/src/certs:ro"
      - "./conf.d:/etc/nginx/conf.d:ro"
    links:
      - api-server
      - pdf-server

Since our load balancer will be sending HTTPS traffic, we'll first need to ensure that our nginx reverse proxy can process HTTPS traffic by updating our conf.d file.

conf.d/default.conf

client_header_timeout   300;
client_body_timeout     300;
send_timeout            300;
proxy_connect_timeout   300;
proxy_read_timeout      300;
proxy_send_timeout      300;
server {
  listen 443 ssl;
  ssl_certificate      /src/certs/mydomain.com.pem;
  ssl_certificate_key  /src/certs/mydomain.com-key.pem;
  client_max_body_size 20M;
  client_body_buffer_size 20M;
  location / {
    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-Proto $scheme;
    proxy_read_timeout  300;
    proxy_pass          https://api-server:3000;
    proxy_redirect      https://api-server:3000 https://$host;
  }
  location /pdf/ {
    rewrite ^/pdf/(.*)$ /$1 break;
    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-Proto $scheme;
    proxy_read_timeout  300;
    proxy_pass          https://pdf-server:4005;
    proxy_redirect      https://pdf-server:4005 https://$host;
  }
}

You can find more details about this reverse proxy configuration in our deployment guide, but generally, this reverse proxy will be able to receive HTTPS traffic and direct it, encrypted, to our containers at HTTPS addresses (in this case provided by the Docker network, e.g. https://api-server:3000 or https://pdf-server:4005).

Now that our instances can receive HTTPS traffic and direct it to our containers, the final step will be to configure our Form.io applications to be able to receive this traffic.

Container/NodeJS Configuration

Since the Form.io Enterprise Server and the Form.io PDF Server are NodeJS applications, we'll need to configure them to be able to process TLS/SSL-encrypted traffic. For this, we'll add the SSL_ENABLED, SSL_KEY, and SSL_CERT variables to each environment configuration (you of course can accomplish this via an environment file or via the Elastic Beanstalk configuration console).

.env

ENABLE_SSL=true
SSL_CERT="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
SSL_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"

Pay special attention to these variables, as they should contain the content of the key and the certificate and not their path in the filesystem. Additionally, they should be a single line escaped with \n characters.

Keep in mind that NodeJS maintains its own trusted root certificate store. If you're using certificates with a private CA (or using a tool like mkcert) you'll need to add your root certificate to NodeJS by using the NODE_EXTRA_CA_CERTS environment variable (e.g. NODE_EXTRA_CA_CERTS=/src/certs/rootCA.pem). We don't recommend self-signed certificates for this configuration.

With these variables in your environment, NodeJS will be able to accept TLS/SSL-encrypted traffic, and your deployment is encrypted end-to-end!

Last updated