From dc35743b7587f0b3450ee1ff1dd201db5a728aad Mon Sep 17 00:00:00 2001 From: Ryan Whitworth Date: Tue, 31 Mar 2026 22:04:35 -0400 Subject: [PATCH 1/3] Fail hard when /dev/urandom is unavailable for secret generation --- crates/openshell-bootstrap/src/runtime_apple.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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()) } From 382b8321b00cd4e125e8b70eef52e8cf811b12b0 Mon Sep 17 00:00:00 2001 From: Ryan Whitworth Date: Tue, 31 Mar 2026 22:05:17 -0400 Subject: [PATCH 2/3] Constrain CA pathlen to 0 and replace Docker SANs with Apple Container --- crates/openshell-bootstrap/src/pki.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/openshell-bootstrap/src/pki.rs b/crates/openshell-bootstrap/src/pki.rs index 107fea97..40670281 100644 --- a/crates/openshell-bootstrap/src/pki.rs +++ b/crates/openshell-bootstrap/src/pki.rs @@ -20,17 +20,13 @@ 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. /// @@ -48,7 +44,7 @@ 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 .distinguished_name .push(DnType::OrganizationName, "openshell"); From 170b47e39cee19b6d41a9a23f96d051f8cb1ee8d Mon Sep 17 00:00:00 2001 From: Ryan Whitworth Date: Tue, 31 Mar 2026 22:06:18 -0400 Subject: [PATCH 3/3] Set 365-day certificate validity instead of effectively-infinite --- crates/openshell-bootstrap/Cargo.toml | 1 + crates/openshell-bootstrap/src/pki.rs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) 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 40670281..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)] @@ -33,10 +34,12 @@ const DEFAULT_SERVER_SANS: &[&str] = &[ /// `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() @@ -45,6 +48,8 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { .into_diagnostic() .wrap_err("failed to create CA params")?; 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"); @@ -66,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"); @@ -82,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");