Compare commits

..
2 changed files with 4 additions and 92 deletions

View file

@ -563,34 +563,5 @@ in
# spelled out where the bundle is written above; no key path is ever
# exposed (an agent that could read one could mint trusted certs).
systemd.services.hive-c0re.environment.HIVE_TLS_CA_PATH = "${cfg.stateDir}/trust-bundle.pem";
# The same anchor, named for the swarm-queue clients that need it when
# they mint a token from authelia over TLS. Declared HERE, beside the
# bundle, rather than in each consumer's module: the path is this
# module's fact, and two consumers re-deriving `${stateDir}/…` would be
# two places to fix the day it moves.
#
# ⚠️ This is what was missing. `swarm-queue-client` built a bare
# `reqwest::Client`, so it trusted only the platform roots and died at
# `invalid peer certificate: UnknownIssuer` against a swarm whose
# authelia is signed by the swarm CA — while this very bundle sat on
# disk, already assembled, already handed to hive-c0re under a different
# variable name. The anchor was never missing; nothing pointed the queue
# client at it.
#
# Set unconditionally within this module's `active` guard, exactly like
# the line above: where there is no hive CA this module contributes
# nothing at all, and the clients then fall back to the platform roots —
# which is correct for a swarm fronted by a public certificate.
systemd.services.hive-c0re.environment.HIVE_C0RE_OIDC_CA_FILE = "${cfg.stateDir}/trust-bundle.pem";
# ⚠️ Gated, where the hive-c0re line above is not, and the asymmetry is
# the point: hive-c0re runs on every hive, the controller runs on one.
# Defining an environment key on a unit that does not exist CREATES a
# unit fragment for it — inert (no `ExecStart`, empty `wantedBy`, never
# activated) but present on every non-controller hive with a CA. Caught
# in review on this PR; it evaluates and builds clean either way, which
# is exactly why it needed a reviewer rather than a check.
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE =
lib.mkIf hyperhiveCfg.swarm.controller.enable "${cfg.stateDir}/trust-bundle.pem";
};
}

View file

@ -62,23 +62,6 @@ pub enum Error {
#[error("building the token-endpoint HTTP client")]
HttpClient(#[source] reqwest::Error),
/// Distinct from `HttpClient` because the operator's next move differs:
/// this one names a path they configured, and it fires before any
/// network call.
#[error("reading the token-endpoint CA certificate from {path}")]
CaFile {
path: String,
#[source]
source: std::io::Error,
},
#[error("parsing the token-endpoint CA certificate from {path} as PEM")]
CaParse {
path: String,
#[source]
source: reqwest::Error,
},
#[error("requesting an access token from authelia")]
TokenRequest(#[source] reqwest::Error),
@ -175,19 +158,6 @@ pub struct QueueConfig {
/// read here, and putting it in the environment would publish it to
/// anything that can read `/proc/<pid>/environ`.
pub client_secret_file: PathBuf,
/// Extra trust anchor for the token endpoint, when it is not signed by
/// a publicly-trusted CA.
///
/// Optional, and deliberately NOT part of the all-or-none group below: a
/// swarm fronted by a public certificate needs no extra anchor, and
/// making this required would break that deployment to fix ours. Absent
/// means "the platform's roots are enough", which is the correct default
/// for a client that might talk to anything.
///
/// ⚠️ Without it, a swarm using its own CA fails at TLS with
/// `invalid peer certificate: UnknownIssuer` — the anchor exists on the
/// host and this client simply never looked at it.
pub ca_file: Option<PathBuf>,
}
impl QueueConfig {
@ -212,14 +182,6 @@ impl QueueConfig {
let client_id = std::env::var(format!("{prefix}_OIDC_CLIENT_ID")).ok();
let secret = std::env::var(format!("{prefix}_OIDC_CLIENT_SECRET_FILE")).ok();
// Read outside the match on purpose: this one is INDEPENDENT of the
// all-or-none rule, so it must not participate in the tuple that
// decides whether the queue is configured at all. A CA path with no
// queue is meaningless rather than half-configured.
let ca_file = std::env::var(format!("{prefix}_OIDC_CA_FILE"))
.ok()
.map(PathBuf::from);
match (url, token_endpoint, client_id, secret) {
(None, None, None, None) => Ok(None),
(Some(url), Some(token_endpoint), Some(client_id), Some(secret)) => Ok(Some(Self {
@ -227,7 +189,6 @@ impl QueueConfig {
token_endpoint,
client_id,
client_secret_file: PathBuf::from(secret),
ca_file,
})),
// A partially-set environment is a deployment bug, and the failure
// it would otherwise produce is the expensive kind: the process
@ -312,30 +273,10 @@ pub async fn connect(cfg: QueueConfig) -> Result<async_nats::Client, Error> {
// no retry and nothing in the log to say why. Failing fast lets
// `async-nats` do what it already does well — back off and try again.
// 10s is generous for a form POST to a local IdP.
let mut builder = reqwest::Client::builder().timeout(std::time::Duration::from_secs(10));
// The swarm's own CA, when the token endpoint is signed by it. ADDED, not
// substituted: `add_root_certificate` extends the default set rather than
// replacing it, so a swarm can front authelia publicly and still have
// this work.
//
// Failing here rather than falling back to the platform roots is the
// point — an operator who named a CA file wants that anchor, and a
// silent fallback would turn their typo into `UnknownIssuer` five layers
// away, inside an auth callback, on a retry loop.
if let Some(path) = &cfg.ca_file {
let pem = std::fs::read(path).map_err(|source| Error::CaFile {
path: path.display().to_string(),
source,
})?;
let cert = reqwest::Certificate::from_pem(&pem).map_err(|source| Error::CaParse {
path: path.display().to_string(),
source,
})?;
builder = builder.add_root_certificate(cert);
}
let http = builder.build().map_err(Error::HttpClient)?;
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(Error::HttpClient)?;
let url = cfg.url.clone();
let client = async_nats::ConnectOptions::with_auth_callback(move |_nonce| {