Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/openshell-bootstrap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
27 changes: 16 additions & 11 deletions crates/openshell-bootstrap/src/pki.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -20,35 +21,35 @@ 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<PkiBundle> {
let now = OffsetDateTime::now_utc();
let expiry = now + Duration::days(365);

// --- CA ---
let ca_key = KeyPair::generate()
.into_diagnostic()
.wrap_err("failed to generate CA key")?;
let mut ca_params = CertificateParams::new(Vec::<String>::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");
Expand All @@ -70,6 +71,8 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
.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");
Expand All @@ -86,6 +89,8 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
let mut client_params = CertificateParams::new(Vec::<String>::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");
Expand Down
8 changes: 4 additions & 4 deletions crates/openshell-bootstrap/src/runtime_apple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -489,11 +489,11 @@ fn find_server_binary() -> Result<PathBuf> {
}

/// Generate a random hex secret for SSH handshake HMAC.
fn generate_secret() -> String {
fn generate_secret() -> Result<String> {
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())
}
Loading