Skip to main content

rws_gateway_api/gateway_api/
backend_tls.rs

1//! BackendTLSPolicy: which Service ports rws reaches over TLS, trusting
2//! which CAs, under which name. A policy targets Services in its namespace
3//! (optionally one port, by name); between two policies for the same target
4//! the older wins and the other is `Conflicted`, and a port's own policy
5//! beats one for the whole Service. The client configuration is built once
6//! per distinct policy content and shared.
7
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex, OnceLock};
10
11use kube::ResourceExt;
12use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
13use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
14
15use super::resources::BackendTlsPolicy;
16use super::translate::Snapshot;
17use super::types::*;
18use crate::core::x509::SubjectAltName;
19
20/// How rws connects to a backend a policy covers.
21#[derive(Clone)]
22pub struct BackendTls {
23    /// Identifies the content (CAs, hostname, SANs): the pool key.
24    pub label: String,
25    pub config: Arc<rustls::ClientConfig>,
26    /// SNI, and the name verified when the policy lists no SANs.
27    pub server_name: String,
28    /// How the backend's certificate is checked (kept to add a client
29    /// certificate, [`BackendTls::with_client_cert`]).
30    verifier: Arc<dyn ServerCertVerifier>,
31}
32
33impl PartialEq for BackendTls {
34    fn eq(&self, other: &Self) -> bool {
35        self.label == other.label
36    }
37}
38
39impl std::fmt::Debug for BackendTls {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.debug_struct("BackendTls").field("label", &self.label).field("server_name", &self.server_name).finish()
42    }
43}
44
45/// A policy's verdict: accepted (with its configuration) or why not.
46#[derive(Clone, Debug, PartialEq)]
47pub struct Evaluated {
48    /// `Accepted`: `Ok`, or `Err((reason, message))`.
49    pub accepted: Result<(), (&'static str, String)>,
50    /// `ResolvedRefs`: `Ok`, or `Err((reason, message))`.
51    pub resolved: Result<(), (&'static str, String)>,
52    /// What connections use, when accepted.
53    pub tls: Option<BackendTls>,
54}
55
56/// Every policy's verdict, by (namespace, name), oldest first.
57pub fn evaluate(snapshot: &Snapshot) -> Vec<((String, String), Evaluated)> {
58    let mut policies: Vec<&BackendTlsPolicy> = snapshot.backend_tls_policies.iter().collect();
59    policies.sort_by_key(|p| (p.metadata.creation_timestamp.as_ref().map(|t| t.0.to_string()).unwrap_or_default(), p.namespace(), p.name_any()));
60    let mut claimed: Vec<(String, String, Option<String>)> = Vec::new();
61    policies
62        .into_iter()
63        .map(|policy| {
64            let namespace = policy.namespace().unwrap_or_default();
65            let mut evaluated = evaluate_one(policy, &namespace, snapshot);
66            let targets: Vec<(String, String, Option<String>)> = policy
67                .spec
68                .target_refs
69                .iter()
70                .filter(|t| t.group.is_empty() && t.kind == "Service")
71                .map(|t| (namespace.clone(), t.name.clone(), t.section_name.clone()))
72                .collect();
73            if evaluated.accepted.is_ok() {
74                if targets.iter().any(|t| claimed.contains(t)) {
75                    evaluated.accepted = Err(("Conflicted", "An older BackendTLSPolicy targets the same Service and section".to_string()));
76                    evaluated.tls = None;
77                } else {
78                    claimed.extend(targets);
79                }
80            }
81            ((namespace, policy.name_any()), evaluated)
82        })
83        .collect()
84}
85
86fn evaluate_one(policy: &BackendTlsPolicy, namespace: &str, snapshot: &Snapshot) -> Evaluated {
87    let validation = &policy.spec.validation;
88    let fail = |accepted: &'static str, resolved: Option<(&'static str, String)>, message: String| Evaluated {
89        accepted: Err((accepted, message.clone())),
90        resolved: resolved.map_or(Ok(()), Err),
91        tls: None,
92    };
93    let mut pem = Vec::new();
94    for reference in &validation.ca_certificate_refs {
95        if !reference.group.is_empty() || reference.kind != "ConfigMap" {
96            let message = format!("{} isn't a supported CA certificate reference", reference.kind);
97            return fail("NoValidCACertificate", Some(("InvalidKind", message.clone())), message);
98        }
99        let ca = snapshot
100            .config_maps
101            .iter()
102            .find(|c| c.namespace().as_deref() == Some(namespace) && c.name_any() == reference.name)
103            .and_then(|c| c.data.as_ref())
104            .and_then(|d| d.get("ca.crt"))
105            .filter(|ca| crate::core::x509::cert_info_from_pem(ca.as_bytes()).is_some());
106        let Some(ca) = ca else {
107            let message = format!("ConfigMap {namespace}/{} has no valid ca.crt", reference.name);
108            return fail("NoValidCACertificate", Some(("InvalidCACertificateRef", message.clone())), message);
109        };
110        pem.extend_from_slice(ca.as_bytes());
111        pem.push(b'\n');
112    }
113    let system = validation.well_known_ca_certificates.as_deref() == Some("System");
114    if pem.is_empty() && !system {
115        return fail("NoValidCACertificate", None, "Neither caCertificateRefs nor wellKnownCACertificates: System".to_string());
116    }
117    match build(if system && pem.is_empty() { None } else { Some(&pem) }, validation) {
118        Ok(tls) => Evaluated { accepted: Ok(()), resolved: Ok(()), tls: Some(tls) },
119        Err(e) => fail("NoValidCACertificate", Some(("InvalidCACertificateRef", e.clone())), e),
120    }
121}
122
123/// The policy for a Service port: the one naming the port, else the one for
124/// the whole Service. `None`: plain HTTP; `Some(Err)`: a policy covers it
125/// but isn't usable — requests fail rather than go out in the clear.
126pub fn for_service(evaluated: &[((String, String), Evaluated)], snapshot: &Snapshot, namespace: &str, service: &str, port_name: Option<&str>) -> Option<Result<BackendTls, String>> {
127    let targets = |section: Option<&str>| {
128        evaluated.iter().find(|(key, e)| {
129            let policy = snapshot.backend_tls_policies.iter().find(|p| p.namespace().as_deref() == Some(key.0.as_str()) && p.name_any() == key.1);
130            key.0 == namespace
131                && policy.is_some_and(|p| p.spec.target_refs.iter().any(|t| t.group.is_empty() && t.kind == "Service" && t.name == service && t.section_name.as_deref() == section))
132                && !matches!(&e.accepted, Err((reason, _)) if *reason == "Conflicted")
133        })
134    };
135    let (_, found) = port_name.and_then(|p| targets(Some(p))).or_else(|| targets(None))?;
136    Some(found.tls.clone().ok_or_else(|| found.accepted.as_ref().err().map(|(_, m)| m.clone()).unwrap_or_default()))
137}
138
139/// Client configurations by label, built once.
140fn cache() -> &'static Mutex<HashMap<String, BackendTls>> {
141    static CACHE: OnceLock<Mutex<HashMap<String, BackendTls>>> = OnceLock::new();
142    CACHE.get_or_init(Default::default)
143}
144
145/// The client configuration for `validation`, trusting `ca_pem` (the
146/// platform's roots when `None`).
147pub fn build(ca_pem: Option<&[u8]>, validation: &BackendTlsValidation) -> Result<BackendTls, String> {
148    use std::hash::{Hash, Hasher};
149    let mut hasher = std::collections::hash_map::DefaultHasher::new();
150    ca_pem.hash(&mut hasher);
151    validation.hostname.hash(&mut hasher);
152    for san in &validation.subject_alt_names {
153        (&san.san_type, &san.hostname, &san.uri).hash(&mut hasher);
154    }
155    let label = format!("backend-tls:{:016x}", hasher.finish());
156    if let Some(found) = cache().lock().unwrap_or_else(|e| e.into_inner()).get(&label) {
157        return Ok(found.clone());
158    }
159    let mut roots = rustls::RootCertStore::empty();
160    match ca_pem {
161        Some(pem) => {
162            use rustls::pki_types::pem::PemObject;
163            for cert in CertificateDer::pem_slice_iter(pem) {
164                roots.add(cert.map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
165            }
166        }
167        None => roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
168    }
169    let _ = crate::crypto_provider().install_default();
170    let webpki = rustls::client::WebPkiServerVerifier::builder(Arc::new(roots)).build().map_err(|e| e.to_string())?;
171    let verifier: Arc<dyn ServerCertVerifier> = if validation.subject_alt_names.is_empty() {
172        webpki
173    } else {
174        let sans = validation
175            .subject_alt_names
176            .iter()
177            .filter_map(|s| match s.san_type.as_str() {
178                "Hostname" => Some(SubjectAltName::Dns(s.hostname.to_ascii_lowercase())),
179                "URI" => Some(SubjectAltName::Uri(s.uri.clone())),
180                _ => None,
181            })
182            .collect();
183        Arc::new(SanVerifier { webpki, sans })
184    };
185    let config = crate::tls_client_builder().dangerous().with_custom_certificate_verifier(Arc::clone(&verifier)).with_no_client_auth();
186    let tls = BackendTls { label: label.clone(), config: Arc::new(config), server_name: validation.hostname.clone(), verifier };
187    cache().lock().unwrap_or_else(|e| e.into_inner()).insert(label, tls.clone());
188    Ok(tls)
189}
190
191impl BackendTls {
192    /// The same, presenting a client certificate (the Gateway's
193    /// `spec.tls.backend.clientCertificateRef`): PEM chain and key.
194    pub fn with_client_cert(&self, chain_pem: &[u8], key_pem: &[u8]) -> Result<BackendTls, String> {
195        use rustls::pki_types::pem::PemObject;
196        use std::hash::{Hash, Hasher};
197        let mut hasher = std::collections::hash_map::DefaultHasher::new();
198        (chain_pem, key_pem).hash(&mut hasher);
199        let label = format!("{}+client:{:016x}", self.label, hasher.finish());
200        if let Some(found) = cache().lock().unwrap_or_else(|e| e.into_inner()).get(&label) {
201            return Ok(found.clone());
202        }
203        let chain = CertificateDer::pem_slice_iter(chain_pem).collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
204        let key = rustls::pki_types::PrivateKeyDer::from_pem_slice(key_pem).map_err(|e| e.to_string())?;
205        let config = crate::tls_client_builder()
206            .dangerous()
207            .with_custom_certificate_verifier(Arc::clone(&self.verifier))
208            .with_client_auth_cert(chain, key)
209            .map_err(|e| e.to_string())?;
210        let tls = BackendTls { label: label.clone(), config: Arc::new(config), server_name: self.server_name.clone(), verifier: Arc::clone(&self.verifier) };
211        cache().lock().unwrap_or_else(|e| e.into_inner()).insert(label, tls.clone());
212        Ok(tls)
213    }
214}
215
216/// Verifies the chain with the trusted CAs, then that the certificate
217/// carries one of the policy's subjectAltNames (instead of the hostname).
218#[derive(Debug)]
219struct SanVerifier {
220    webpki: Arc<rustls::client::WebPkiServerVerifier>,
221    sans: Vec<SubjectAltName>,
222}
223
224/// Whether a certificate name (maybe `*.`) covers a wanted DNS name.
225fn dns_matches(certificate: &str, wanted: &str) -> bool {
226    let certificate = certificate.to_ascii_lowercase();
227    match certificate.strip_prefix("*.") {
228        Some(suffix) => wanted.split_once('.').is_some_and(|(label, rest)| !label.is_empty() && rest == suffix),
229        None => certificate == wanted,
230    }
231}
232
233impl ServerCertVerifier for SanVerifier {
234    fn verify_server_cert(
235        &self,
236        end_entity: &CertificateDer<'_>,
237        intermediates: &[CertificateDer<'_>],
238        server_name: &ServerName<'_>,
239        ocsp_response: &[u8],
240        now: UnixTime,
241    ) -> Result<ServerCertVerified, rustls::Error> {
242        // The chain must verify; only the name check is replaced.
243        match self.webpki.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) {
244            Ok(_) | Err(rustls::Error::InvalidCertificate(rustls::CertificateError::NotValidForName | rustls::CertificateError::NotValidForNameContext { .. })) => {}
245            Err(e) => return Err(e),
246        }
247        let names = crate::core::x509::subject_alt_names(end_entity.as_ref()).unwrap_or_default();
248        let matched = self.sans.iter().any(|wanted| {
249            names.iter().any(|name| match (wanted, name) {
250                (SubjectAltName::Dns(w), SubjectAltName::Dns(n)) => dns_matches(n, w),
251                (SubjectAltName::Uri(w), SubjectAltName::Uri(n)) => w == n,
252                _ => false,
253            })
254        });
255        if matched {
256            Ok(ServerCertVerified::assertion())
257        } else {
258            Err(rustls::Error::InvalidCertificate(rustls::CertificateError::NotValidForName))
259        }
260    }
261
262    fn verify_tls12_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &rustls::DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
263        self.webpki.verify_tls12_signature(message, cert, dss)
264    }
265
266    fn verify_tls13_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &rustls::DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
267        self.webpki.verify_tls13_signature(message, cert, dss)
268    }
269
270    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
271        self.webpki.supported_verify_schemes()
272    }
273}
274
275#[cfg(test)]
276mod tests;