Skip to content

Commit 5e94a62

Browse files
committed
feat: automatic TLS certificate reload
When running on Linux, monitor changes done to the TLS certificate and key used by the https server. When both change, react by updating the TLS configuration. This allows to rotate the TLS certificate used by a Policy Server without having to restart the process. Signed-off-by: Flavio Castelli <fcastelli@suse.com>
1 parent 1677586 commit 5e94a62

File tree

4 files changed

+380
-13
lines changed

4 files changed

+380
-13
lines changed

Cargo.lock

+118-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+14-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ opentelemetry = { version = "0.23.0", default-features = false, features = [
2929
] }
3030
opentelemetry_sdk = { version = "0.23.0", features = ["rt-tokio"] }
3131
pprof = { version = "0.13", features = ["prost-codec"] }
32-
policy-evaluator = { git = "https://github.com/kubewarden/policy-evaluator", tag = "v0.18.1" }
32+
policy-evaluator = { git = "https://github.com/kubewarden/policy-evaluator", tag = "v0.18.2" }
3333
rustls-pki-types = { version = "1", features = ["alloc"] }
3434
rayon = "1.10"
3535
regex = "1.10"
@@ -55,9 +55,22 @@ jemalloc_pprof = "0.4.1"
5555
tikv-jemalloc-ctl = "0.5.4"
5656
rhai = { version = "1.19.0", features = ["sync"] }
5757

58+
[target.'cfg(target_os = "linux")'.dependencies]
59+
inotify = "0.10"
60+
tokio-stream = "0.1.15"
61+
5862
[dev-dependencies]
5963
mockall = "0.12"
6064
rstest = "0.21"
6165
tempfile = "3.10.1"
6266
tower = { version = "0.4", features = ["util"] }
6367
http-body-util = "0.1.1"
68+
69+
[target.'cfg(target_os = "linux")'.dev-dependencies]
70+
rcgen = { version = "0.13", features = ["crypto"] }
71+
openssl = "0.10"
72+
reqwest = { version = "0.12", default-features = false, features = [
73+
"charset",
74+
"http2",
75+
"rustls-tls-manual-roots",
76+
] }

src/lib.rs

+88-4
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@ use tokio::{
3939
};
4040
use tower_http::trace::{self, TraceLayer};
4141

42+
// This is required by certificate hot reload when using inotify, which is available only on linux
43+
#[cfg(target_os = "linux")]
44+
use tokio_stream::StreamExt;
45+
4246
use crate::api::handlers::{
4347
audit_handler, pprof_get_cpu, pprof_get_heap, readiness_handler, validate_handler,
4448
validate_raw_handler,
4549
};
4650
use crate::api::state::ApiServerState;
4751
use crate::evaluation::precompiled_policy::{PrecompiledPolicies, PrecompiledPolicy};
4852
use crate::policy_downloader::{Downloader, FetchedPolicies};
49-
use config::Config;
53+
use config::{Config, TlsConfig};
5054

5155
use tikv_jemallocator::Jemalloc;
5256

@@ -193,9 +197,7 @@ impl PolicyServer {
193197
});
194198

195199
let tls_config = if let Some(tls_config) = config.tls_config {
196-
let rustls_config =
197-
RustlsConfig::from_pem_file(tls_config.cert_file, tls_config.key_file).await?;
198-
Some(rustls_config)
200+
Some(create_tls_config_and_watch_certificate_changes(tls_config).await?)
199201
} else {
200202
None
201203
};
@@ -269,6 +271,88 @@ impl PolicyServer {
269271
}
270272
}
271273

274+
/// There's no watching of the certificate files on non-linux platforms
275+
/// since we rely on inotify to watch for changes
276+
#[cfg(not(target_os = "linux"))]
277+
async fn create_tls_config_and_watch_certificate_changes(
278+
tls_config: TlsConfig,
279+
) -> Result<RustlsConfig> {
280+
let cfg = RustlsConfig::from_pem_file(tls_config.cert_file, tls_config.key_file).await?;
281+
Ok(cfg)
282+
}
283+
284+
/// Return the RustlsConfig and watch for changes in the certificate files
285+
/// using inotify.
286+
/// When a both the certificate and its key are changed, the RustlsConfig is reloaded,
287+
/// causing the https server to use the new certificate.
288+
///
289+
/// Relying on inotify is only available on linux
290+
#[cfg(target_os = "linux")]
291+
async fn create_tls_config_and_watch_certificate_changes(
292+
tls_config: TlsConfig,
293+
) -> Result<RustlsConfig> {
294+
let cert_file = tls_config.cert_file.clone();
295+
let key_file = tls_config.key_file.clone();
296+
297+
let rust_config =
298+
RustlsConfig::from_pem_file(tls_config.cert_file, tls_config.key_file).await?;
299+
let reloadable_rust_config = rust_config.clone();
300+
301+
let inotify =
302+
inotify::Inotify::init().map_err(|e| anyhow!("Cannot initialize inotify: {e}"))?;
303+
let cert_watch = inotify
304+
.watches()
305+
.add(cert_file.clone(), inotify::WatchMask::MODIFY)
306+
.map_err(|e| anyhow!("Cannot watch certificate file: {e}"))?;
307+
let key_watch = inotify
308+
.watches()
309+
.add(key_file.clone(), inotify::WatchMask::MODIFY)
310+
.map_err(|e| anyhow!("Cannot watch key file: {e}"))?;
311+
312+
let buffer = [0; 1024];
313+
let stream = inotify
314+
.into_event_stream(buffer)
315+
.map_err(|e| anyhow!("Cannot create inotify event stream: {e}"))?;
316+
317+
tokio::spawn(async move {
318+
tokio::pin!(stream);
319+
let mut cert_changed = false;
320+
let mut key_changed = false;
321+
322+
while let Some(event) = stream.next().await {
323+
let event = match event {
324+
Ok(event) => event,
325+
Err(e) => {
326+
warn!("Cannot read inotify event: {e}");
327+
continue;
328+
}
329+
};
330+
331+
if event.wd == cert_watch {
332+
info!("TLS certificate file has been modified");
333+
cert_changed = true;
334+
}
335+
if event.wd == key_watch {
336+
info!("TLS key file has been modified");
337+
key_changed = true;
338+
}
339+
340+
if key_changed && cert_changed {
341+
info!("reloading TLS certificate");
342+
343+
cert_changed = false;
344+
key_changed = false;
345+
reloadable_rust_config
346+
.reload_from_pem_file(cert_file.clone(), key_file.clone())
347+
.await
348+
.expect("Cannot reload TLS certificate"); // we want to panic here
349+
}
350+
}
351+
});
352+
353+
Ok(rust_config)
354+
}
355+
272356
fn precompile_policies(
273357
engine: &wasmtime::Engine,
274358
fetched_policies: &FetchedPolicies,

0 commit comments

Comments
 (0)