1
0
mirror of https://github.com/actix/examples synced 2025-05-14 00:43:53 +02:00

feat: add tls-hot-reload example

This commit is contained in:
Rob Ede 2025-05-12 04:23:06 +01:00
parent dfce64475b
commit 0293f48530
No known key found for this signature in database
GPG Key ID: 97C636207D3EF933
11 changed files with 308 additions and 17 deletions

View File

@ -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

35
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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 }

View File

@ -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 }

View File

@ -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();
}

View File

@ -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 }

View File

@ -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 <https://127.0.0.1:8443>
[`mkcert`]: https://github.com/FiloSottile/mkcert
[httpie]: https://httpie.io/cli
[`inspect-cert-chain`]: https://github.com/robjtede/inspect-cert-chain

View File

@ -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-----

View File

@ -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-----

View File

@ -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(
"<!DOCTYPE html><html><body>\
<p>Welcome to your TLS-secured homepage!</p>\
</body></html>",
)
}
#[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<Event>| 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<rustls::sign::CertifiedKey> {
// 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))
}