I’ve begun moving from warp to axum with my Rust projects for various reasons. One project requires TLS, so I had to figure out the appropriate way to use certs in axum. Here’s the simple version, based off of axum’s tls-rustls example. First, add some dependencies to your Cargo.toml:

[dependencies]
axum = "0.7"
axum-server = { version = "0.7", features = ["tls-rustls"] }
tokio = { version = "1.40.0", features = ["rt-multi-thread"] }

Then, we’ll start with the basic version that does TLS:

use std::net::SocketAddr;

use axum::{routing::get, Router};

use axum_server::tls_rustls::RustlsConfig;

const HTTPS_PORT: u16 = 8443;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root));

    let config = RustlsConfig::from_pem_file("cert3.pem", "privkey3.pem")
        .await
        .unwrap();

    let addr = SocketAddr::from(([0, 0, 0, 0], HTTPS_PORT));

    axum_server::bind_rustls(addr, config)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root() -> &'static str {
    "Hello, World!"
}

The only real important parts here are the use of a specific port for HTTPS (see below) and the use of the from_pem_file function. (There are both tls_rustls and tls_openssl modules for your TLS implementation of choice.)

One thing you might want is an HTTP-to-HTTPS redirect: if a user hits your server with the http scheme, you can automatically redirect to an https endpoint. This requires a few more imports, a function to do the redirect, and a spawned task. A complete, updated version of the above code:

use std::net::SocketAddr;

use axum::{
    extract::Host,
    handler::HandlerWithoutStateExt,
    http::{StatusCode, Uri},
    response::Redirect,
    routing::get,
    BoxError, Router,
};

use axum_server::tls_rustls::RustlsConfig;

const HTTP_PORT: u16 = 8080;
const HTTPS_PORT: u16 = 8443;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root));

    tokio::spawn(redirect_http_to_https());

    let config = RustlsConfig::from_pem_file("cert3.pem", "privkey3.pem")
        .await
        .unwrap();

    let addr = SocketAddr::from(([0, 0, 0, 0], HTTPS_PORT));

    axum_server::bind_rustls(addr, config)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root() -> &'static str {
    "Hello, World!"
}

async fn redirect_http_to_https() {
    fn make_https(host: String, uri: Uri) -> Result<Uri, BoxError> {
        let mut parts = uri.into_parts();

        parts.scheme = Some(axum::http::uri::Scheme::HTTPS);

        if parts.path_and_query.is_none() {
            parts.path_and_query = Some("/".parse().unwrap());
        }

        let https_host = host.replace(&HTTP_PORT.to_string(), &HTTPS_PORT.to_string());
        parts.authority = Some(https_host.parse()?);

        Ok(Uri::from_parts(parts)?)
    }

    let redirect = move |Host(host): Host, uri: Uri| async move {
        match make_https(host, uri) {
            Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
            Err(_) => Err(StatusCode::BAD_REQUEST),
        }
    };

    let addr = SocketAddr::from(([127, 0, 0, 1], HTTP_PORT));
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();

    axum::serve(listener, redirect.into_make_service())
        .await
        .unwrap();
}

Note that HTTP and HTTPS must use different ports, else you’ll get an “Address already in use” error when trying to bind multiple schemes.