diff --git a/.taplo.toml b/.taplo.toml index 6da4c7f1..7ac39197 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -7,23 +7,23 @@ column_width = 100 [[rule]] include = ["**/Cargo.toml"] keys = [ - "dependencies", - "*-dependencies", - "workspace.dependencies", - "workspace.*-dependencies", - "target.*.dependencies", - "target.*.*-dependencies", + "dependencies", + "*-dependencies", + "workspace.dependencies", + "workspace.*-dependencies", + "target.*.dependencies", + "target.*.*-dependencies", ] formatting.reorder_keys = true [[rule]] include = ["**/Cargo.toml"] keys = [ - "dependencies.*", - "*-dependencies.*", - "workspace.dependencies.*", - "workspace.*-dependencies.*", - "target.*.dependencies", - "target.*.*-dependencies", + "dependencies.*", + "*-dependencies.*", + "workspace.dependencies.*", + "workspace.*-dependencies.*", + "target.*.dependencies", + "target.*.*-dependencies", ] formatting.reorder_keys = false diff --git a/Cargo.lock b/Cargo.lock index 7d5cab90..ae3448a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,7 +81,6 @@ dependencies = [ "actix-web", "casbin", "env_logger", - "tokio", ] [[package]] @@ -3157,6 +3156,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "example-tls-hot-reload" +version = "0.0.0" +dependencies = [ + "actix-web", + "color-eyre", + "examples-common", + "eyre", + "notify", + "rustls 0.23.27", + "rustls-channel-resolver", + "tokio", + "tracing", +] + +[[package]] +name = "examples-common" +version = "0.0.0" +dependencies = [ + "rustls 0.23.27", + "tracing", + "tracing-subscriber", +] + [[package]] name = "eyre" version = "0.6.12" @@ -6967,6 +6990,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-channel-resolver" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173e7e58f0cbb61d2ea2b21af1b5ed6e35fa6c17818fc2cc5d903b44b4715d01" +dependencies = [ + "rand 0.9.1", + "rustls 0.23.27", +] + [[package]] name = "rustls-client-cert" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 48a54e0d..137adead 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "databases/postgres", "databases/redis", "docker", + "examples-common", "forms/form", "forms/multipart-s3", "forms/multipart", @@ -34,6 +35,7 @@ members = [ "https-tls/acme-letsencrypt", "https-tls/awc-https", "https-tls/cert-watch", + "https-tls/hot-reload", "https-tls/openssl", "https-tls/rustls-client-cert", "https-tls/rustls", @@ -101,6 +103,7 @@ color-eyre = "0.6" derive_more = "2" dotenvy = "0.15" env_logger = "0.11" +examples-common = { path = "./examples-common" } eyre = { version = "0.6", default-features = false, features = ["auto-install", "track-caller"] } futures-util = { version = "0.3.17", default-features = false, features = ["std"] } log = "0.4" diff --git a/auth/casbin/Cargo.toml b/auth/casbin/Cargo.toml index bc1b524a..bdc27140 100644 --- a/auth/casbin/Cargo.toml +++ b/auth/casbin/Cargo.toml @@ -4,8 +4,6 @@ edition.workspace = true rust-version.workspace = true [dependencies] -actix-web.workspace = true - +actix-web = { workspace = true } casbin = "2" -env_logger.workspace = true -tokio.workspace = true +env_logger = { workspace = true } diff --git a/examples-common/Cargo.toml b/examples-common/Cargo.toml new file mode 100644 index 00000000..90391993 --- /dev/null +++ b/examples-common/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "examples-common" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +rustls = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/examples-common/src/lib.rs b/examples-common/src/lib.rs new file mode 100644 index 00000000..9bbd1015 --- /dev/null +++ b/examples-common/src/lib.rs @@ -0,0 +1,21 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{EnvFilter, prelude::*}; + +/// Initializes standard tracing subscriber. +pub fn init_standard_logger() { + tracing_subscriber::registry() + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); +} + +/// Load default rustls provider, to be done once for the process. +pub fn init_rustls_provider() { + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .unwrap(); +} diff --git a/https-tls/hot-reload/Cargo.toml b/https-tls/hot-reload/Cargo.toml new file mode 100644 index 00000000..8807ccfe --- /dev/null +++ b/https-tls/hot-reload/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "example-tls-hot-reload" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +actix-web = { workspace = true, features = ["rustls-0_23"] } +color-eyre = { workspace = true } +examples-common = { workspace = true } +eyre = { workspace = true } +notify = { workspace = true } +rustls = { workspace = true } +rustls-channel-resolver = "0.3" +tokio = { workspace = true, features = ["time", "rt", "macros"] } +tracing = { workspace = true } diff --git a/https-tls/hot-reload/README.md b/https-tls/hot-reload/README.md new file mode 100644 index 00000000..c583a371 --- /dev/null +++ b/https-tls/hot-reload/README.md @@ -0,0 +1,63 @@ +# HTTPS Server With TLS Cert/Key Hot Reload + +## Usage + +All documentation assumes your terminal is in this directly (`cd https-tls/hot-reload`). + +### Certificate + +We put the self-signed certificate in this directory as an example but your browser would complain that it isn't secure. So we recommend to use [`mkcert`] to trust it. To use local CA, you should run: + +```shell +$ mkcert -install +``` + +If you want to generate your own cert/private key file, then run: + +```shell +$ mkcert -key-file key.pem -cert-file cert.pem 127.0.0.1 localhost +``` + +### Running The Example Server + +```shell +$ RUST_LOG=info,example=debug cargo run +Starting HTTPS server at https://localhost:8443 +``` + +Reload the server by modifying the certificate metadata: + +```shell +$ touch cert.pem +``` + +For a deeper inspection, use a tool like [`inspect-cert-chain`] between refreshes of the cert/key files using [`mkcert`] as shown above: + +```shell +$ inspect-cert-chain --host=localhost --port=8443 +... +Serial Number: + 06:81:db:16:ff:c4:73:69:73:69:ae:d1:0e:3d:d1:5e +... + +$ mkcert -key-file key.pem -cert-file cert.pem 127.0.0.1 localhost +... + +$ inspect-cert-chain --host=localhost --port=8443 +... +Serial Number: + 00:a8:39:e7:aa:2e:73:18:f6:4e:d5:71:1e:c7:21:51:58 +... +``` + +Observing a change in the serial number without restarting the server demonstrates that the setup works. + +### Client + +- [HTTPie]: `http --verify=no :8443` +- cURL: `curl -v --insecure https://127.0.0.1:8443` +- Browser: navigate to + +[`mkcert`]: https://github.com/FiloSottile/mkcert +[httpie]: https://httpie.io/cli +[`inspect-cert-chain`]: https://github.com/robjtede/inspect-cert-chain diff --git a/https-tls/hot-reload/cert.pem b/https-tls/hot-reload/cert.pem new file mode 100644 index 00000000..5abcb64e --- /dev/null +++ b/https-tls/hot-reload/cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELDCCApSgAwIBAgIRAJV3HOzuRhQcriZIKUYROVkwDQYJKoZIhvcNAQELBQAw +aTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMR8wHQYDVQQLDBZyb2JA +c29tYnJhLmxvY2FsIChSb2IpMSYwJAYDVQQDDB1ta2NlcnQgcm9iQHNvbWJyYS5s +b2NhbCAoUm9iKTAeFw0yNTA1MTIwMzIwNTdaFw0yNzA4MTIwMzIwNTdaMFQxJzAl +BgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEpMCcGA1UECwwg +cm9iQE1hY0Jvb2tQcm8ubG9jYWxkb21haW4gKFJvYikwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDsKN1MBUrrXkhDDXIfhvciA03pVrhXLrla9slY2p/t +VPgayFASUfRs1KcyD8+8tpn3z6BWsUup67vfJFM7u1HTHbkEAXWwnEBXOc+503le +JAip33dXSXZQcLErcS0Ad1P26t/lVKctZgNuPiOCHG9OaU3BacGCFCECg0bc4lez +c8B+jhGgPjKholBMA6lLJrBZPS/R0aPRTdUbiaG7pa7U3Au4/wnQmF62zGkpN7ZY +nwExeLDixw/YZLO5Mc6sFzYTFFTfAVwzzYYWoej+tKoMyJFnA8vx+qY6HMVseNOM +xXY+x8pCEmeEs7DUYbCIMHwihkg4T+XFVpvc9OvMUFsrAgMBAAGjZDBiMA4GA1Ud +DwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBQX/S2W +axc/eafsJz5icz2r7oqvUDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJ +KoZIhvcNAQELBQADggGBAEnR2sgqDpNYDX4CW+cbkpdKW5dwAO/fATM24vv+EcL2 +hI3nAaUAYKRixtGgbvBEVUmdlQmQHR0hj9L8MZ73/c/G0dakJFf3V5feVCDKXLGo +pL2lP7m6O6H4Q+BErHHO2FTMILZNmxG6+EmCZCVz4FpVvXfMFGih4cMk7Wedb/0U +yj0vcg2BqHFr8HaXbqu0AwkMkKTW0N4QkW2+6V+Uki8cm1m9a/KRtINGxd24ljwf +xM/KOp1ifWvoVupiAVHeCgJNKs06frI4AMf1HbFmyAftnOGwp+fIrfUMZbnp+Coj +NBXPoM5Zq1eheXtTEbny22EOy7IrtjDNSXgJNUUsbEL4w3GH8W2OdbXQMb0zH9Bj +lBRSMNkKPXoQgTHum9UATqQPBhgcP2JD4g89BYqv0JHgPYWuA+LUvSfpObxXebS/ +oLRHhQpDU3zXKem6Lr5GSMZGDz2gAECVVpGalIdvmyL1LHiHFmb7oTvswxaSPcfj +OKnFl5b+4ezaAW4D6dLxhg== +-----END CERTIFICATE----- diff --git a/https-tls/hot-reload/key.pem b/https-tls/hot-reload/key.pem new file mode 100644 index 00000000..c91c29ba --- /dev/null +++ b/https-tls/hot-reload/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDsKN1MBUrrXkhD +DXIfhvciA03pVrhXLrla9slY2p/tVPgayFASUfRs1KcyD8+8tpn3z6BWsUup67vf +JFM7u1HTHbkEAXWwnEBXOc+503leJAip33dXSXZQcLErcS0Ad1P26t/lVKctZgNu +PiOCHG9OaU3BacGCFCECg0bc4lezc8B+jhGgPjKholBMA6lLJrBZPS/R0aPRTdUb +iaG7pa7U3Au4/wnQmF62zGkpN7ZYnwExeLDixw/YZLO5Mc6sFzYTFFTfAVwzzYYW +oej+tKoMyJFnA8vx+qY6HMVseNOMxXY+x8pCEmeEs7DUYbCIMHwihkg4T+XFVpvc +9OvMUFsrAgMBAAECggEBAIfu4apvZXdjVp7Z73W8PyYh1sfX9dWg/GoioTT26pU2 +knUAFi7lY5b9NJv5Q+7xAGEG7tjXxqCxIvvHMe2w3eFyO1vV50NYPSS4Dxx8YGDS +xvXYvh3NGEAnDaPeyjN5fCgle+jKOExGavUa6V9sNJlivbH1yL+yDGog3DoqQqb0 +f6jo4hmwlRlsd7o+YsrW9v8vCWWDYKoYh13e77iQBGVw1b32qILeMEuQhhuDsh2T +Qd+nyUIBSuXaPtrwSQgWrkNn1fBSHwvMsfXijwTK06LBq23h0g5rG7w7JtG4JEum +XNq+7DaZJDYa0fq5gIDhl5GV2MQNEfbjuYJ/fN09SukCgYEA9HPPasVy7Pdv8YAk +ylsV9vEz/qMt+MIlg5Z2Y6HjWI7WwDOD4pR7gDdaJq5ymlR7I8EokppA6wOMM3Te +eog55DzWiOQxZTyPTXzpEKdxi9BZDI6F+tJO3Zdj5ppWVD4NN48NatGsun/kEIPL +IqY+AAKRxf+els74BTRuKQ3R5i8CgYEA91DE3HZu9ZmSv5rknKCO/c+3iQi2UNa6 +XsqH75tU+dPI/1U2z9gLOvu9JF5StVSmfdxk4NIzcJRJW//l7vzTKH58p/M9CeE5 +JYUtJkd96WvJ1jB+obTFnkC4yCYN9jm2K+9kVFcDkwFUVrhomkIby/+dktr7R3T9 +lsyfcKikF8UCgYEA8HzGf4oESDAdRv8EMrdtYmVk+4vZfDKz6UKq8dWf7c2IY8nK +Y6wj272YyRkx0bZu9nveyGtMlmgFE9JT1UQTgACCJmYoWio76MWMHEA+qoesM3g7 +QsiHoeR/+au4ZmQtaI0pa/8e6NNMsRqXS100/ZmJg7q4cDDpO2WbQnRAHS0CgYEA +9GzEE2uNoHgGXA32sYHRsLGRIAMXRO/jw/mAveOT6VFRzmBmyqYn+0R/m6kJLyOZ +ZLzkinnU0wgLNLzFgBwpiVTxWIACrHgGpblodPOlUoPwOBs3nBPwV8Z5mX5awCYr +kGKJkv1oj+p5czfQUdzSYhygnFqGjAno8xgK4Cob+00CgYEApLj4DB4lkwWGOfQE +RkTHFz1mWhBTtQ5Kc0+Jnw/jAcQYUnlOpMK0+72LzWRjYweYuUdIvWOGaajFuVhw +H86HoarFU2sO8GnTJR1P35wBgheXG1Lumo7AVRorrq694totQfzqXrJxVnKG6jvW +bFLe+y8oZBvIa0OntEPWsLegasc= +-----END PRIVATE KEY----- diff --git a/https-tls/hot-reload/src/main.rs b/https-tls/hot-reload/src/main.rs new file mode 100644 index 00000000..ccbd27e8 --- /dev/null +++ b/https-tls/hot-reload/src/main.rs @@ -0,0 +1,96 @@ +use std::path::Path; + +use actix_web::{ + App, HttpRequest, HttpResponse, HttpServer, http::header::ContentType, middleware, web, +}; +use eyre::WrapErr as _; +use notify::{Event, RecursiveMode, Watcher as _}; +use rustls::{ + ServerConfig, + pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, + sign::CertifiedKey, +}; + +async fn index(req: HttpRequest) -> HttpResponse { + tracing::debug!("{req:?}"); + + HttpResponse::Ok().content_type(ContentType::html()).body( + "\ +

Welcome to your TLS-secured homepage!

\ + ", + ) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> eyre::Result<()> { + color_eyre::install()?; + examples_common::init_standard_logger(); + examples_common::init_rustls_provider(); + + // initial load of certificate and key files + let cert_key = load_certified_key()?; + + // signal channel used to notify rustls of cert/key file changes + let (reload_tx, cert_resolver) = rustls_channel_resolver::channel::<8>(cert_key); + + let rustls_config = ServerConfig::builder() + .with_no_client_auth() + .with_cert_resolver(cert_resolver); + + // unsupervised watcher thread which will just shutdown when the server stops + tracing::debug!("Setting up cert watcher"); + + let mut file_watcher = + notify::recommended_watcher(move |res: notify::Result| match res { + Ok(ev) => { + tracing::info!("files changed: {:?}", ev.paths); + + let cert_key = load_certified_key().unwrap(); + reload_tx.update(cert_key); + } + Err(err) => { + tracing::error!("file watch error: {err}"); + } + }) + .wrap_err("Failed to set up file watcher")?; + + file_watcher + .watch(Path::new("cert.pem"), RecursiveMode::NonRecursive) + .wrap_err("Failed to watch cert file")?; + file_watcher + .watch(Path::new("key.pem"), RecursiveMode::NonRecursive) + .wrap_err("Failed to watch key file")?; + + tracing::info!("Starting HTTPS server at https://localhost:8443"); + + // start running server as normal (as opposed to in a loop like the cert-watch example) + HttpServer::new(|| { + App::new() + .service(web::resource("/").to(index)) + .wrap(middleware::Logger::default().log_target("@")) + }) + .workers(2) + .bind_rustls_0_23(("127.0.0.1", 8443), rustls_config)? + .run() + .await?; + + Ok(()) +} + +fn load_certified_key() -> eyre::Result { + // load TLS key/cert files + let cert_chain = CertificateDer::pem_file_iter("cert.pem") + .wrap_err("Could not locate certificate chain file")? + .flatten() + .collect(); + + // load TLS private key file + let key = + PrivateKeyDer::from_pem_file("key.pem").wrap_err("Could not locate PKCS 8 private keys")?; + + // parse private key by crypto provider + let key = rustls::crypto::aws_lc_rs::sign::any_supported_type(&key) + .wrap_err("Private key type is unsupported")?; + + Ok(CertifiedKey::new(cert_chain, key)) +}