diff --git a/crates/openshell-bootstrap/Cargo.toml b/crates/openshell-bootstrap/Cargo.toml index 87be3942..b66b12f1 100644 --- a/crates/openshell-bootstrap/Cargo.toml +++ b/crates/openshell-bootstrap/Cargo.toml @@ -15,6 +15,7 @@ libc = "0.2" miette = { workspace = true } rcgen = { workspace = true } serde = { workspace = true } +time = "0.3" serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/crates/openshell-bootstrap/src/pki.rs b/crates/openshell-bootstrap/src/pki.rs index 107fea97..675a0007 100644 --- a/crates/openshell-bootstrap/src/pki.rs +++ b/crates/openshell-bootstrap/src/pki.rs @@ -4,6 +4,7 @@ use miette::{IntoDiagnostic, Result, WrapErr}; use rcgen::{BasicConstraints, CertificateParams, DnType, Ia5String, IsCa, KeyPair, SanType}; use std::net::IpAddr; +use time::{Duration, OffsetDateTime}; /// All PEM-encoded materials produced by [`generate_pki`]. #[allow(clippy::struct_field_names)] @@ -20,27 +21,25 @@ pub struct PkiBundle { /// Default SANs always included on the server certificate. const DEFAULT_SERVER_SANS: &[&str] = &[ "openshell", - "openshell.openshell.svc", - "openshell.openshell.svc.cluster.local", "localhost", - "host.docker.internal", + "host.containers.internal", "127.0.0.1", ]; -/// SANs for the container bridge daemon certificate (macOS host). -/// -/// `host.containers.internal` is the hostname Apple Container VMs use to reach -/// the host, analogous to Docker's `host.docker.internal`. +/// SANs for the server certificate. `host.containers.internal` is the hostname +/// Apple Container VMs use to reach the macOS host. /// Generate a complete PKI bundle: CA, server cert, and client cert. /// /// `extra_sans` are additional Subject Alternative Names to add to the server /// certificate (e.g. the remote host's IP or hostname for remote deployments). /// -/// Certificate validity uses the `rcgen` defaults (1975–4096), which effectively -/// never expire. This is appropriate for an internal dev-cluster PKI where certs -/// are ephemeral to the cluster's lifetime. +/// Certificates are valid for 365 days from generation. Regenerate the PKI +/// bundle before expiry. pub fn generate_pki(extra_sans: &[String]) -> Result { + let now = OffsetDateTime::now_utc(); + let expiry = now + Duration::days(365); + // --- CA --- let ca_key = KeyPair::generate() .into_diagnostic() @@ -48,7 +47,9 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { let mut ca_params = CertificateParams::new(Vec::::new()) .into_diagnostic() .wrap_err("failed to create CA params")?; - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0)); + ca_params.not_before = now; + ca_params.not_after = expiry; ca_params .distinguished_name .push(DnType::OrganizationName, "openshell"); @@ -70,6 +71,8 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { .into_diagnostic() .wrap_err("failed to create server cert params")?; server_params.subject_alt_names = server_sans; + server_params.not_before = now; + server_params.not_after = expiry; server_params .distinguished_name .push(DnType::CommonName, "openshell-server"); @@ -86,6 +89,8 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { let mut client_params = CertificateParams::new(Vec::::new()) .into_diagnostic() .wrap_err("failed to create client cert params")?; + client_params.not_before = now; + client_params.not_after = expiry; client_params .distinguished_name .push(DnType::CommonName, "openshell-client"); diff --git a/crates/openshell-bootstrap/src/runtime_apple.rs b/crates/openshell-bootstrap/src/runtime_apple.rs index e3f6235c..6b08cd04 100644 --- a/crates/openshell-bootstrap/src/runtime_apple.rs +++ b/crates/openshell-bootstrap/src/runtime_apple.rs @@ -130,7 +130,7 @@ impl AppleContainerRuntime { let server_bin = find_server_binary()?; // Generate SSH handshake secret. - let secret = generate_secret(); + let secret = generate_secret()?; // Build server arguments. let db_url = format!("sqlite://{}/openshell.db", data_dir.display()); @@ -489,11 +489,11 @@ fn find_server_binary() -> Result { } /// Generate a random hex secret for SSH handshake HMAC. -fn generate_secret() -> String { +fn generate_secret() -> Result { use std::io::Read; let mut bytes = [0u8; 32]; std::fs::File::open("/dev/urandom") .and_then(|mut f| f.read_exact(&mut bytes)) - .unwrap_or_default(); - bytes.iter().map(|b| format!("{b:02x}")).collect() + .map_err(|e| miette::miette!("cannot read /dev/urandom for secret generation: {e}"))?; + Ok(bytes.iter().map(|b| format!("{b:02x}")).collect()) }