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
- Create a CA (Certificate Authority)
- Generate Server Certificates
- Generate Client Certificates
- Define minimal Flask application
- Enable certificate based authentication (CBA) for Flask’s development server
- Use CBA for a gunicorn powered Flask application
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:
- generate a private key
- create a CSR (Certificate Signing Request) which is basically a statement file with details about the server that wants to obtain a certificate
- try to issue the certificate for the server based on its CSR through CA. The generated certificate will contain details about the server (from CSR), its public key and CA’s digital signature that is used by the clients to verify the trustworthiness of certificate.
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