HTTPS Certificate Based Authentication with Flask and gunicorn

July 4, 2019
auth flask gunicorn

HTTPS Certificate Based Authentication with Flask and gunicorn

The intent of the post is to briefly instruct how to enable 2-sided (server & client) HTTPS authentication for Flask powered web applications, using the Flask’s built-in server and the more mature WSGI server - gunicorn.

In this tutorial, the certificates used for authentication are self-signed certificates. For applications that reside and run in a in-company network, this is totally fine, however in a production environment it is more desirable to use certificates issued by trusted certificate authorities (e.g LetsEncrypt) and have web servers (e.g. Nginx, Apache) to handle all HTTPS stuff.

Without too much details, lets define and jump right into the action items, which are:

Certificates Generation

In order to generate certificates, make sure that you have installed the openssl library for your corresponding operating system.

Create a CA (Certificate Authority)

Foremost, we must create a Certificate Authority (CA) that should be perceived as the entity that is going to issue and sign our digital certificates. It will be involved in the validation of the ownership and integrity of the certificates.

openssl req -nodes -new -x509 -days 365 -keyout ca.key -out ca-crt.pem

When running the above command, you will be prompted to enter details about the CA, so make sure to populate with truthful information.
On a successful attempt, it will generate the private key of CA ca.key and the root certificate ca-crt.pem.

Generate Server Certificates

For establishing HTTPS connections, the server always must have a pair of private & public keys, as they will be used for exchanging the symmetric session key (generated by the client), later to be used for encrypting/decrypting messages, therefore ensuring a secure communication between client and server.
To have them in place, we will:

Note that for CSR, you will be required to enter details about the server, therefore make sure to specify them accordingly.

# create server private key and server CSR
openssl req -nodes -new -keyout server.key -out server.csr

# generate certicate based on server's CSR using CA root certificate and CA private key
openssl x509 -req -days 365 -in server.csr -CA ca-crt.pem -CAkey ca.key -CAcreateserial -out server.crt

# verify the certificate (optionally)
openssl verify -CAfile ca-crt.pem server.crt

Generate Client Certificates

Because we want to achieve a mutual certificate based authentication, we must repeat the same steps that we’ve done for server certificate.

# create client private key and client CSR
openssl req -nodes -new -keyout client.key -out client.csr

# generate certicate based on client's CSR using CA root certificate and CA private key
openssl x509 -req -days 365 -in client.csr -CA ca-crt.pem -CAkey ca.key -CAcreateserial -out client.crt

# verify the certificate (optionally)
openssl verify -CAfile ca-crt.pem client.crt

Define minimal Flask application

Lets build a minimal Flask application that serves requests via HTTP as we’re all used to.

from flask import Flask

app = Flask(__name__)


@app.route('/ping')
def ping():
    return 'pong'


if __name__ == '__main__':
    app.run('0.0.0.0', 8080)

Verify that everything is working as expected:

$ curl http://localhost:8080/ping
pong

Enable certificate based authentication (CBA) for Flask’s development server

Firstly we will enable HTTPS only by requiring server certificates, meaning that clients will be able to make requests via https and only they will be in charge of verifying the server’s certificate.

import ssl

...

if __name__ == '__main__':
    context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
    context.load_cert_chain('server.crt', 'server.key')
    app.run('0.0.0.0', 8080, ssl_context=context)

To see this in action:

$ curl http://localhost:8080/ping
curl: (56) Recv failure: Connection reset by peer

$ curl https://localhost:8080/ping
curl: (60) SSL certificate problem: unable to get local issuer certificate
..
# this is not a problem, curl just does not trust our CA

$ curl --insecure https://localhost:8080/ping
pong

Great, now we can communicate with the Flask app using truly HTTPS connections.

If we want our Flask app to mutually verify the identity of all of its clients, we should adjust it slightly:

...

if __name__ == '__main__':
    context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
    context.verify_mode = ssl.CERT_REQUIRED
    context.load_verify_locations('ca-crt.pem')
    context.load_cert_chain('server.crt', 'server.key')
    app.run('0.0.0.0', 8080, ssl_context=context)

Finally we can test everything we wanted to achieve:

$ curl --insecure https://localhost:8080/ping
curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure
# server dropped the client as it did not provide a certificate

$ curl --insecure --cacert ca-crt.pem --key client.key --cert client.crt https://localhost:8080/ping
pong
# client received a successful response from server

Use CBA for a gunicorn powered Flask application

Running all this with gunicorn is even easier, because we don’t need to do all that manipulations regarding the SSLContext. Just run gunicorn with a few more arguments and you’re all set up:

gunicorn --bind :8080 --keyfile server.key --certfile server.crt --ca-certs ca-crt.pem --cert-reqs 2 app:app
comments powered by Disqus