TLS/SSL/HTTPS
How do I run Sanic via HTTPS?
If you do not have TLS certificates yet, see the end of this page.
Single domain and single certificate#
Let Sanic automatically load your certificate files, which need to be named fullchain.pem
and privkey.pem
in the given folder:
sudo sanic myserver:app -H :: -p 443 \
--tls /etc/letsencrypt/live/example.com/
app.run("::", 443, ssl="/etc/letsencrypt/live/example.com/")
Or, you can pass cert and key filenames separately as a dictionary:
Additionally, password
may be added if the key is encrypted, all fields except for the password are passed to request.conn_info.cert
.
ssl = {
"cert": "/path/to/fullchain.pem",
"key": "/path/to/privkey.pem",
"password": "for encrypted privkey file", # Optional
}
app.run(host="0.0.0.0", port=8443, ssl=ssl)
Alternatively, ssl.SSLContext
may be passed, if you need full control over details such as which crypto algorithms are permitted. By default Sanic only allows secure algorithms, which may restrict access from very old devices.
import ssl
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain("certs/fullchain.pem", "certs/privkey.pem")
app.run(host="0.0.0.0", port=8443, ssl=context)
Multiple domains with separate certificates#
A list of multiple certificates may be provided, in which case Sanic chooses the one matching the hostname the user is connecting to. This occurs so early in the TLS handshake that Sanic has not sent any packets to the client yet.
If the client sends no SNI (Server Name Indication), the first certificate on the list will be used even though on the client browser it will likely fail with a TLS error due to name mismatch. To prevent this fallback and to cause immediate disconnection of clients without a known hostname, add None
as the first entry on the list. --tls-strict-host
is the equivalent CLI option.
ssl = ["certs/example.com/", "certs/bigcorp.test/"]
app.run(host="0.0.0.0", port=8443, ssl=ssl)
sanic myserver:app
--tls certs/example.com/
--tls certs/bigcorp.test/
--tls-strict-host
Tip
You may also use None
in front of a single certificate if you do not wish to reveal your certificate, true hostname or site content to anyone connecting to the IP address instead of the proper DNS name.
Dictionaries can be used on the list. This allows also specifying which domains a certificate matches to, although the names present on the certificate itself cannot be controlled from here. If names are not specified, the names from the certificate itself are used.
To only allow connections to the main domain example.com and only to subdomains of bigcorp.test:
ssl = [
None, # No fallback if names do not match!
{
"cert": "certs/example.com/fullchain.pem",
"key": "certs/example.com/privkey.pem",
"names": ["example.com", "*.bigcorp.test"],
}
]
app.run(host="0.0.0.0", port=8443, ssl=ssl)
Accessing TLS information in handlers via request.conn_info
fields#
.ssl
- is the connection secure (bool).cert
- certificate info and dict fields of the currently active cert (dict).server_name
- the SNI sent by the client (str, may be empty)
Do note that all conn_info
fields are per connection, where there may be many requests over time. If a proxy is used in front of your server, these requests on the same pipe may even come from different users.
Redirect HTTP to HTTPS, with certificate requests still over HTTP#
In addition to your normal server(s) running HTTPS, run another server for redirection, http_redir.py
:
from sanic import Sanic, exceptions, response
app = Sanic("http_redir")
# Serve ACME/certbot files without HTTPS, for certificate renewals
app.static("/.well-known", "/var/www/.well-known", resource_type="dir")
@app.exception(exceptions.NotFound, exceptions.MethodNotSupported)
def redirect_everything_else(request, exception):
server, path = request.server_name, request.path
if server and path.startswith("/"):
return response.redirect(f"https://{server}{path}", status=308)
return response.text("Bad Request. Please use HTTPS!", status=400)
It is best to setup this as a systemd unit separate of your HTTPS servers. You may need to run HTTP while initially requesting your certificates, while you cannot run the HTTPS server yet. Start for IPv4 and IPv6:
sanic http_redir:app -H 0.0.0.0 -p 80
sanic http_redir:app -H :: -p 80
Alternatively, it is possible to run the HTTP redirect application from the main application:
# app == Your main application
# redirect == Your http_redir application
@app.before_server_start
async def start(app, _):
app.ctx.redirect = await redirect.create_server(
port=80, return_asyncio_server=True
)
app.add_task(runner(redirect, app.ctx.redirect))
@app.before_server_stop
async def stop(app, _):
await app.ctx.redirect.close()
async def runner(app, app_server):
app.state.is_running = True
try:
app.signalize()
app.finalize()
app.state.is_started = True
await app_server.serve_forever()
finally:
app.state.is_running = False
app.state.is_stopping = True
Get certificates for your domain names#
You can get free certificates from Let's Encrypt. Install certbot via your package manager, and request a certificate:
sudo certbot certonly --key-type ecdsa --preferred-chain "ISRG Root X1" -d example.com -d www.example.com
Multiple domain names may be added by further -d
arguments, all stored into a single certificate which gets saved to /etc/letsencrypt/live/example.com/
as per the first domain that you list here.
The key type and preferred chain options are necessary for getting a minimal size certificate file, essential for making your server run as fast as possible. The chain will still contain one RSA certificate until when Let's Encrypt gets their new EC chain trusted in all major browsers.