From 50fd3e618257bf62d5b7b016c8608a9d03a8e843 Mon Sep 17 00:00:00 2001 From: asonix Date: Wed, 31 Jan 2024 17:47:42 -0600 Subject: [PATCH] Enable serving over TLS --- Cargo.lock | 119 +++++++++++++++++++++++++------------- Cargo.toml | 5 +- dev.toml | 2 + pict-rs.toml | 14 +++++ scripts/setup-tls.sh | 9 +++ src/config/commandline.rs | 18 ++++++ src/config/file.rs | 6 ++ src/lib.rs | 92 +++++++++++++++++++++++++---- src/middleware/payload.rs | 4 +- src/tls.rs | 65 +++++++++++++++++++++ 10 files changed, 280 insertions(+), 54 deletions(-) create mode 100755 scripts/setup-tls.sh create mode 100644 src/tls.rs diff --git a/Cargo.lock b/Cargo.lock index d587887..7f408f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,11 +4,11 @@ version = 3 [[package]] name = "actix-codec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "bytes", "futures-core", "futures-sink", @@ -44,6 +44,7 @@ dependencies = [ "actix-codec", "actix-rt", "actix-service", + "actix-tls", "actix-utils", "ahash", "base64 0.21.7", @@ -53,6 +54,7 @@ dependencies = [ "derive_more", "encoding_rs", "futures-core", + "h2", "http", "httparse", "httpdate", @@ -146,6 +148,25 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-tls" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929e47cc23865cdb856e59673cfba2d28f00b3bbd060dfc80e33a00a3cea8317" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "pin-project-lite", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tracing", + "webpki-roots 0.25.3", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -168,6 +189,7 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", + "actix-tls", "actix-utils", "ahash", "bytes", @@ -261,9 +283,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" [[package]] name = "anstyle-parse" @@ -885,9 +907,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "eyre" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -1095,7 +1117,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.2.1", "slab", "tokio", "tokio-util", @@ -1286,6 +1308,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-more" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" + [[package]] name = "indenter" version = "0.3.3" @@ -1304,9 +1332,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1384,9 +1412,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linked-hash-map" @@ -1620,7 +1648,7 @@ checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" dependencies = [ "futures-core", "futures-sink", - "indexmap 2.1.0", + "indexmap 2.2.1", "js-sys", "once_cell", "pin-project-lite", @@ -1838,7 +1866,9 @@ dependencies = [ "reqwest", "reqwest-middleware", "reqwest-tracing", + "rustls 0.21.10", "rustls 0.22.2", + "rustls-channel-resolver", "rustls-pemfile 2.0.0", "rusty-s3", "serde", @@ -1857,7 +1887,7 @@ dependencies = [ "tokio-postgres-rustls", "tokio-uring", "tokio-util", - "toml 0.8.8", + "toml 0.8.9", "tracing", "tracing-actix-web", "tracing-error", @@ -1871,18 +1901,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -2148,7 +2178,7 @@ dependencies = [ "time", "tokio", "tokio-postgres", - "toml 0.8.8", + "toml 0.8.9", "url", "walkdir", ] @@ -2174,7 +2204,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -2189,9 +2219,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -2212,9 +2242,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64 0.21.7", "bytes", @@ -2239,6 +2269,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-rustls 0.24.1", @@ -2365,6 +2396,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-channel-resolver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0a6bf546dc283b4c1413532d2bf53a64b3a006ee57f7ca0f4984f35841cacb" +dependencies = [ + "nanorand", + "rustls 0.21.10", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -2491,9 +2532,9 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] @@ -2509,9 +2550,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -2520,9 +2561,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -3022,9 +3063,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325" dependencies = [ "serde", "serde_spanned", @@ -3043,11 +3084,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.1", "serde", "serde_spanned", "toml_datetime", @@ -3447,9 +3488,9 @@ checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -3677,9 +3718,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.34" +version = "0.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +checksum = "818ce546a11a9986bc24f93d0cdf38a8a1a400f1473ea8c82e59f6e0ffab9249" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 8723bd4..b637651 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ io-uring = ["dep:tokio-uring", "sled/io_uring", "actix-web/experimental-io-uring [dependencies] actix-form-data = "0.7.0-beta.6" -actix-web = { version = "4.0.0", default-features = false } +actix-web = { version = "4.0.0", default-features = false, features = ["rustls-0_21"] } async-trait = "0.1.51" barrel = { version = "0.7.0", features = ["pg"] } base64 = "0.21.0" @@ -48,6 +48,9 @@ reqwest-middleware = "0.2.2" reqwest-tracing = { version = "0.4.5" } # pinned to tokio-postgres-rustls rustls = "0.22.0" +rustls_021 = { package = "rustls", version = "0.21" } +# pinned to rustls +rustls-channel-resolver = "0.1.0" # pinned to rustls rustls-pemfile = "2.0.0" rusty-s3 = "0.5.0" diff --git a/dev.toml b/dev.toml index 6bc4022..6526fe8 100644 --- a/dev.toml +++ b/dev.toml @@ -2,6 +2,8 @@ address = '[::]:8080' api_key = 'api-key' max_file_count = 10 +certificate = "./data/pictrs.crt" +private_key = "./data/pictrs.key" [upgrade] concurrency = 512 diff --git a/pict-rs.toml b/pict-rs.toml index 189036a..77e5757 100644 --- a/pict-rs.toml +++ b/pict-rs.toml @@ -37,6 +37,20 @@ max_file_count = 1 # default: The system's advertised temporary directory ("/tmp" on most linuxes) temporary_directory = "/tmp" +## Optional: path to server certificate to enable TLS +# environment variable: PICTRS__SERVER__CERTIFICATE +# default: empty +# +# Note that both certificate and private_key must be set to enable TLS +certificate = "/path/to/server.crt" + +## Optional: path to server key to enable TLS +# environment variable: PICTRS__SERVER__PRIVATE_KEY +# default: empty +# +# Note that both private_key and certificate must be set to enable TLS +private_key = "/path/to/server.key" + ## Client configuration [client] ## Optional: time (in seconds) the client will wait for a response before giving up diff --git a/scripts/setup-tls.sh b/scripts/setup-tls.sh new file mode 100755 index 0000000..fe9d9c8 --- /dev/null +++ b/scripts/setup-tls.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -xe + +mkdir -p ./data + +certstrap --depot-path ./data init --common-name pictrsCA +certstrap --depot-path ./data request-cert --common-name pictrs --domain localhost +certstrap --depot-path ./data sign pictrs --CA pictrsCA diff --git a/src/config/commandline.rs b/src/config/commandline.rs index 2768995..431bcbe 100644 --- a/src/config/commandline.rs +++ b/src/config/commandline.rs @@ -55,6 +55,8 @@ impl Args { address, api_key, temporary_directory, + certificate, + private_key, client_timeout, upgrade_concurrency, metrics_prometheus_address, @@ -114,6 +116,8 @@ impl Args { danger_dummy_mode, max_file_count, temporary_directory, + certificate, + private_key, }; let client = Client { @@ -521,6 +525,10 @@ struct Server { max_file_count: Option, #[serde(skip_serializing_if = "Option::is_none")] temporary_directory: Option, + #[serde(skip_serializing_if = "Option::is_none")] + certificate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + private_key: Option, } #[derive(Debug, Default, serde::Serialize)] @@ -913,6 +921,16 @@ struct Run { #[arg(long)] temporary_directory: Option, + /// The path to the TLS certificate. Both the certificate and the private_key must be specified + /// to enable TLS + #[arg(long)] + certificate: Option, + + /// The path to the private key used to negotiate TLS. Both the private_key and the certificate + /// must be specified to enable TLS + #[arg(long)] + private_key: Option, + /// How long (in seconds) the internel HTTP client should wait for responses /// /// This number defaults to 30 diff --git a/src/config/file.rs b/src/config/file.rs index e75271f..fd9262c 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -118,6 +118,12 @@ pub(crate) struct Server { pub(crate) max_file_count: u32, pub(crate) temporary_directory: PathBuf, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) certificate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) private_key: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] diff --git a/src/lib.rs b/src/lib.rs index 6b1e602..8b506c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ mod serde_str; mod store; mod stream; mod sync; +mod tls; mod tmp_file; mod validate; @@ -77,6 +78,7 @@ use self::{ serde_str::Serde, store::{file_store::FileStore, object_store::ObjectStore, Store}, stream::{empty, once}, + tls::Tls, }; pub use self::config::{ConfigSource, PictRsConfiguration}; @@ -1840,14 +1842,16 @@ async fn launch_file_store std::io::Result<()> { +) -> color_eyre::Result<()> { let process_map = ProcessMap::new(); let address = config.server.address; spawn_cleanup(repo.clone(), &config); - HttpServer::new(move || { + let tls = Tls::from_config(&config); + + let server = HttpServer::new(move || { let tmp_dir = tmp_dir.clone(); let client = client.clone(); let store = store.clone(); @@ -1872,10 +1876,41 @@ async fn launch_file_store(certified_key); + + let handle = crate::sync::abort_on_drop(crate::sync::spawn("cert-reader", async move { + let mut interval = tokio::time::interval(Duration::from_secs(30)); + interval.tick().await; + + loop { + interval.tick().await; + + match tls.open_keys().await { + Ok(certified_key) => tx.update(certified_key), + Err(e) => tracing::error!("Failed to open keys {}", format!("{e}")), + } + } + })); + + let config = rustls_021::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_cert_resolver(rx); + + server.bind_rustls_021(address, config)?.run().await?; + + handle.abort(); + let _ = handle.await; + } else { + server.bind(address)?.run().await?; + } + + Ok(()) } async fn launch_object_store( @@ -1885,14 +1920,16 @@ async fn launch_object_store std::io::Result<()> { +) -> color_eyre::Result<()> { let process_map = ProcessMap::new(); let address = config.server.address; spawn_cleanup(repo.clone(), &config); - HttpServer::new(move || { + let tls = Tls::from_config(&config); + + let server = HttpServer::new(move || { let tmp_dir = tmp_dir.clone(); let client = client.clone(); let store = store.clone(); @@ -1917,10 +1954,41 @@ async fn launch_object_store(certified_key); + + let handle = crate::sync::abort_on_drop(crate::sync::spawn("cert-reader", async move { + let mut interval = tokio::time::interval(Duration::from_secs(30)); + interval.tick().await; + + loop { + interval.tick().await; + + match tls.open_keys().await { + Ok(certified_key) => tx.update(certified_key), + Err(e) => tracing::error!("Failed to open keys {}", format!("{e}")), + } + } + })); + + let config = rustls_021::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_cert_resolver(rx); + + server.bind_rustls_021(address, config)?.run().await?; + + handle.abort(); + let _ = handle.await; + } else { + server.bind(address)?.run().await?; + } + + Ok(()) } #[allow(clippy::too_many_arguments)] diff --git a/src/middleware/payload.rs b/src/middleware/payload.rs index ea72dc5..ab4c342 100644 --- a/src/middleware/payload.rs +++ b/src/middleware/payload.rs @@ -80,7 +80,7 @@ async fn drain(rx: flume::Receiver) { } if count > 0 { - tracing::info!("Drained {count} dropped payloads"); + tracing::debug!("Drained {count} dropped payloads"); } } @@ -135,7 +135,7 @@ impl Drop for DrainHandle { impl Drop for PayloadStream { fn drop(&mut self) { if let Some(payload) = self.inner.take() { - tracing::warn!("Dropped unclosed payload, draining"); + tracing::debug!("Dropped unclosed payload, draining"); if self.sender.try_send(payload).is_err() { metrics::counter!("pict-rs.payload.drain.fail-send").increment(1); tracing::error!("Failed to send unclosed payload for draining"); diff --git a/src/tls.rs b/src/tls.rs new file mode 100644 index 0000000..e225814 --- /dev/null +++ b/src/tls.rs @@ -0,0 +1,65 @@ +use std::path::PathBuf; + +pub(super) struct Tls { + certificate: PathBuf, + private_key: PathBuf, +} + +#[derive(Debug, thiserror::Error)] +enum TlsError { + #[error("Failed to read file")] + Io(#[from] std::io::Error), + + #[error("Failed to sign certificate")] + Sign(#[from] rustls_021::sign::SignError), + + #[error("No certificates found in certificate file")] + MissingCerts, + + #[error("No key found in private key file")] + MissingKey, +} + +impl Tls { + pub(super) fn from_config(config: &crate::config::Configuration) -> Option { + config + .server + .certificate + .as_ref() + .zip(config.server.private_key.as_ref()) + .map(|(cert, key)| Tls { + certificate: cert.clone(), + private_key: key.clone(), + }) + } + + pub(super) async fn open_keys(&self) -> color_eyre::Result { + let cert_bytes = tokio::fs::read(&self.certificate) + .await + .map_err(TlsError::from)?; + + let certs = rustls_pemfile::certs(&mut cert_bytes.as_slice()) + .map(|res| res.map(|c| rustls_021::Certificate(c.to_vec()))) + .collect::, _>>() + .map_err(TlsError::from)?; + + if certs.is_empty() { + return Err(TlsError::MissingCerts.into()); + } + + let key_bytes = tokio::fs::read(&self.private_key) + .await + .map_err(TlsError::from)?; + + let private_key = rustls_pemfile::private_key(&mut key_bytes.as_slice()) + .map_err(TlsError::from)? + .ok_or(TlsError::MissingKey)?; + + let private_key = rustls_021::sign::any_supported_type(&rustls_021::PrivateKey(Vec::from( + private_key.secret_der(), + ))) + .map_err(TlsError::from)?; + + Ok(rustls_021::sign::CertifiedKey::new(certs, private_key)) + } +}