salvo-rs

salvo-tls-acme

11
1
# Install this skill:
npx skills add salvo-rs/salvo-skills --skill "salvo-tls-acme"

Install specific skill from multi-skill repository

# Description

Configure TLS/HTTPS with automatic certificate management via ACME (Let's Encrypt). Use for production deployments with secure connections.

# SKILL.md


name: salvo-tls-acme
description: Configure TLS/HTTPS with automatic certificate management via ACME (Let's Encrypt). Use for production deployments with secure connections.


Salvo TLS and ACME Configuration

This skill helps configure TLS/HTTPS and automatic certificate management in Salvo applications.

TLS with Rustls

Setup

[dependencies]
salvo = { version = "1.88.1", features = ["rustls"] }

Basic TLS Configuration

use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};

#[handler]
async fn hello() -> &'static str {
    "Hello over HTTPS!"
}

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);

    // Load certificate and private key
    let config = RustlsConfig::new(
        Keycert::new()
            .cert_from_path("certs/cert.pem")
            .unwrap()
            .key_from_path("certs/key.pem")
            .unwrap()
    );

    let acceptor = TcpListener::new("0.0.0.0:443")
        .rustls(config)
        .bind()
        .await;

    Server::new(acceptor).serve(router).await;
}

Certificate from Memory

use salvo::conn::rustls::{Keycert, RustlsConfig};

let cert_pem = include_bytes!("../certs/cert.pem");
let key_pem = include_bytes!("../certs/key.pem");

let config = RustlsConfig::new(
    Keycert::new()
        .cert(cert_pem.to_vec())
        .key(key_pem.to_vec())
);

ACME (Let's Encrypt) Auto-Certificates

Setup

[dependencies]
salvo = { version = "1.88.1", features = ["acme"] }

HTTP-01 Challenge

use salvo::prelude::*;
use salvo::conn::acme::{AcmeConfig, AcmeListener, ChallengeType};

#[handler]
async fn hello() -> &'static str {
    "Hello with auto-certificate!"
}

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);

    // Configure ACME
    let config = AcmeConfig::builder()
        .domains(["example.com", "www.example.com"])
        .contacts(["mailto:[email protected]"])
        .challenge_type(ChallengeType::Http01)
        .cache_path("./acme_cache")
        .build()
        .unwrap();

    // ACME listener handles HTTP-01 challenges and serves HTTPS
    let acceptor = AcmeListener::builder()
        .acme_config(config)
        .bind("0.0.0.0:443")
        .await;

    Server::new(acceptor).serve(router).await;
}

TLS-ALPN-01 Challenge

For environments where port 80 is not available:

use salvo::conn::acme::{AcmeConfig, AcmeListener, ChallengeType};

let config = AcmeConfig::builder()
    .domains(["example.com"])
    .contacts(["mailto:[email protected]"])
    .challenge_type(ChallengeType::TlsAlpn01)
    .cache_path("./acme_cache")
    .build()
    .unwrap();

Force HTTPS Redirect

use salvo::prelude::*;

#[handler]
async fn force_https(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
    // Check if request is HTTP (not HTTPS)
    if req.uri().scheme_str() == Some("http") {
        let host = req.header::<String>("Host").unwrap_or_default();
        let path = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("/");
        let https_url = format!("https://{}{}", host, path);

        res.status_code(StatusCode::MOVED_PERMANENTLY);
        res.headers_mut().insert("Location", https_url.parse().unwrap());
        ctrl.skip_rest();
        return;
    }

    ctrl.call_next(req, depot, res).await;
}

HTTP and HTTPS on Different Ports

use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);

    // HTTPS on 443
    let tls_config = RustlsConfig::new(
        Keycert::new()
            .cert_from_path("certs/cert.pem").unwrap()
            .key_from_path("certs/key.pem").unwrap()
    );

    let https_acceptor = TcpListener::new("0.0.0.0:443")
        .rustls(tls_config)
        .bind()
        .await;

    // HTTP on 80 (for redirects or ACME challenges)
    let http_acceptor = TcpListener::new("0.0.0.0:80")
        .bind()
        .await;

    // Run both servers
    tokio::join!(
        Server::new(https_acceptor).serve(router.clone()),
        Server::new(http_acceptor).serve(Router::new().hoop(redirect_to_https)),
    );
}

#[handler]
async fn redirect_to_https(req: &mut Request, res: &mut Response) {
    let host = req.header::<String>("Host").unwrap_or_default();
    let path = req.uri().path();
    res.render(salvo::writing::Redirect::permanent(format!("https://{}{}", host, path)));
}

Certificate Hot Reload

use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};
use std::sync::Arc;
use tokio::sync::RwLock;

// Reload certificates without restarting
async fn reload_certificates(config: Arc<RwLock<RustlsConfig>>) {
    let new_config = RustlsConfig::new(
        Keycert::new()
            .cert_from_path("certs/cert.pem").unwrap()
            .key_from_path("certs/key.pem").unwrap()
    );

    let mut guard = config.write().await;
    *guard = new_config;
}

HTTP/2 Support

HTTP/2 is automatically enabled when using Rustls:

use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};

// HTTP/2 is enabled by default with TLS
let config = RustlsConfig::new(
    Keycert::new()
        .cert_from_path("certs/cert.pem").unwrap()
        .key_from_path("certs/key.pem").unwrap()
);

HTTP/3 (QUIC) Support

[dependencies]
salvo = { version = "1.88.1", features = ["quinn"] }
use salvo::prelude::*;
use salvo::conn::quinn::QuinnListener;

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);

    let acceptor = QuinnListener::builder()
        .cert_path("certs/cert.pem")
        .key_path("certs/key.pem")
        .bind("0.0.0.0:443")
        .await;

    Server::new(acceptor).serve(router).await;
}

Security Headers for HTTPS

use salvo::prelude::*;

#[handler]
async fn security_headers(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
    // HTTP Strict Transport Security
    res.headers_mut().insert(
        "Strict-Transport-Security",
        "max-age=31536000; includeSubDomains; preload".parse().unwrap()
    );

    // Prevent mixed content
    res.headers_mut().insert(
        "Content-Security-Policy",
        "upgrade-insecure-requests".parse().unwrap()
    );

    ctrl.call_next(req, depot, res).await;
}

Complete ACME Example

use salvo::prelude::*;
use salvo::conn::acme::{AcmeConfig, AcmeListener, ChallengeType};

#[handler]
async fn hello() -> &'static str {
    "Hello with Let's Encrypt!"
}

#[handler]
async fn security_headers(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
    res.headers_mut().insert(
        "Strict-Transport-Security",
        "max-age=31536000; includeSubDomains".parse().unwrap()
    );
    ctrl.call_next(req, depot, res).await;
}

#[tokio::main]
async fn main() {
    let router = Router::new()
        .hoop(security_headers)
        .get(hello);

    let acme_config = AcmeConfig::builder()
        .domains(["example.com", "www.example.com"])
        .contacts(["mailto:[email protected]"])
        .challenge_type(ChallengeType::Http01)
        .cache_path("./acme_cache")
        .directory_url("https://acme-v02.api.letsencrypt.org/directory")
        .build()
        .unwrap();

    let acceptor = AcmeListener::builder()
        .acme_config(acme_config)
        .bind("0.0.0.0:443")
        .await;

    println!("Server running on https://example.com");
    Server::new(acceptor).serve(router).await;
}

Staging Environment

Use Let's Encrypt staging for testing:

let acme_config = AcmeConfig::builder()
    .domains(["example.com"])
    .contacts(["mailto:[email protected]"])
    .directory_url("https://acme-staging-v02.api.letsencrypt.org/directory")  // Staging
    .build()
    .unwrap();

Best Practices

  1. Use ACME in production: Automatic certificate renewal
  2. Set HSTS header: Force browsers to use HTTPS
  3. Enable HTTP/2: Better performance with TLS
  4. Test with staging: Use Let's Encrypt staging before production
  5. Cache certificates: Persist to disk for restart recovery
  6. Monitor expiration: Alert before certificates expire
  7. Redirect HTTP to HTTPS: Don't serve content over HTTP
  8. Use strong ciphers: Let Rustls handle cipher selection

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.