Skip to content

Commit f9788b0

Browse files
authored
Merge pull request #826 from flavio/cert-reload
feat: automatic TLS certificate reload
2 parents 9c0ce5f + 5e94a62 commit f9788b0

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)