In a previous post, I covered how to enable FIPS 140-2 mode on hosts and within containers. Organizations building applications for FedRAMP or DoD CC SRG compliance need to enable end to end encryption of data in transit. In a containerized environment, this can be difficult. Most containerized applications terminate TLS at the cloud load balancer, such as an Application Load Balancer. This post will walk through terminating the TLS connection within the Pod or Task using NGINX.

SC-13 - “… if cryptography is required based on the selection of other security controls, organizations define each type of cryptographic use and the type of cryptography required (e.g., protection of classified information: NSA-approved cryptography; provision of digital signatures: FIPS-validated cryptography).”

First, you will need a FIPS 140-2 enabled host operating system. See my previous post about how to do this on Amazon Linux 2.

Second, you will need to verify that it is in FIPS 140-2 mode:

cat /proc/sys/crypto/fips_enabled # should be 1
sysctl crypto.fips_enabled # should be 1
openssl version # similar to OpenSSL 1.0.2k-fips  26 Jan 2017
openssl md5 /dev/null # should output an error that includes "disabled for fips"

Next, you will need to build an NGINX container with the FIPS 140-2 packages installed. In this example, I am using CentOS 7 and the official NGINX repository configured:

FROM centos:7

RUN yum update -y \
    && yum install -y dracut-fips openssl \
    && echo "[nginx]" > /etc/yum.repos.d/nginx.repo \
    && echo "name=nginx repo" >> /etc/yum.repos.d/nginx.repo \
    && echo "baseurl=https://nginx.org/packages/centos/\$releasever/\$basearch/" >> /etc/yum.repos.d/nginx.repo \
    && echo "gpgcheck=0" >> /etc/yum.repos.d/nginx.repo \
    && echo "enabled=1" >> /etc/yum.repos.d/nginx.repo \
    && yum update -y \
    && yum install -y nginx \
    && ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log

EXPOSE 80 443

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]

After we have the file created, we will build the container:

docker image build -t nginx-fips:latest .

Next, we will validate that our container works properly. Let’s start by creating a server.conf file to test our configuration. Note we will mount this into the container later.

server {
    listen 443 ssl;

    ssl_certificate     /etc/nginx/ssl/test.crt;
    ssl_certificate_key /etc/nginx/ssl/test.key;

    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

Now, lets generate some self-signed certificates for testing.

# create a directory to store our certs
mkdir certs

# generate the certs using OpenSSL in FIPS mode
docker container run -it -v $(pwd)/certs:/etc/nginx/ssl nginx-fips:latest \
  openssl req -newkey rsa:2048 -nodes -keyout /etc/nginx/ssl/test.key -x509 -days 365 -out /etc/nginx/ssl/test.crt

Next, let’s run the container in the background on our FIPS 140-2 enabled host.

docker container run -it -d \
  -p 8080:80 \
  -p 8443:443 \
  -v $(pwd)/certs:/etc/nginx/ssl \
  -v $(pwd)/server.conf:/etc/nginx/conf.d/default.conf \
  nginx-fips:latest

Finally, let’s test if the container is properly handling FIPS 140-2. From host operating system, run the follow commands:


# test if the container is accepting connections
(echo "GET /" ; sleep 1) | openssl s_client -connect localhost:8443
# this command should work and output the default nginx homepage

# ensure RC4-MD5 is disabled
(echo "GET /" ; sleep 1) | openssl s_client -connect localhost:8443 -cipher RC4-MD5
# expected output:
# Error with command: "-cipher RC4-MD5"
# 140202641934240:error:1410D0B9:SSL routines:SSL_CTX_set_cipher_list:no cipher match:ssl_lib.c:1383:

# ensure CAMELLIA256-SHA is disabled
(echo "GET /" ; sleep 1) | openssl s_client -connect localhost:8443 -cipher CAMELLIA256-SHA
# expected output:
# Error with command: "-cipher CAMELLIA256-SHA"
# 140211194984352:error:1410D0B9:SSL routines:SSL_CTX_set_cipher_list:no cipher match:ssl_lib.c:1383:

# ensure AES256-SHA is enabled
(echo "GET /" ; sleep 1) | openssl s_client -connect localhost:8443 -cipher AES256-SHA
# this command should work and output the default nginx homepage

That’s it! We now have an NGINX container terminating TLS with FIPS 140-2 compliance. To use this container in Kubernetes or Amazon ECS, you will need to run this container next to your application container within the Pod or Task. Then proxy connections over localhost to the application. For example, if you are running a Python application, you would run this NGINX container and expose port 443 to the load balancer. Then the NGINX container would terminate the TLS connection and proxy traffic over localhost (within the same host namespace) to your Python web application. This example assumes you are running a Layer 4 Load Balancer, such as Network Load Balancer from AWS in front of your containers.

Using this architecture, all TLS connections will be encrypted end to end over the wire with TLS terminating in the Pod or Task. This is analagous to running an NGINX service and Python service on the same host and using NGINX to accept web traffic.

I recommend loading TLS certificates into your container using AWS Secrets Manager or AWS Systems Manager Parameter Store with KMS at runtime. This will ensure your SSL certificates are encrypted at rest and mounted into the container securely only when they are needed. These should be mounted to memory backed volumes, so that if the host is restarted the certificates are not persisted on disk.