167 lines
5.4 KiB
Rust
167 lines
5.4 KiB
Rust
//! Watches Kubernetes TLS Secrets and loads certificates.
|
|
//!
|
|
//! Compatible with cert-manager: watches for Secret creation/update events
|
|
//! and parses `tls.crt` and `tls.key` into the ConfigStore for TLS termination.
|
|
//!
|
|
//! Key convention:
|
|
//! - `tls-secret:<secretName>` — the raw cert, cross-referenced by reconciler
|
|
//! via the `tls-host:<secretName>` mapping written by the ingress watcher.
|
|
//! - After reconciliation, the reconciler copies certs to `tls:<host>` for
|
|
//! direct SNI lookup by the proxy.
|
|
|
|
use futures::StreamExt;
|
|
use futures::pin_mut;
|
|
use gingress_proxy::config::{ConfigStore, TlsCert};
|
|
use kube::ResourceExt;
|
|
use kube::runtime::watcher::{self, Event};
|
|
use std::sync::Arc;
|
|
|
|
/// Watch Secrets of type `kubernetes.io/tls` and update the ConfigStore.
|
|
///
|
|
/// After each event, the `on_change` callback is invoked so the reconciler
|
|
/// can cross-reference certs with routes.
|
|
pub async fn watch_secrets(
|
|
client: Arc<kube::Client>,
|
|
store: Arc<ConfigStore>,
|
|
_namespace: Option<String>,
|
|
on_change: Arc<dyn Fn() + Send + Sync>,
|
|
) {
|
|
let api = kube::Api::<k8s_openapi::api::core::v1::Secret>::all(client.as_ref().clone());
|
|
|
|
let config = watcher::Config {
|
|
field_selector: Some("type=kubernetes.io/tls".to_string()),
|
|
..Default::default()
|
|
};
|
|
|
|
let secret_watcher = watcher::watcher(api, config);
|
|
pin_mut!(secret_watcher);
|
|
|
|
while let Some(event) = secret_watcher.next().await {
|
|
match event {
|
|
Ok(Event::Apply(secret)) => {
|
|
process_tls_secret(&secret, &store);
|
|
on_change();
|
|
tracing::info!(
|
|
name = %secret.name_any(),
|
|
namespace = %secret.namespace().unwrap_or_default(),
|
|
"TLS Secret applied"
|
|
);
|
|
}
|
|
Ok(Event::Init) => {
|
|
store.remove_prefix("tls-secret:");
|
|
tracing::info!("Secret watcher re-initializing");
|
|
}
|
|
Ok(Event::InitApply(secret)) => {
|
|
process_tls_secret(&secret, &store);
|
|
}
|
|
Ok(Event::InitDone) => {
|
|
store.signal_reload();
|
|
on_change();
|
|
tracing::info!("Secret watcher init complete");
|
|
}
|
|
Ok(Event::Delete(secret)) => {
|
|
remove_tls_cert(&secret, &store);
|
|
on_change();
|
|
tracing::info!(
|
|
name = %secret.name_any(),
|
|
"TLS Secret deleted, cert removed"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Secret watcher error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse a TLS secret and store the certificate.
|
|
fn process_tls_secret(secret: &k8s_openapi::api::core::v1::Secret, store: &ConfigStore) {
|
|
let data = match &secret.data {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
|
|
let cert_pem = match data
|
|
.get("tls.crt")
|
|
.and_then(|v| std::str::from_utf8(&v.0).ok())
|
|
{
|
|
Some(v) => v.to_string(),
|
|
None => {
|
|
tracing::warn!(name = %secret.name_any(), "TLS Secret missing tls.crt");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let key_pem = match data
|
|
.get("tls.key")
|
|
.and_then(|v| std::str::from_utf8(&v.0).ok())
|
|
{
|
|
Some(v) => v.to_string(),
|
|
None => {
|
|
tracing::warn!(name = %secret.name_any(), "TLS Secret missing tls.key");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let secret_name = secret.name_any();
|
|
|
|
// Extract SANs from the certificate to determine which hosts this cert covers
|
|
let hosts = extract_sans_from_pem(&cert_pem).unwrap_or_else(|| vec![secret_name.clone()]);
|
|
|
|
let tls_cert = TlsCert {
|
|
host: hosts.first().cloned().unwrap_or(secret_name.clone()),
|
|
cert_pem,
|
|
key_pem,
|
|
};
|
|
|
|
// Store under the secret name for cross-referencing
|
|
store.set(&format!("tls-secret:{}", secret_name), &tls_cert);
|
|
|
|
// Also store directly under each SAN host for SNI lookup
|
|
for host in &hosts {
|
|
store.set(&format!("tls:{}", host), &tls_cert);
|
|
}
|
|
|
|
store.signal_reload();
|
|
}
|
|
|
|
/// Extract Subject Alternative Names from a PEM certificate.
|
|
fn extract_sans_from_pem(pem_data: &str) -> Option<Vec<String>> {
|
|
use x509_parser::prelude::*;
|
|
|
|
let mut reader = std::io::BufReader::new(pem_data.as_bytes());
|
|
let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.ok()?;
|
|
let cert_der = certs.first()?;
|
|
let (_, cert) = X509Certificate::from_der(cert_der).ok()?;
|
|
|
|
let mut hosts: Vec<String> = Vec::new();
|
|
|
|
if let Ok(Some(san)) = cert.subject_alternative_name() {
|
|
for name in &san.value.general_names {
|
|
if let GeneralName::DNSName(dns) = name {
|
|
hosts.push(dns.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: use CN
|
|
if hosts.is_empty() {
|
|
if let Some(cn) = cert.subject().iter_common_name().next() {
|
|
hosts.push(cn.as_str().unwrap_or_default().to_string());
|
|
}
|
|
}
|
|
|
|
if hosts.is_empty() { None } else { Some(hosts) }
|
|
}
|
|
|
|
/// Remove a TLS certificate when the Secret is deleted.
|
|
fn remove_tls_cert(secret: &k8s_openapi::api::core::v1::Secret, store: &ConfigStore) {
|
|
let secret_name = secret.name_any();
|
|
store.remove(&format!("tls-secret:{}", secret_name));
|
|
// Also clean up tls-host mapping
|
|
store.remove(&format!("tls-host:{}", secret_name));
|
|
store.signal_reload();
|
|
}
|