From Flask, Gunicorn, Nginx to Docker and Security through HTTPS and Client Certificate

Juniarto Samsudin
4 min readMar 19, 2020

The journey is not an easy one but it is rewarding. By the end of the journey, you can tell whether someone is talking bullshit when that someone speaks about security or certificates. Talk is cheap. Let’s do the dirty work.

At the core is your Flask App. You can do many fancy routings, REST API calls. Cool. Until someone tell you, that running Flask App is not scale-able[ I don’t know why scale-able is needed, if your application only serve one user]. But anyway, you wrapped your Flask App with GUNICORN. What the FUCK is GUNICORN? Put it simply, it is an application server. When your Flask App is inside GUNICORN, no one can say it is weak or not scale-able. Good! Then one fine day, one asshole, say it to your face, your Flask App is not secure. The traffic between client browser and your application can be sniffed and encrypted. Asshole may be right. You don’t want to touch and change your code. You put another layer on top of GUNICORN. The mighty NGINX. You put ssl_certificate and ssl_certificate_ke to encrypt the traffic and ssl_client_certificate to authenticate your client. Mission accomplish.

Let’s go to the nitty-gritty details:

  1. Flask Application (app.py) and GUNICORN docker.
#app.pyfrom flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
return 'Hello World!'


if
__name__ == '__main__':
app.run(host='0.0.0.0', port=8001)

To run, you can either:

# Run on port 8001
python app.py

or

# Run on port 5000
export FLASK_APP = app.py
flask run --host=0.0.0.0 --port=5000

But you want to wrap your application up with GUNICORN and DOCKERIZED it.

Create gunicorn configuration (gunicorn-cfg.py)

#gunicorn-cfg.pybind = '0.0.0.0:5005'
workers = 1

Create Dockerfile

FROM python:3.7

ENV FLASK_APP app.py

COPY app.py gunicorn-cfg.py requirements.txt ./
RUN
pip install -r requirements.txt

EXPOSE 5005
CMD ["gunicorn", "--config", "gunicorn-cfg.py", "app:app"]

Build Docker Image

docker build -t juniarto/helloworld .

Run Docker

docker run -d --name helloworld juniarto/helloworld:latest

Pay attention to the following details.

  1. Gunicorn is running on port 5005.
  2. The Dockerfile expose port 5005.
  3. Docker run DOES NOT map any port to host.

This means port 5005 is not reachable from the host. It is only reachable inside docker internal network. The design is to make GUNICORN docker only reachable by another docker in docker internal network[in this case, our NGINX docker]. The only way to reach your application is through NGINX. Your application is pretty much safely encapsulated from hackers.

You still with me? Good

2. NGINX

There are 3 main parts in NGINX:

a. To Provide HTTPS: Create server certificates

b. To Provide Client Authentication: Create CA key and CA Certificate

a. Create server certificates for NGINX

mkdir -p /home/juniarto/nginx_certopenssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout nginx-selfsigned.key -out nginx-selfsigned.crt

You will get two files in /home/juniarto/nginx_cert

1. nginx-selfsigned.key

2. nginx-selfsigned.crt

b. Create CA key and CA Certificate [ AT SERVER]

mkdir -p /home/juniarto/client_cert/CAopenssl genrsa -des3 -out ca.key 4096      # CA Key
openssl req -new -x509 -days 365 -key ca.key -out ca.crt #CA Cert

ca.key is also known as SERVER PRIVATE KEY. This key should stay in the server, and should not be sent to other party.

ca.crt is known as SERVER PUBLIC KEY.

c. Create Client Certificate [AT CLIENT]

openssl genrsa -des3 -out user.key 4096       #User Keyopenssl req -new -key user.key -out user.csr  #Cert. Signing Request

user.key is known as USER/CLIENT PRIVATE KEY. It stays in the client and should not be sent to the server.

user.csr is known as CERTIFICATE SIGNING REQUEST. Send user.csr to the server. Server will create user.crt using server’s ca.crt and ca.key.

d. Send user.csr to server, to be signed by CA [AT SERVER]. Produce user.crt

openssl x509 -req -days 365 -in user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out user.crt
You’ll typically want to increment the serial number with each signing. Once the certificate expires, a new CSR doesn’t need to be recreated; the same one can be signed, which will create a new certificate tied to that public key.

The signed certificate would be sent back to the user along with the CA cert (not private key!), for installation on their device.

e. But, usually you have to bundle ca.crt and user.crt before you send it to client.[AT CLIENT]

openssl pkcs12 -export -out user.pfx -inkey user.key -in user.crt -certfile ca.crt

Pass the user.pfx to the client to be installed in their browser.

f. NGINX Configuration File

Create NGINX Configuration File (HelloWorld.conf) in /home/juniarto/nginx_conf

NB: Please copy ca.crt [for client authentication] to /home/juniarto/nginx_cert

proxy_pass is pointing to our gunicorn application in docker internal network.

server {
listen 443 ssl;
ssl_certificate /etc/nginx/nginx_cert/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/nginx_cert/nginx-selfsigned.key;
ssl_client_certificate /etc/nginx/nginx_cert/ca.crt;
ssl_verify_client optional;
location / {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://helloworld:5005;
proxy_set_header Host $host;
proxy_set_header X_Forwarded-For $proxy_add_x_forwarded_for;
}
}

g. Launch NGINX docker

docker run --name mynginx2 -v /home/juniarto/nginx_conf/:/etc/nginx/conf.d -v /home/juniarto/nginx_cert:/etc/nginx/nginx_cert --link=helloworld -p 443:443 -d nginx

--

--