feat(#1930): forward otel headers credential into agent containers via nspawn --load-credential

This commit is contained in:
damocles 2026-06-23 21:12:39 +02:00 committed by mara
commit 21ec7dc23d
6 changed files with 140 additions and 20 deletions

View file

@ -3,7 +3,7 @@
use std::path::Path;
use anyhow::{Context, Result, bail};
use hive_sh4re::priv_proto::BindMount;
use hive_sh4re::priv_proto::{BindMount, CredentialMount};
use tokio::process::Command;
use crate::coordinator::{AgentPaths, HiveEnv};
@ -1188,6 +1188,42 @@ fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
});
}
/// Hive-wide secrets forwarded into every agent container via nspawn
/// `--load-credential=<name>:<host_path>`. Currently just the OTEL
/// auth-header secret, when `services.hyperhive.otel.headersCredential`
/// is set (surfaced as `HYPERHIVE_OTEL_HEADERS_CREDENTIAL` on hive-c0re's
/// unit env — the same host option meta.rs reads to inject
/// `hyperhive.otel.headersCredential`). The inner harness unit reads it
/// via `LoadCredential=otel-headers` (inherit). The secret never lands in
/// a bind mount, the nix store, or the generated config.
///
/// A configured-but-missing file is skipped with a warning rather than
/// forwarded (nspawn would refuse to start the container otherwise): a
/// host-level secret typo shouldn't take down every agent's start; OTEL
/// just exports without the auth header until the file appears.
fn hive_load_credentials() -> Vec<CredentialMount> {
let mut out = Vec::new();
let Ok(path) = std::env::var("HYPERHIVE_OTEL_HEADERS_CREDENTIAL") else {
return out;
};
if path.is_empty() {
return out;
}
if std::path::Path::new(&path).is_file() {
out.push(CredentialMount {
name: "otel-headers".to_owned(),
host_path: path,
});
} else {
tracing::warn!(
%path,
"HYPERHIVE_OTEL_HEADERS_CREDENTIAL is set but the file is missing; \
skipping --load-credential (OTEL will export without the auth header)"
);
}
out
}
#[allow(
clippy::too_many_lines,
reason = "one contiguous nspawn-flag assembly block; the length is the flag \
@ -1234,6 +1270,10 @@ async fn set_nspawn_flags(
// is needed here — the bind alone is enough.
let claude_mount = container_claude_mount(agent_name);
// Hive-wide secrets forwarded into the container's credential store
// (currently just the OTEL auth-header). Same for every agent.
let load_creds = hive_load_credentials();
let mut binds: Vec<BindMount> = vec![
BindMount {
host_path: runtime_dir.to_string_lossy().into_owned(),
@ -1367,7 +1407,13 @@ async fn set_nspawn_flags(
(bad CIDR? prefix too narrow?); skipping PRIVATE_NETWORK write to \
avoid misconfigured isolation"
);
return crate::priv_client::write_nspawn_flags(container, &binds, None).await;
return crate::priv_client::write_nspawn_flags(
container,
&binds,
None,
&load_creds,
)
.await;
};
let Some(gateway_ip) = bridge_gateway_ip(&subnet) else {
tracing::warn!(
@ -1376,7 +1422,13 @@ async fn set_nspawn_flags(
skipping PRIVATE_NETWORK write to avoid an isolated container with no \
default route or resolver"
);
return crate::priv_client::write_nspawn_flags(container, &binds, None).await;
return crate::priv_client::write_nspawn_flags(
container,
&binds,
None,
&load_creds,
)
.await;
};
tracing::info!(
%agent_name, %agent_ip, %gateway_ip, %bridge,
@ -1393,7 +1445,7 @@ async fn set_nspawn_flags(
};
// Delegate the actual conf-file rewrite to hive-priv (runs as root).
crate::priv_client::write_nspawn_flags(container, &binds, isolation).await
crate::priv_client::write_nspawn_flags(container, &binds, isolation, &load_creds).await
}
/// Build the per-line callback for `create_container_streaming` /

View file

@ -8,8 +8,8 @@
use anyhow::{Context as _, Result, bail};
use hive_sh4re::priv_proto::{
BindMount, InfraAction, InfraContainer, JournalQuery, NetworkIsolation, PRIV_SOCK, PrivEvent,
PrivRequest, PrivResponse, PrivStream,
BindMount, CredentialMount, InfraAction, InfraContainer, JournalQuery, NetworkIsolation,
PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
@ -179,11 +179,13 @@ pub async fn write_nspawn_flags(
container: &str,
binds: &[BindMount],
isolation: Option<NetworkIsolation>,
load_credentials: &[CredentialMount],
) -> Result<()> {
ok(call(&PrivRequest::WriteNspawnFlags {
container: container.to_owned(),
binds: binds.to_vec(),
isolation,
load_credentials: load_credentials.to_vec(),
})
.await?)
}