Uvicorn with HTTPS
Self-signed Transport Layer Security (TLS) is a certificate generated and signed by the same entity that uses it rather than being issued by a trusted third-party Certificate Authority (CA). When a website or application uses self-signed TLS, the browser will warn the user that the connection may not be secure, as a trusted authority cannot verify the server's identity.
Self-signed TLS can be helpful in certain situations, such as when testing an application in a closed environment or when using a private network where it is not feasible or necessary to obtain a trusted certificate from a third-party CA. Moreover, browser warnings can be overcome by importing your self-signed CA certificate.
However, when deploying your application for real, it is recommended to use a trusted CA to issue TLS certificates to ensure the highest level of security and trustworthiness for website and application users, demonstrated in the final deployment section.
Danger
While this is only a noddy self-signed certificate for development and learning purposes it is critical to never include private keys in version control. Avoid doing so in any situation to maintain good habits!
Create a new directory for keeping shell scripts, this will be external to your Python project:
Add the following script, updating the parameters as required:
#!/bin/bash
# PARAMETERS
CA_CN="ca.local"
CA_KEYPASSWORD="changeme"
CA_DAYS=3650
CA_COUNTRY="UK"
CA_STATE="South Yorks"
CA_LOCATION="Doncaster"
CA_ORG="Local CA"
CA_UNIT="CA dept"
CA_EMAIL="noreply@ca.local"
CERT_CN="www.exampleforyou.net"
CERT_ALT_NAME="localhost"
CERT_KEYPASSWORD="changeme"
CERT_DAYS=3650
CERT_COUNTRY="UK"
CERT_STATE="South Yorks"
CERT_LOCATION="Doncaster"
CERT_ORG="Exampleforyou"
CERT_UNIT="Developers"
CERT_EMAIL="noreply@exampleforyou.net"
# Create ext file
/bin/cat > "$CERT_CN.ext" <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth,clientAuth
subjectAltName = @alt_names
[req]
default_bits = 4096
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[dn]
C=$CERT_COUNTRY
ST=$CERT_STATE
L=$CERT_LOCATION
O=$CERT_ORG
OU=$CERT_UNIT
CN=$CERT_CN
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1=$CERT_CN
DNS.2=$CERT_ALT_NAME
EOF
# CA Cert
# Create private key for local CA
/usr/bin/openssl genrsa -des3 -out local-ca.key -passout pass:"$CA_KEYPASSWORD" 2048
# Create root certificate
/usr/bin/openssl req -x509 -new -nodes -key local-ca.key -sha256 -days "$CA_DAYS" -passin pass:"$CA_KEYPASSWORD" -subj "/C=$CA_COUNTRY/ST=$CA_STATE/L=$CA_LOCATION/O=$CA_ORG/OU=$CA_UNIT/CN=$CA_CN/emailAddress=$CA_EMAIL" -out local-ca.crt
# Certificate
# Create private key for host
/usr/bin/openssl genrsa -des3 -out "$CERT_CN.key" -passout pass:"$CERT_KEYPASSWORD" 2048
# Generate Certificate Signing Request
/usr/bin/openssl req -new -key "$CERT_CN.key" -out "$CERT_CN.csr" -passin pass:"$CERT_KEYPASSWORD" -days $CERT_DAYS -subj "/C=$CERT_COUNTRY/ST=$CERT_STATE/L=$CERT_LOCATION/O=$CERT_ORG/OU=$CERT_UNIT/CN=$CERT_CN/emailAddress=$CERT_EMAIL"
# Remove the password from the private key
/usr/bin/cp "$CERT_CN.key" "$CERT_CN.key.original"
/usr/bin/openssl rsa -in "$CERT_CN.key.original" -out "$CERT_CN.key" -passin pass:"$CERT_KEYPASSWORD"
# Create Certificate
openssl x509 -req -in "$CERT_CN.csr" -CA local-ca.crt -CAkey local-ca.key -CAcreateserial -passin pass:"$CA_KEYPASSWORD" -out "$CERT_CN.crt" -days "$CERT_DAYS" -sha256 -extfile "$CERT_CN.ext"
If not already installed, the script uses openssl
, on a Debian host:
Make the script executable:
Run the script, this will generate a CA root certificate and an application certificate and private key:
The script generates a local Certificate CA Authority certificate, a private key and certificate for the domain www.exampleforyou.net
. I also included X509v3 Subject Alternative Name
for the localhost
domain for working in development.
You can view the certificate using OpenSSL, for example:
Using these assets, you can import the newly generate local CA certificate into your local Linux host, so it will trust the self-signed certificates.
On Fedora and Red Hat Enterprise Linux based systems, you can then import and trust the local CA certificate:
Alternatively, it's easy to find the certificate settings in the browser you're using and import you local CA certificate directly into your browser.
In Firefox, go to settings and search for "certificates", then select "View Certificates", and under "Authorities", you can import you local CA certificate.
Ensure you're back in the root of your project directory:
You can now simulate a mounted directory, back in the root of your Python project, create a mnt
directory:
Create a soft link, where you specify the target followed by the name of the link, for example:
The project should look something like this, using tree -I venv
:
.
├── api
│ └── health_api.py
├── config
│ └── settings.py
├── main.py
├── mnt
│ └── certs -> /home/<USERNAME>/volumes/certs
├── non_ver_reqs.txt
├── README.md
└── requirements.txt
When developing on a local system, DNS will always look in /etc/hosts
first. This
means you can use the fully qualified domain name (even if it's a bogus domain). Add your domain to /etc/hosts
to simulate the use of real DNS resolution, and test the SSL certificates:
Update settings.py
to include new environment variables for the certificate and key
file locations, and additionally, one to set the debug level.
import pathlib
from pydantic.env_settings import BaseSettings
# Project Directories
ROOT = pathlib.Path(__file__).resolve().parent.parent
class Settings(BaseSettings):
CONF_API_V1_STR: str = "/api/v1"
CONF_MAIN_APP_HOST = "0.0.0.0"
CONF_MAIN_APP_PORT = 8000
CONF_DEBUG_LEVEL = "debug" # NEW
CONF_SSL_KEYFILE = "mnt/certs/www.exampleforyou.net.key" # NEW
CONF_SSL_CERTFILE = "mnt/certs/www.exampleforyou.net.crt" # NEW
settings = Settings()
Finally, update main.py
to include these parameters, which will serve the
application using HTTPS:
if __name__ == '__main__':
configure()
uvicorn.run(main_app,
host=settings.CONF_MAIN_APP_HOST,
port=settings.CONF_MAIN_APP_PORT,
log_level=settings.CONF_DEBUG_LEVEL, # NEW
ssl_keyfile=settings.CONF_SSL_KEYFILE, # NEW
ssl_certfile=settings.CONF_SSL_CERTFILE) # NEW
else:
configure()
All being well, you should be able to start the application as normal with python main.py
, but now have it using HTTPS and the FQDN www.exampleforyou.net
.
However, running this as a regular user on local Linux system (without a reverse proxy listening on port 443), means you still need to specify a port, for example https://www.exampleforyou.net:8000/docs
You can open up the ports below 1000 for regular users on your local Linux host, and specify port 443 if you want to remove the port number from the URL, for example:
To ensure the setting persists, you can add this to the /etc/sysctl.d/99-custom.conf
file:
Force the system to read and set the new kernel parameters:
Not you can change the port in your settings.py
:
import pathlib
from pydantic.env_settings import BaseSettings
# Project Directories
ROOT = pathlib.Path(__file__).resolve().parent.parent
class Settings(BaseSettings):
CONF_API_V1_STR: str = "/api/v1"
CONF_MAIN_APP_HOST = "0.0.0.0"
CONF_MAIN_APP_PORT = 443 # UPDATED
CONF_DEBUG_LEVEL = "debug"
CONF_SSL_KEYFILE = "mnt/certs/www.exampleforyou.net.key"
CONF_SSL_CERTFILE = "mnt/certs/www.exampleforyou.net.crt"
settings = Settings()
And now you can simulate the real world https://www.exampleforyou.net/docs:
Either way, you get to serve the application locally, using a domain name, and with a nice padlock with no complaints from the browser about invalid certificates, pretty cool right?
Tip
I also include an alternative DNS in the certificate generation script for localhost
.
Therefore, you can also use https://localhost/docs for local development.