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:
parent
111c27e225
commit
72f5e39278
5 changed files with 171 additions and 10 deletions
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue