SSL
It is best practice, to ensure communication between the user and the app's server be encrypted. It is essential however when the communication consists of sensitive information such as the user's password. As it is quite easy to enable this encryption I'd recommend only using encrypted communication.
To do so we can utilise HTTPS using SSL (or TLS) which is widely
supported and easy to use. To do so we need to get a certificate that
browsers will recognise. Fortunately Let's
Encrypt will issue us a certificate for
free. Let's Encrypt is usable with Terraform via the acme provider,
which is activated by adding the following to
infrastructure/main.tf
,
terraform {
required_providers {
...
acme = {
source = "vancluever/acme"
version = "~> 2.0"
}
}
}
and running terraform init
to initialise it.
To acquire a certificate for a domain name we'll need to prove to
Let's Encrypt that we control the domain name. Fortunately this is
doable via the acme provider via the following added to
infrastructure/certs.tf
,
provider "acme" {
server_url = "https://acme-v02.api.letsencrypt.org/directory"
}
resource "tls_private_key" "private_key" {
algorithm = "RSA"
}
resource "acme_registration" "me" {
account_key_pem = tls_private_key.private_key.private_key_pem
email_address = var.heroku_username
}
resource "acme_certificate" "tozo_dev" {
account_key_pem = acme_registration.me.account_key_pem
common_name = "tozo.dev"
dns_challenge {
provider = "gandiv5"
config = {
GANDIV5_API_KEY = var.gandi_api_key
}
}
}
Now we have a certificate we can ask Heroku to use it when serving our
app, via the following additions to infrastructure/heroku.tf
,
resource "heroku_domain" "tozo" {
...
sni_endpoint_id = heroku_ssl.tozo_dev.id
}
resource "heroku_ssl" "tozo_dev" {
app_id = heroku_app.tozo.uuid
certificate_chain = "${acme_certificate.tozo_dev.certificate_pem}${acme_certificate.tozo_dev.issuer_pem}"
private_key = acme_certificate.tozo_dev.private_key_pem
depends_on = [heroku_formation.tozo-web]
}
Finally we need to ensure that any visitor to our app uses HTTPS, even
if they initially visit using HTTP. This is achieved by a responding
with a redirect for any HTTP visitors by adding the following to
backend/src/backend/run.py
,
from typing import Optional
from quart import redirect, request, Response
def create_app() -> Quart:
...
@app.before_request
async def redirect_to_https() -> Optional[Response]:
if request.headers.get("X-Forwarded-Proto", "https") != "https":
return redirect(request.url.replace("http://", "https://", 1), 301)
return None