server/lib/src/twin_tls.rs
//! **The twin over mutual TLS 1.3 — holger's side of `nordisk-twin`** (stinky 14
//! §P3, 2026-09-27; gunnar's model, gunnar's code moved into the shared crate).
//!
//! Until this module a twin followed its primary over PLAIN http with an
//! optional bearer, and bound every door to loopback — so a twin on another
//! machine could follow nothing, and a remote recharge had nothing to dial
//! (stinky 14 §2 corrections 1 and 2). Now:
//!
//! * **the primary is its own replication issuer** — `nordisk_twin::pki`, key
//! `0600` under `<data_root>/`[`ISSUER_DIR`] (holger has no sealed secret
//! store; gunnar keeps its issuer key sealed under its KEK — when holger grows
//! one, the key moves there and nothing else changes). Never the installer CA:
//! the medium's bootstrap certificate chains to that one.
//! * **the replication door** — a second gRPC listener at the gRPC port + 1
//! ([`ReplicationDoorConfig`]), TLS 1.3 only, a client certificate chained to
//! the issuer REQUIRED at the handshake, serving only what a reader needs:
//! `RepositoryService` (the mirror's list + fetch) and `holger.estate.v1`, on a
//! read-only `HolgerGrpc` whose auth knows exactly two CNs ([`door_auth`]).
//! Every refused handshake is named (`holger-twin-12`) on the log and on
//! `GET /-/twin`. A config with no `replication_door` opens nothing, mints
//! nothing, and the box behaves exactly as before.
//! * **certify, driven by the front** — `BecomeTwin` → `TwinCsr` (the twin's key
//! is born on the twin) → `IssueTwinCertificate` (the primary's issuer, CN
//! forced, clientAuth, 365 d) → `InstallTwinCertificate` (our key? the CA
//! signed it? the CA pinned — re-pointing refused, renewal taken).
//! * **the follow dials mTLS** — [`channel`]: the first recorded door that
//! answers a 3 s TCP probe, the pinned CA, the twin's own certificate, the
//! door's fixed name [`DOOR_NAME`]. Plain http (+ `HOLGER_PRIMARY_BEARER`)
//! remains ONLY for an uncertified twin of a LOOPBACK primary — the in-process
//! tests; a twin of a remote primary with no credential refuses to follow in
//! the clear (`holger-twin-13`).
//! * **recharge from a remote twin** — the same three steps with purpose
//! `recharge`: a fresh main mints the request, the TWIN's door issuer signs it
//! (principal [`RECHARGE_PRINCIPAL`]), the main installs it and `Recharge`
//! pulls over the twin's door. The twin keeps that door on its configured
//! address whatever its role: it is the one door a twin must keep open.
//!
//! **The estate cut** (H2, HCUT 2026-10-26): the half that needs `nordisk-twin` (the
//! issuer, certify, the credential, the dialler and the door) is `twin_tls/mtls.rs`,
//! built with `estate`. Without it (the upstream and the downstream) this module is
//! the names, the config block and the pure rules, and `twin_tls/mtls_off.rs` answers:
//! no credential can be installed or read, and a declared door refuses the start.
//!
//! Nothing here holds a holger type in its core — the neutral half is in
//! `Skidbladnir/crates/installer/nordisk-twin`; this is directories, principal
//! names, the error codes and the auth map.
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use holger_errcode as ec;
use serde::{Deserialize, Serialize};
/// The door's name — the SAN every twin checks, whatever address it dialled.
pub const DOOR_NAME: &str = "holger-replication";
/// The CN of a twin following its primary.
pub const TWIN_PRINCIPAL: &str = "holger-twin";
/// The CN of a fresh main recharging from its twin.
pub const RECHARGE_PRINCIPAL: &str = "holger-recharge";
/// `<data_root>/` — this box's replication issuer and its door record.
pub const ISSUER_DIR: &str = "holger-replication-issuer";
/// `<data_root>/` — the twin's credential (following its primary).
pub const TWIN_DIR: &str = "holger-twin-tls";
/// `<data_root>/` — a fresh main's credential (reading its twin).
pub const RECHARGE_DIR: &str = "holger-recharge-tls";
/// In [`ISSUER_DIR`]: the door's bound port, written at every start, removed
/// when the door is off — what `IssueTwinCertificate` asks before it certifies.
pub const DOOR_PORT: &str = "door.port";
/// In [`ISSUER_DIR`]: the addresses the door answers on, in order.
pub const DOOR_HOSTS: &str = "door.hosts";
/// **Directories that are this BOX's identity** — never replicated (a twin
/// carrying its primary's issuer key could mint the primary's twins), never
/// "uncovered" in an estate report, and never what makes a box un-fresh (a box
/// opens its door, and so mints its issuer, on its FIRST start — before any
/// election could find it fresh). With them, the recharge markers: whether a
/// box was recharged is ITS fact — a twin carrying `holger-recharged` onward
/// would make the next main it recharges skip its own recharge.
///
/// The boot hold ([`crate::twin_follow::RECHARGE_HOLD`]) is the box's own the same way, and so
/// is its pull-channel enrolment (`fleet/`: the box key, the orders it took, its own jobs): a
/// re-laid main enrols at the ONE box before the recharge order it pulls can fill it, so that
/// enrolment must not make it un-fresh, and a twin must never carry the main's box key.
pub const BOX_OWN: &[&str] = &[
ISSUER_DIR,
TWIN_DIR,
RECHARGE_DIR,
crate::twin_follow::RECHARGED,
crate::twin_follow::RECHARGING,
crate::twin_follow::RECHARGE_HOLD,
FLEET_DIR,
];
/// The pull channel's directory under the data root (holger-fleet's `fleet/…`).
pub const FLEET_DIR: &str = "fleet";
/// The shared crate, for a caller that drives certify or dials a door with
/// holger's types in hand (the front, the tests).
#[cfg(feature = "estate")]
pub use nordisk_twin;
#[cfg(feature = "estate")]
mod mtls;
#[cfg(feature = "estate")]
pub use mtls::*;
#[cfg(not(feature = "estate"))]
mod mtls_off;
#[cfg(not(feature = "estate"))]
pub use mtls_off::*;
/// **The `replication_door:` block.** Absent ⇒ no door.
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ReplicationDoorConfig {
/// `host:port` to bind. Absent ⇒ the first exposed endpoint's CONFIGURED
/// gRPC host (never the twin's loopback rewrite) and its port + 1.
#[serde(default)]
pub ron_bind: Option<String>,
/// Addresses the door is reached at besides the bound one (the utility IP
/// when the door binds `0.0.0.0`, a tunnel address), in the reader's order
/// of preference; each goes into the door's SAN and to the reader.
#[serde(default)]
pub ron_advertise: Vec<String>,
}
/// Which credential slot a pairing step is about.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Purpose {
/// A twin following its primary.
Twin,
/// A fresh main reading its twin.
Recharge,
}
impl Purpose {
/// `"twin"` (or empty) / `"recharge"`; anything else refused by name.
pub fn parse(s: &str) -> Result<Purpose, String> {
match s.trim() {
"" | "twin" => Ok(Purpose::Twin),
"recharge" => Ok(Purpose::Recharge),
other => Err(ec::TWIN_PAIRING_REFUSED.tag(format!(
"purpose `{other}` is neither `twin` nor `recharge`"
))),
}
}
pub fn dir(self, root: &Path) -> PathBuf {
root.join(match self {
Purpose::Twin => TWIN_DIR,
Purpose::Recharge => RECHARGE_DIR,
})
}
pub fn principal(self) -> &'static str {
match self {
Purpose::Twin => TWIN_PRINCIPAL,
Purpose::Recharge => RECHARGE_PRINCIPAL,
}
}
}
/// **Where the door binds by default**: the configured gRPC host, its port + 1.
pub fn default_bind(configured_grpc: &str) -> Result<String, String> {
let addr: SocketAddr = configured_grpc.parse().map_err(|e| format!("the gRPC door `{configured_grpc}` is not host:port ({e}); declare replication_door.ron_bind"))?;
let port = addr
.port()
.checked_add(1)
.ok_or_else(|| format!("the gRPC port {} has no port + 1", addr.port()))?;
Ok(SocketAddr::new(addr.ip(), port).to_string())
}
/// The addresses the door answers on, in order: the advertised ones, then the
/// bound IP when it is a specific one.
pub fn door_hosts(bound: &SocketAddr, advertise: &[String]) -> Vec<String> {
let mut v: Vec<String> = advertise
.iter()
.map(|a| a.trim().to_string())
.filter(|a| !a.is_empty())
.collect();
if !bound.ip().is_unspecified() && !v.contains(&bound.ip().to_string()) {
v.push(bound.ip().to_string());
}
v
}
/// **The hosts a door is dialled at**: what its source reported, or — a door
/// bound on 0.0.0.0 reports none — the host of the control address the source
/// was asked at. The same rule as vm1's certify (holger-estate-front
/// `door_hosts_or_source`).
pub fn hosts_or_source(reported: Vec<String>, source_ctl: &str) -> Vec<String> {
if reported.iter().any(|h| !h.trim().is_empty()) {
return reported;
}
let host = source_ctl
.rsplit_once(':')
.map_or(source_ctl, |(h, _)| h)
.trim_matches(['[', ']']);
if host.is_empty() {
Vec::new()
} else {
vec![host.to_string()]
}
}
/// Write the door record (the listener is up).
pub fn record_door(root: &Path, port: u16, hosts: &[String]) -> Result<(), String> {
let dir = root.join(ISSUER_DIR);
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
std::fs::write(dir.join(DOOR_PORT), port.to_string())
.map_err(|e| format!("{}: {e}", dir.join(DOOR_PORT).display()))?;
std::fs::write(dir.join(DOOR_HOSTS), hosts.join(","))
.map_err(|e| format!("{}: {e}", dir.join(DOOR_HOSTS).display()))
}
/// Remove the door record (no door this start) — so no twin is certified for a
/// door that is not there.
pub fn clear_door(root: &Path) {
let _ = std::fs::remove_file(root.join(ISSUER_DIR).join(DOOR_PORT));
}
/// The recorded door, when this box serves one: `(port, hosts)`.
pub fn recorded_door(root: &Path) -> Option<(u16, Vec<String>)> {
let dir = root.join(ISSUER_DIR);
let port = std::fs::read_to_string(dir.join(DOOR_PORT))
.ok()?
.trim()
.parse()
.ok()?;
let hosts = std::fs::read_to_string(dir.join(DOOR_HOSTS))
.ok()
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|h| !h.is_empty())
.map(str::to_string)
.collect();
Some((port, hosts))
}
/// **The door's auth**: mTLS only, and exactly two principals — the twin and
/// the recharging main — each an admin-READ reader on a door that is read-only
/// by construction (`OperationMode::ReadOnly`, and only the read services are
/// mounted); every other CN `Denied`, every read authenticated. No bearer is
/// accepted here: the handshake already proved the issuer signed the peer.
pub fn door_auth() -> crate::auth::AuthConfig {
let mut roles = std::collections::HashMap::new();
roles.insert(TWIN_PRINCIPAL.to_string(), crate::auth::Role::Admin);
roles.insert(RECHARGE_PRINCIPAL.to_string(), crate::auth::Role::Admin);
crate::auth::AuthConfig {
methods: vec![crate::auth::AuthMethodConfig::Mtls {
ca_cert: String::new(),
}],
roles,
default_role: Some(crate::auth::Role::Denied),
require_auth_for_reads: true,
..Default::default()
}
}
/// What `IssueTwinCertificate` answers.
#[derive(Debug, Clone)]
pub struct Issued {
pub cert_pem: String,
pub issuer_pem: String,
pub port: u16,
pub hosts: Vec<String>,
pub not_after: i64,
}
/// Pure: **the new primary as this twin dials it.** A main reports the address
/// its control door is BOUND on (`RechargeStatus.control`, `0.0.0.0:50051`),
/// which names no host; the door this twin was just certified for names it
/// (`hosts[0]`), and the port stays the primary's. MEASURED by the
/// private-holger-ops estate-box gate (2026-10-04, U1): after a recharge the twin
/// was re-pointed at `0.0.0.0:50051`, and its next re-admission waited for ever on
/// "the pairing loop says twin of 10.6.0.14, and this box's role record says
/// `twin` of `0.0.0.0:50051`". A primary that names its host is kept as it is.
pub fn dialable(primary: &str, hosts: &[String]) -> String {
let p = primary.trim();
let (host, port) = match p.rsplit_once(':') {
Some((h, pt)) if pt.chars().all(|c| c.is_ascii_digit()) => (h.trim_matches(['[', ']']), pt),
_ => (p, ""),
};
let unspecified = host.is_empty()
|| host
.parse::<std::net::IpAddr>()
.map(|ip| ip.is_unspecified())
.unwrap_or(false);
match (
unspecified,
hosts.iter().map(|h| h.trim()).find(|h| !h.is_empty()),
) {
(true, Some(h)) if !port.is_empty() => format!("{h}:{port}"),
(true, Some(h)) => h.to_string(),
_ => p.to_string(),
}
}
/// True when `endpoint` (`http://host:port`, `host:port`) names a loopback host
/// — the one case a twin with no credential may follow in the clear.
pub fn is_loopback(endpoint: &str) -> bool {
let rest = endpoint
.split_once("://")
.map(|(_, r)| r)
.unwrap_or(endpoint);
let hostport = rest.split('/').next().unwrap_or(rest);
if let Ok(sa) = hostport.parse::<SocketAddr>() {
return sa.ip().is_loopback();
}
let host = hostport
.rsplit_once(':')
.map(|(h, _)| h)
.unwrap_or(hostport);
host == "localhost"
|| host
.parse::<std::net::IpAddr>()
.map(|ip| ip.is_loopback())
.unwrap_or(false)
}
/// Pure: **a peer that hung up before the handshake was whole** — a TCP probe, not a
/// refusal: nothing was presented, nothing was refused. nordisk-twin names it
/// `closed` only for a bare EOF; tokio-rustls wraps the same EOF as `tls handshake
/// eof`, so it arrived here as a `handshake` refusal: the twin's follow cycle put
/// `[holger-twin-12] handshake: … tls handshake eof` on its primary's log eight
/// times a minute, for ever — WARN spam by the estate's own machine (MEASURED by the
/// private-holger-ops estate-box gate, 2026-10-04). A client that refuses the door's
/// certificate sends an alert, and one without a certificate is refused by rustls by
/// name: both stay refusals.
pub fn is_probe(name: &str, why: &str) -> bool {
name == "closed" || (name == "handshake" && why.trim() == "tls handshake eof")
}
/// The refusal a twin with no credential and a remote source says, each cycle.
pub fn not_certified(source: &str) -> String {
ec::TWIN_NOT_CERTIFIED.tag(format!(
"no replication credential is installed and `{source}` is not on loopback: this box will not follow it in the clear. The front certifies it (BecomeTwin → TwinCsr → IssueTwinCertificate → InstallTwinCertificate); the next cycle dials the door"
))
}
#[cfg(test)]
mod tests {
/// ★ A twin re-pointed at a main that reports its BIND address follows the
/// host its new door names; a primary that names its host is kept.
#[test]
fn a_twin_is_re_pointed_at_an_address_it_can_dial() {
let hosts = vec!["10.6.0.14".to_string()];
assert_eq!(super::dialable("0.0.0.0:50051", &hosts), "10.6.0.14:50051");
assert_eq!(super::dialable("[::]:50051", &hosts), "10.6.0.14:50051");
assert_eq!(
super::dialable("10.6.0.14:50051", &["10.9.9.9".to_string()]),
"10.6.0.14:50051",
"a named host is kept"
);
assert_eq!(
super::dialable("0.0.0.0:50051", &[]),
"0.0.0.0:50051",
"nothing better to say: unchanged"
);
}
/// ★ A peer that hung up mid-handshake is a probe (no WARN); a real refusal stays one.
#[test]
fn an_eof_before_the_handshake_is_a_probe_and_a_refusal_stays_one() {
assert!(super::is_probe("closed", "early eof"));
assert!(
super::is_probe("handshake", "tls handshake eof"),
"tokio-rustls' spelling of the same EOF"
);
assert!(
!super::is_probe("handshake", "no handshake within 10s"),
"a peer that holds the handshake open is refused"
);
assert!(!super::is_probe("certificate", "peer sent no certificates"));
}
/// ★ `holger-server recharge` (GrpcBoxes) certifies against a door bound on
/// 0.0.0.0 that reports no host (MEASURED on the dwarf mock 2026-09-29,
/// [twin-bad-host]): the source's address stands in.
#[test]
fn a_door_with_no_reported_host_is_dialled_at_its_source() {
assert_eq!(
super::hosts_or_source(vec![], "10.13.8.112:50051"),
vec!["10.13.8.112".to_string()]
);
assert_eq!(
super::hosts_or_source(vec!["10.9.9.9".into()], "10.13.8.112:50051"),
vec!["10.9.9.9".to_string()]
);
}
use super::*;
#[test]
fn the_door_sits_beside_the_configured_grpc_door_and_names_its_hosts() {
assert_eq!(default_bind("0.0.0.0:50051").unwrap(), "0.0.0.0:50052");
assert_eq!(default_bind("10.13.8.2:50051").unwrap(), "10.13.8.2:50052");
assert!(
default_bind("grpc.holger.rs:50051").is_err(),
"a name is not a bind"
);
let any: SocketAddr = "0.0.0.0:50052".parse().unwrap();
assert!(
door_hosts(&any, &[]).is_empty(),
"0.0.0.0 is not an address a twin can dial"
);
assert_eq!(door_hosts(&any, &["10.44.0.2".into()]), ["10.44.0.2"]);
let one: SocketAddr = "10.13.8.2:50052".parse().unwrap();
assert_eq!(
door_hosts(&one, &["10.44.0.2".into()]),
["10.44.0.2", "10.13.8.2"]
);
for (e, lo) in [
("http://127.0.0.1:50051", true),
("127.0.0.2:1", true),
("http://localhost:1", true),
("http://10.13.8.2:50051", false),
("grpc.holger.rs:50051", false),
] {
assert_eq!(is_loopback(e), lo, "{e}");
}
assert_eq!(Purpose::parse("").unwrap(), Purpose::Twin);
assert!(Purpose::parse("admin")
.unwrap_err()
.contains(ec::TWIN_PAIRING_REFUSED.code));
}
}