server/lib/tests/config_boot_posture.rs
//! Boot-time config posture: the checks holger makes BEFORE it serves a byte.
//!
//! These drive the real `read_ron_config` → `validate_auth_posture` /
//! `validate_profile` path (no listeners, no sockets), because that is the path a
//! `holger-server start` takes and the one an operator's config file meets.
//!
//! Each test here is the RED-when-broken proof for one finding of the 2026-09
//! configuration review:
//!
//! * **unbound OIDC** — an `Oidc` method with no resolvable JWKS cannot verify a
//! JWT offline, so `nornir-auth-oidc` falls back to `/userinfo`, which checks
//! neither `iss` nor `aud`. Every auth example holger ships has this shape.
//! * **critical static-bearer** — "another method is configured" is not "another
//! method this build can dispatch".
//! * **strict keys** — a mistyped RON key used to be skipped in silence, so a
//! config that reads as `Critical` booted as `Standard`.
use holger_server_lib::read_ron_config;
fn load(ron: &str) -> (tempfile::TempDir, holger_server_lib::Holger) {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("h.ron");
std::fs::write(&path, ron).unwrap();
let h = read_ron_config(&path).expect("RON must parse");
(tmp, h)
}
fn load_err(ron: &str) -> String {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("h.ron");
std::fs::write(&path, ron).unwrap();
// `Holger` is not `Debug`, so unwrap the Result by hand rather than `expect_err`.
match read_ron_config(&path) {
Ok(_) => panic!("this config must be refused, but it parsed:\n{ron}"),
Err(e) => format!("{e:#}"),
}
}
// ─── Finding 1: a configured OIDC deployment silently stops checking audience ──
/// The EXACT shape every shipped auth example uses (`.nornir/auth-guide.md`,
/// `.nornir/auth-rbac-design.md`, `.nornir/auth-design.md`,
/// `.nornir/auth-for-idiots.md`): issuer + audience, no key material.
fn oidc_example_config(extra: &str) -> String {
format!(
r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
auth: Some((
methods: [ Oidc(issuer_url: "https://keycloak.example/realms/holger", audience: Some("holger")) ],
require_auth_for_reads: true,
)),
audit: (dir: "a", enabled: false),
{extra}
)"#
)
}
#[test]
fn the_shipped_oidc_example_shape_is_refused() {
// Only meaningful where the provider is actually linked; a tier that pinned no
// OIDC dispatches the method to nothing (fail-closed) and the refusal would be
// noise — see `auth::refuse_unbound_oidc`.
if !cfg!(feature = "auth-oidc") {
return;
}
let (_tmp, h) = load(&oidc_example_config(""));
let err = h
.validate_auth_posture()
.expect_err("issuer+audience with no JWKS must refuse to boot");
let msg = format!("{err:#}");
assert!(
msg.contains("https://keycloak.example/realms/holger"),
"the refusal must name the issuer: {msg}"
);
assert!(
msg.contains("userinfo") && msg.contains("aud"),
"the refusal must say WHAT is lost: {msg}"
);
assert!(
msg.contains("auth_allow_unbound_oidc"),
"the refusal must name the way out: {msg}"
);
}
/// A `jwks_uri` ALONE boots, and that is a deliberate line.
///
/// It is a CONDITIONAL hazard, unlike a method with no key material at all: a
/// correct, reachable endpoint verifies properly and pins issuer and audience, and
/// it degrades only when the fetch yields nothing. Refusing it would also refuse
/// holger's own main flow — every marketplace tenant is provisioned with exactly
/// this shape — and a control that refuses the product's primary configuration is
/// a control that gets switched off. It is warned about at every boot instead; the
/// durable fix is in edda's `jwks_resolvable`.
#[test]
fn a_jwks_uri_alone_boots_because_the_hazard_is_conditional() {
let ron = r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
auth: Some((
methods: [ Oidc(
issuer_url: "https://keycloak.example/realms/holger",
audience: Some("holger"),
jwks_uri: Some("https://keycloak.example/realms/holger/protocol/openid-connect/certs"),
) ],
require_auth_for_reads: true,
)),
audit: (dir: "a", enabled: false),
)"#;
let (_tmp, h) = load(ron);
h.validate_auth_posture()
.expect("a configured key endpoint must not stop the server booting");
}
/// The shape an operator should land in: a pre-provisioned key file, with the
/// `jwks_uri` kept beside it for refresh. This is what the refusal recommends, so
/// it has to actually boot.
#[test]
fn a_pre_provisioned_key_file_beside_a_jwks_uri_boots() {
let dir = tempfile::tempdir().unwrap();
let keys = dir.path().join("jwks.json");
std::fs::write(&keys, r#"{"keys":[]}"#).unwrap();
let ron = format!(
r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
auth: Some((
methods: [ Oidc(
issuer_url: "https://keycloak.example/realms/holger",
audience: Some("holger"),
jwks_path: Some("{}"),
jwks_uri: Some("https://keycloak.example/realms/holger/protocol/openid-connect/certs"),
) ],
require_auth_for_reads: true,
)),
audit: (dir: "a", enabled: false),
)"#,
keys.display()
);
let (_tmp, h) = load(&ron);
h.validate_auth_posture()
.expect("a readable key file binds iss + aud — this must boot on every tier");
}
/// A pre-provisioned inline JWKS is the airgap path and binds in every tier.
#[test]
fn an_inline_jwks_boots_in_every_tier() {
let ron = r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
auth: Some((
methods: [ Oidc(
issuer_url: "https://keycloak.example/realms/holger",
audience: Some("holger"),
jwks: Some("{\"keys\":[]}"),
) ],
require_auth_for_reads: true,
)),
audit: (dir: "a", enabled: false),
)"#;
let (_tmp, h) = load(ron);
h.validate_auth_posture()
.expect("an inline key set binds the token with no network at all");
}
/// ★ A34-1: **the hatch no longer reopens `/userinfo`.** nornir-auth-oidc 0.2
/// never sends a JWT to `/userinfo`, so an Oidc method with no key source
/// authenticates nobody: holger REFUSES it, hatch or no hatch, and says why.
#[test]
fn a34_1_the_hatch_no_longer_lets_an_unbound_method_boot() {
if !cfg!(feature = "auth-oidc") {
return;
}
let (_tmp, h) = load(&oidc_example_config("auth_allow_unbound_oidc: true,"));
let err = h
.validate_auth_posture()
.expect_err("an Oidc method that can verify no token must refuse to boot");
let msg = format!("{err:#}");
assert!(
msg.contains("https://keycloak.example/realms/holger"),
"names the issuer: {msg}"
);
assert!(
msg.contains("REFUSING TO START"),
"a refusal, not a warning: {msg}"
);
}
/// ★ A34-1: a `jwks_uri` holger cannot verify with (not https) is REFUSED, not
/// warned about: the provider refuses an http key endpoint, so the method
/// would authenticate nobody.
#[test]
fn a34_1_a_jwks_uri_that_is_not_https_is_refused() {
if !cfg!(all(feature = "auth-oidc", feature = "online")) {
return;
}
let ron = r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
auth: Some((
methods: [ Oidc(
issuer_url: "https://keycloak.example/realms/holger",
audience: Some("holger"),
jwks_uri: Some("http://keycloak.example/realms/holger/protocol/openid-connect/certs"),
) ],
require_auth_for_reads: true,
)),
audit: (dir: "a", enabled: false),
)"#;
let (_tmp, h) = load(ron);
let err = h
.validate_auth_posture()
.expect_err("an http jwks_uri cannot verify a token and must refuse to boot");
let msg = format!("{err:#}");
assert!(msg.contains("https"), "says what is wrong: {msg}");
assert!(
msg.contains("REFUSING TO START"),
"a refusal, not a warning: {msg}"
);
}
/// ★ A34-1/A34-4: an Oidc method with NO audience is refused: no token can be
/// bound to holger, so the provider refuses every JWT.
#[test]
fn a34_1_an_oidc_method_without_an_audience_is_refused() {
if !cfg!(feature = "auth-oidc") {
return;
}
let ron = r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
auth: Some((
methods: [ Oidc(
issuer_url: "https://keycloak.example/realms/holger",
jwks: Some("{\"keys\":[]}"),
) ],
require_auth_for_reads: true,
)),
audit: (dir: "a", enabled: false),
)"#;
let (_tmp, h) = load(ron);
let err = h
.validate_auth_posture()
.expect_err("an Oidc method with no audience binds no token to holger");
let msg = format!("{err:#}");
assert!(
msg.contains("audience"),
"names the missing audience: {msg}"
);
}
/// A config with no `auth` block at all is the open airgap-mirror posture and is
/// none of this check's business.
#[test]
fn no_auth_block_is_untouched() {
let ron = r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "g", ron_url: "127.0.0.1:1") ],
storage_endpoints: [],
audit: (dir: "a", enabled: false),
)"#;
let (_tmp, h) = load(ron);
h.validate_auth_posture()
.expect("no auth configured, nothing to refuse");
}
// ─── Finding 2: Critical's static-bearer check counted an undispatchable method ─
/// A `Critical` config whose only non-bearer method is one this build did not pin
/// has exactly ONE working mechanism — the shared secret — and must be refused.
///
/// This is the END-TO-END shape (RON file → `validate_profile`), and it only has
/// teeth on a tier that omits a provider — which is the shipped `holger-server`
/// (`mini`: bearer + mTLS, no OIDC/OAuth2), not the dev default, where all four are
/// pinned and the config below really is two-mechanism. So it self-skips here and
/// runs under `--no-default-features --features embedded`. The build-independent RED
/// proof of the rule itself is
/// `auth::boot_posture_tests::an_unpinned_alternative_does_not_count_as_a_second_mechanism`,
/// which injects the pinned-predicate instead of reading `cfg!`.
#[test]
fn critical_refuses_a_bearer_plus_an_unpinned_provider() {
if cfg!(feature = "auth-oauth2") {
return;
}
let ron = r#"(
repositories: [],
exposed_endpoints: [
(ron_name: "g", ron_url: "127.0.0.1:1",
ron_tls: Some((cert: "c.pem", key: "k.pem", client_ca: Some("ca.pem"), crl: Some("crl.pem")))),
],
storage_endpoints: [],
auth: Some((
methods: [
StaticToken(subject: "ci", token: Some("secret")),
OAuth2(introspection_url: "https://idp.example/introspect", client_id: "holger"),
],
require_auth_for_reads: true,
)),
audit: (dir: "a", enabled: true),
security_profile: Critical,
)"#;
let (_tmp, h) = load(ron);
let err = h
.validate_profile()
.expect_err("an unpinned provider is not a second mechanism");
let msg = format!("{err:#}");
assert!(
msg.contains("static bearer must not be the only"),
"got: {msg}"
);
assert!(
msg.contains("oauth2") && msg.contains("auth-oauth2"),
"the refusal must name the unpinned provider and the feature that would fix it: {msg}"
);
}
/// The contrast: mTLS is pinned in every holger tier, so bearer + mTLS really is
/// two mechanisms and `Critical` validates.
#[test]
fn critical_accepts_a_bearer_plus_a_pinned_provider() {
if !cfg!(feature = "auth-mtls") {
return;
}
let ron = r#"(
repositories: [],
exposed_endpoints: [
(ron_name: "g", ron_url: "127.0.0.1:1",
ron_tls: Some((cert: "c.pem", key: "k.pem", client_ca: Some("ca.pem"), crl: Some("crl.pem")))),
],
storage_endpoints: [],
auth: Some((
methods: [
StaticToken(subject: "ci", token: Some("secret")),
Mtls(ca_cert: "ca.pem"),
],
require_auth_for_reads: true,
default_role: Some(Reader),
)),
audit: (dir: "a", enabled: true),
security_profile: Critical,
)"#;
let (_tmp, h) = load(ron);
h.validate_profile()
.expect("bearer + a DISPATCHABLE mTLS method is two mechanisms");
}
// ─── Finding 3: a mistyped RON key was silently ignored ────────────────────────
/// The headline case: the operator writes `Critical`, the typo drops it, and the
/// node boots `Standard` with every fail-closed gate unreached. Now it stops.
#[test]
fn a_typo_in_security_profile_is_refused_not_skipped() {
let msg = load_err(
r#"(
repositories: [],
exposed_endpoints: [],
storage_endpoints: [],
security_profil: Critical,
)"#,
);
assert!(
msg.contains("security_profil"),
"the refusal must name the offending key: {msg}"
);
}
/// Same shape one level down, where the Critical profile's audit requirement lives.
#[test]
fn a_typo_inside_the_audit_block_is_refused() {
let msg = load_err(
r#"(
repositories: [],
exposed_endpoints: [],
storage_endpoints: [],
audit: (dir: "a", enable: false),
)"#,
);
assert!(
msg.contains("enable"),
"the refusal must name the key: {msg}"
);
}
/// A typo'd escape hatch must NOT silently read as "off" either — otherwise the
/// hatch added for finding 1 would grow finding 3's failure mode.
#[test]
fn a_typo_in_the_oidc_escape_hatch_is_refused() {
let msg = load_err(&oidc_example_config("auth_allow_unbound_oid: true,"));
assert!(
msg.contains("auth_allow_unbound_oid"),
"the refusal must name the key: {msg}"
);
}
/// Every key holger DOES know still parses — the strict reading must not have
/// narrowed the accepted config surface.
#[test]
fn the_full_known_key_set_still_parses() {
let ron = r#"(
repositories: [],
exposed_endpoints: [],
storage_endpoints: [],
auth: None,
audit: (dir: "a", enabled: false),
security_profile: Standard,
operation_mode: ReadOnly,
ron_plugin_dir: None,
auth_allow_unbound_oidc: false,
)"#;
let (_tmp, h) = load(ron);
assert_eq!(h.operation_mode, holger_server_lib::OperationMode::ReadOnly);
assert_eq!(
h.security_profile,
holger_server_lib::SecurityProfile::Standard
);
}
/// And the config the product itself renders — the dev pair nornir launches —
/// still reads under the strict rules. A renderer that drifts from the struct is
/// exactly what `deny_unknown_fields` would turn into a boot failure in the field.
#[test]
fn the_rendered_dev_pair_config_still_parses_strictly() {
let tmp = tempfile::tempdir().unwrap();
let ron = holger_server_lib::dev_pair_ron(tmp.path(), "127.0.0.1:18443", "127.0.0.1:18444");
let path = tmp.path().join("dev-pair.ron");
std::fs::write(&path, &ron).unwrap();
read_ron_config(&path).expect("holger's own rendered config must survive its own strictness");
}
/// **The `auth` block is strict** (Holger G0, 2026-10-23). The leaf (edda
/// `nornir-auth-policy` 0.1.5) denies unknown fields, so a misspelled control such as
/// `require_auth_for_read` (for `require_auth_for_reads`) stops the boot by name
/// instead of leaving the read gate off.
#[test]
fn the_auth_block_is_strict() {
let ron = r#"(
repositories: [],
exposed_endpoints: [],
storage_endpoints: [],
auth: Some((
methods: [],
require_auth_for_read: true,
)),
audit: (dir: "a", enabled: false),
)"#;
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), ron).unwrap();
let err = match holger_server_lib::read_ron_config(tmp.path()) {
Err(e) => e.to_string(),
Ok(_) => panic!("a typo inside the auth block must be refused"),
};
assert!(
err.contains("require_auth_for_read"),
"names the key: {err}"
);
}
// ─── Finding 4 (2026-09-21, lane H2): a token file nobody ever writes ─────────
/// **The estate's own emitted config, reduced to the line that matters.**
///
/// `private-holger-ops` `cargo xtask estate emit` writes
/// `StaticToken(subject: "console", token_file: Some("/var/lib/holger/admin-token"))`
/// into `deploy/generated/{server,twin,front}/holger.ron`, and **no code path in
/// that repository — or in any installer — ever creates that file**; two printed
/// plan steps ask a human to do it. Measured against the shipped binary on
/// 2026-09-21 the result was total silence:
///
/// ```text
/// Starting Holger "…/deploy/generated/server/holger.ron"
/// gRPC server listening on 0.0.0.0:50051 …
/// Holger is running. Press Ctrl+C to stop.
/// ```
///
/// A box that authenticates nobody, reporting itself healthy. Now `start` stops
/// at the posture check and says which file is missing.
#[test]
fn the_estates_emitted_bearer_refuses_when_nothing_ever_wrote_the_token_file() {
if !cfg!(feature = "auth-bearer") {
return;
}
let missing = tempfile::tempdir().unwrap();
let path = missing.path().join("admin-token");
let ron = format!(
r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "doors", ron_url: "127.0.0.1:1", ron_http_url: Some("127.0.0.1:2")) ],
storage_endpoints: [],
auth: Some((
methods: [ StaticToken(subject: "console", token_file: Some("{}")) ],
require_auth_for_reads: false,
)),
audit: (dir: "a", enabled: false),
)"#,
path.display()
);
let (_tmp, h) = load(&ron);
let err = h
.validate_auth_posture()
.expect_err("a declared bearer with no secret authenticates nobody");
let msg = format!("{err:#}");
assert!(msg.contains("console"), "the subject must be named: {msg}");
assert!(
msg.contains(&path.display().to_string()),
"the file the operator must write must be named: {msg}"
);
assert!(msg.contains("REFUSING TO START"), "got: {msg}");
}
/// **Not vacuous**: the same config with the token file PRESENT boots. This is
/// the half that proves the refusal above is about the missing bytes and not
/// about the method being declared at all.
#[test]
fn the_same_config_boots_once_the_token_file_holds_a_secret() {
if !cfg!(feature = "auth-bearer") {
return;
}
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("admin-token");
std::fs::write(&path, "0123456789abcdef0123456789abcdef\n").unwrap();
std::fs::set_permissions(
&path,
<std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o600),
)
.unwrap();
let ron = format!(
r#"(
repositories: [],
exposed_endpoints: [ (ron_name: "doors", ron_url: "127.0.0.1:1", ron_http_url: Some("127.0.0.1:2")) ],
storage_endpoints: [],
auth: Some((
methods: [ StaticToken(subject: "console", token_file: Some("{}")) ],
require_auth_for_reads: false,
)),
audit: (dir: "a", enabled: false),
)"#,
path.display()
);
let (_tmp, h) = load(&ron);
h.validate_auth_posture()
.expect("a bearer whose file holds a secret is a method that works");
}