Watch
0
0
Fork
You've already forked hyperhive
0

hive-tls: stage bao pki root in every hive's trust bundle

A self-signed hive fronting no swarm service name never filled
`$svcroot` (/var/lib/hive-tls/swarm-services-root.pem): the only writer
was `swarm-services-cert`'s issuance, which exits early on
`want_svc=0`. So those hives' trust-bundle.pem lacked the store's `pki`
root, even though every other hive's swarm-service names chain to it.

On `want_svc=0`, `swarm-services-cert` now reads the root from the
store's unauthenticated `GET /v1/pki/ca/pem`, over the hive's existing
BAO_ADDR, `deploy.bao.serverCaFile` and reader identity
(`deploy.bao.clientCertFile`/`clientKeyFile`) at the TLS layer only; no
login. It writes `$svcroot` and restarts `hive-tls-ca` when the root
changed, as the issuing path already does. The daily
`swarm-services-cert-renew` timer runs the same script, so the copy
follows a root rotation.

The gateway no longer `Requires=` `swarm-services-cert` on such a host:
nginx reads nothing that unit writes there, and the fetch retries
against a sealed store.

`hivectl peer-config` prints the root as step 2 of the self-signed
instructions: the swarm's service names are issued under it, not the
swarm root.

No new option. Refs #4588.
This commit is contained in:
atlas 2026-09-30 21:23:18 +02:00 • committed by mara
commit 72f5e39278
5 changed files with 171 additions and 10 deletions

View file

@ -37,6 +37,12 @@ const HIVE_TLS_TRUST_BUNDLE_PATH: &str = "/var/lib/hive-tls/trust-bundle.pem";
/// the per-hive CA pinning.
const SWARM_CA_ROOT_PATH: &str = "/var/lib/swarm-ca/root.pem";
/// Host path of the secret store's `pki` root (hive-tls.nix's
/// `$svcroot`, folded into the trust bundle). The swarm's service names
/// are issued under it, so a peer trusting only the swarm root above
/// cannot validate them.
const STORE_PKI_ROOT_PATH: &str = "/var/lib/hive-tls/swarm-services-root.pem";
/// Attrset key for a hive in `services.hyperhive.swarm.hives`, derived
/// from its domain's first DNS label.
///
@ -180,11 +186,29 @@ pub(crate) fn wg_peer(domain: &str, pubkey: &str, address: &str, endpoint: Optio
println!(" # the key must be that hive's services.hyperhive.hiveName");
}
/// The `peer-config` step carrying the secret store's `pki` root: the
/// certificate itself, or where it will appear when this hive has not
/// fetched it yet.
fn store_root_step(pem: Option<&str>) -> String {
const HEAD: &str = "# 2. trust the SECRET STORE's pki root on the peer host (once, not per hive);\n\
# the swarm's service names are issued under it:\n";
match pem.map(str::trim_end).filter(|p| !p.is_empty()) {
Some(p) => format!(
"{HEAD}{p}\n\
# (a self-signed hive in this swarm fetches it itself, into {STORE_PKI_ROOT_PATH})\n"
),
None => format!(
"{HEAD}# not on this host yet: swarm-services-cert.service writes it to\n\
# {STORE_PKI_ROOT_PATH}; rerun once that unit has succeeded\n"
),
}
}
/// `peer-config` — print the `swarm.hives."<name>"` block a peer
/// operator pastes to federate with THIS hive, plus the swarm-root
/// install step when this hive serves a self-signed chain. Reads local
/// state only (the TLS trust bundle's presence + the wg key); prints,
/// never mutates.
/// operator pastes to federate with THIS hive, plus the swarm-root and
/// store-root steps when this hive serves a self-signed chain. Reads
/// local state only (the TLS trust bundle, the store root + the wg key);
/// prints, never mutates.
pub(crate) fn peer_config(domain: &str, wg_address: Option<&str>, wg_endpoint: Option<&str>) {
let self_signed = Path::new(HIVE_TLS_TRUST_BUNDLE_PATH).exists();
let key = hive_key(domain);
@ -202,7 +226,10 @@ pub(crate) fn peer_config(domain: &str, wg_address: Option<&str>, wg_endpoint: O
println!("scp {SWARM_CA_ROOT_PATH} <peer-host>:{SWARM_CA_ROOT_PATH}");
println!("# (skip if that host already has the swarm root)");
println!();
println!("# 2. paste into the peer hive's NixOS config:");
let store_root = std::fs::read_to_string(STORE_PKI_ROOT_PATH).ok();
print!("{}", store_root_step(store_root.as_deref()));
println!();
println!("# 3. paste into the peer hive's NixOS config:");
} else {
println!("# paste into the peer hive's NixOS config:");
}
@ -243,3 +270,30 @@ pub(crate) fn wg_status() -> Result<()> {
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{STORE_PKI_ROOT_PATH, store_root_step};
const PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----";
/// The certificate is printed whole, on lines of its own, so a peer
/// operator can copy it out of the output as a PEM file.
#[test]
fn store_root_is_printed_verbatim() {
let s = store_root_step(Some(&format!("{PEM}\n")));
assert!(s.contains(&format!("\n{PEM}\n")), "{s}");
}
/// A hive that has not fetched the root yet says where it will be,
/// rather than printing an empty step.
#[test]
fn missing_store_root_names_the_path_and_the_unit() {
for pem in [None, Some(""), Some("\n")] {
let s = store_root_step(pem);
assert!(s.contains(STORE_PKI_ROOT_PATH), "{s}");
assert!(s.contains("swarm-services-cert.service"), "{s}");
assert!(!s.contains("BEGIN CERTIFICATE"), "{s}");
}
}
}