Web applications are often deployed behind a load balancer or application firewall. This component provides secure encrypted sessions from the internet to the application.

It accomplishes this by listening for requests for the web application on ports exposed to the external environment and forwarding that traffic onto the application’s listening port. This forwarded traffic is often unencrypted.

However, in shared network environments such as Spinup, we highly recommend that traffic from the load balancer to the instance hosting the application be encrypted for conventional Spinup and require it for Secure Spinup.

Implementing encrypted session traffic to a web application hosted on a Spinup Linux instance is accomplished by installing Nginx as proxy on the instance hosting the web application.

This example has been validated on a CentOs 7 instance but is applicable to similar rpm-based distributions.

The procedure involves the following steps:

  1. Generating a self-signed SSL certificate

  2. Installing Nginx

  3. Configuring Nginx as a reverse-proxy using the self-signed SSL certificate

  4. Example configurations

  5. Configuring Nginx to start automatically as a service

  6. Configuring Firewalls

Generating a Self-Signed SSL Certificate

In order to encrypt the connection between the internet-facing load balancer and the instance running your web application, we will need to create and install an SSL certificate.

Since only the load balancer will be connecting to the Nginx proxy, a self-signed certificate is sufficient.

Cut and paste the following shell commands into your remote shell session:

export CERT_KEY='/etc/pki/tls/private/domain.key' # Default CentOS certificate key
export CERT='/etc/pki/tls/certs/domain.crt' # Default CentOS certificate path

cert_expire=${CERT_EXPIRE:-3650}
cert_size=${CERT_SIZE:-rsa:2048}
cert_subject=${CERT_SUBJECT:-/CN=localhost}

sudo openssl req -x509 -nodes \
  -days "${cert_expire}" \
  -newkey "${cert_size}" \
  -keyout "${CERT_KEY}" \
  -out "${CERT}" \
  -subj "${cert_subject}"

(Different distributions, use different locations for the key and the certificate. For example, Ubuntu uses the following:

CERT_KEY='/etc/ssl/private' # Default CentOS certificate key
CERT='/etc/ssl/certs' # Default CentOS certificate path

)

Installing Nginx

Cut and paste the following shell commands into your remote shell session:

# CentOS
sudo yum -y update
sudo yum install -y epel-release
sudo yum install -y nginx

# Amazon Linux 2
# sudo amazon-linux-extras install nginx1.12

# Ubuntu
# sudo apt update
# sudo apt install nginx

Configuring Nginx as Reverse-Proxy

Configuration of Nginx is modular. Global configuration is located in /etc/nginx/nginx.conf. One could create server blocks in this file. However, a better practice is to separate server definitions in separate configuration files that are sourced into the main configuration when it starts.

Separate [server].conf files can be created under the /etc/nginx/conf.d/ directory which are loaded at start. (Debian and Ubuntu take this even further. These distributions place site configurations in /etc/nginx/sites-available/. To enable a specific site, a symlink to the specific /etc/nginx/sites-available/[server].conf is created in /etc/nginx/sites-enabled/.)

Cut and paste the following

export CERT_KEY='/etc/pki/tls/private/domain.key' # Default CentOS certificate key
export CERT='/etc/pki/tls/certs/domain.crt' # Default CentOS certificate path

export SERVER_FQDN="{{ .serverFqdn }}"
export BACKEND_PORT="{{ .backEndPort }}" # change {{}} to the port the webapp is listening on.

sudo tee /etc/nginx/conf.d/reverse-proxy-tls.conf<<-EOF
upstream backend {
  server localhost:${BACKEND_PORT};
}

server {
  listen 80;
  listen [::]:80;
  server_name ${SERVER_FQDN};

  # redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
  return 301 https://${SERVER_FQDN}\$request_uri;
}

server {
  listen 443;
  server_name ${SERVER_FQDN};

  ssl   on;
  ssl_session_cache   shared:SSL:40m;
  ssl_session_timeout  4h;
  ssl_protocols  TLSv1.2;
  ssl_ciphers ECDH+AESGCM:ECDH+AES256-CBC:ECDH+AES128-CBC:DH+3DES:!ADH:!AECDH:!MD5;
  ssl_prefer_server_ciphers   on;

  ssl_certificate         ${CERT};
  ssl_certificate_key     ${CERT_KEY};

  access_log /var/log/nginx/${SERVER_FQDN//./_}.log;
  error_log /var/log/nginx/${SERVER_FQDN//./_}-error.log error;

  location / {
    proxy_pass http://backend;
    proxy_buffers 16 4k;
    proxy_buffer_size 2k;
    proxy_set_header Host \$http_host;
    proxy_set_header ServerName \$server_name;
    proxy_set_header ServerPort 443;
    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 https;
    proxy_set_header X-Forwarded-Scheme https;
    proxy_set_header X-Forwarded-URL-Scheme https;
    proxy_redirect default;
  }
}
EOF

Example Configurations

Example: uWSGI

server {

    # .... Omitted

    location / {
        include uwsgi_params;
        uwsgi_pass unix:///home/klo9/webapp/webapp.sock;
    }
}

Example: R Shiny

export CERT_KEY='/etc/pki/tls/private/domain.key' # Default CentOS certificate key
export CERT='/etc/pki/tls/certs/domain.crt' # Default CentOS certificate path

export SERVER_FQDN="{{ .serverFqdn }}"
export BACKEND_PORT=3838

sudo tee "/etc/nginx/conf.d/${SERVER_FQDN//./_}.conf"<<-EOF
upstream backend {
  server localhost:${BACKEND_PORT};
}

map \$http_upgrade \$connection_upgrade {
  default upgrade;
  '' close;
}

server {
  listen 80;
  listen [::]:80;
  server_name ${SERVER_FQDN};

  # redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
  return 301 https://${SERVER_FQDN}\$request_uri;
}

server {
  listen 443;
  server_name ${SERVER_FQDN};

  ssl   on;
  ssl_session_cache   shared:SSL:40m;
  ssl_session_timeout  4h;
  ssl_protocols  TLSv1.2;
  ssl_ciphers ECDH+AESGCM:ECDH+AES256-CBC:ECDH+AES128-CBC:DH+3DES:!ADH:!AECDH:!MD5;
  ssl_prefer_server_ciphers   on;

  ssl_certificate         ${CERT};
  ssl_certificate_key     ${CERT_KEY};

  access_log /var/log/nginx/${SERVER_FQDN//./_}.log;
  error_log /var/log/nginx/${SERVER_FQDN//./_}-error.log error;

  location / {
    proxy_pass          http://backend;
    proxy_read_timeout  20d;
    proxy_buffering off;
    proxy_http_version 1.1;
    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_set_header Upgrade \$http_upgrade;
    proxy_set_header Connection \$connection_upgrade;
  }
}
EOF

Enable Nginx at Startup

sudo systemctl enable nginx.service
sudo systemctl start nginx.service
note

Note for SELinux

On CentOS 7, in order to use Nginx as a proxy service (as we are doing in these examples), it must be allowed access to the network:

sudo setsebool -P httpd_can_network_connect 1

Note for SELinux

On CentOS 7, in order to use Nginx as a proxy service (as we are doing in these examples), it must be allowed access to the network:

sudo setsebool -P httpd_can_network_connect 1

Firewalls

In order to prevent direct access to the web application and force traffic to the TLS listener running in Nginx, you must enable the Space’s firewall for port 443: How do I use the Firewall in Spinup Spaces?

Additionally, for moderate and low risk CIS OS images, the host based iptables must be modified to allow traffic to port 443.

References

https://www.nginx.com/blog/nginx-ssl/

https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/

https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp

https://docs.nginx.com/nginx/admin-guide/security-controls/securing-http-traffic-upstream/