fix(otel): stop delivering the hive's upstream token to agents
The host-side collector is the only path telemetry leaves a hive, so
HYPERHIVE_OTEL_HEADERS_CREDENTIAL is never emitted and everything
downstream of it is unreachable. What made it worth removing rather than
leaving inert is what it looked like to a reader: a complete,
well-commented mechanism for writing the hive's upstream credential into
a file the agent can read, described in the present tense. Anyone auditing
"can an agent obtain the OTEL token?" had to reconstruct the whole env-var
chain to find out the answer is no.
Gone: the per-agent `hyperhive.otel.headersCredential` option, the
`hive-otel-header` oneshot that merged OTEL_EXPORTER_OTLP_HEADERS into the
agent's own settings.json, and meta.rs's field, env read and render.
⚠️ Scoped by NAMESPACE, not by name. `hyperhive.otel.headersCredential`
(per-agent) and `services.hyperhive.otel.headersCredential` (host) are
different options sharing a leaf name — the host one is read by
`stats/otel_metrics.rs` for c0re's own container-resource exporter and
stays. Sweeping the string would have taken out working code.
The comment above `otelSettingsEnv` now states the property rather than
the absence: there is no auth header and no mechanism to add one, because
an agent exports to the hive's own collector and nothing an agent can read
is a secret to the swarm. The old behaviour is named in the past tense so
it reads as removed rather than overlooked.
meta.rs's assertions that pinned the injection are deleted rather than
adjusted; the surrounding test keeps covering extraResourceAttributes and
the endpoint/protocol injection, which are live.
This commit is contained in:
parent
a5ef31ed8b
commit
fbeff69fd7
3 changed files with 15 additions and 142 deletions
|
|
@ -130,42 +130,6 @@ 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
|
||||
}
|
||||
|
||||
/// Idempotently rewrite the lines in `/etc/nixos-containers/<container>.conf`
|
||||
/// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port
|
||||
/// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind).
|
||||
|
|
@ -218,9 +182,12 @@ 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();
|
||||
// No hive-wide secrets are forwarded into agent containers. hive-priv
|
||||
// still accepts a credential list (see `write_nspawn_flags`), but
|
||||
// nothing produces one: the only entry was the OTEL upstream token,
|
||||
// and an agent has no business holding the hive's credential for
|
||||
// anything outside it.
|
||||
let load_creds: Vec<CredentialMount> = Vec::new();
|
||||
|
||||
let mut binds: Vec<BindMount> = vec![
|
||||
BindMount {
|
||||
|
|
|
|||
|
|
@ -843,7 +843,6 @@ struct OtelConfig {
|
|||
endpoint: String,
|
||||
protocol: String,
|
||||
extra_resource_attributes: Option<String>,
|
||||
headers_credential: Option<String>,
|
||||
metric_interval_ms: Option<u64>,
|
||||
/// `HYPERHIVE_OTEL_DEBUG=1` → `hyperhive.otel.debug = true` →
|
||||
/// `CLAUDE_CODE_OTEL_DIAG_STDERR=1` in every agent's env.
|
||||
|
|
@ -867,9 +866,6 @@ fn otel_config() -> Option<OtelConfig> {
|
|||
let extra_resource_attributes = std::env::var("HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty());
|
||||
let headers_credential = std::env::var("HYPERHIVE_OTEL_HEADERS_CREDENTIAL")
|
||||
.ok()
|
||||
.filter(|v| !v.is_empty());
|
||||
let metric_interval_ms = std::env::var("HYPERHIVE_OTEL_METRIC_INTERVAL_MS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
|
|
@ -881,7 +877,6 @@ fn otel_config() -> Option<OtelConfig> {
|
|||
endpoint,
|
||||
protocol,
|
||||
extra_resource_attributes,
|
||||
headers_credential,
|
||||
metric_interval_ms,
|
||||
debug,
|
||||
})
|
||||
|
|
@ -1209,13 +1204,6 @@ where
|
|||
esc(attrs)
|
||||
);
|
||||
}
|
||||
if let Some(cred) = &otel.headers_credential {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" hyperhive.otel.headersCredential = \"{}\";",
|
||||
esc(cred)
|
||||
);
|
||||
}
|
||||
if let Some(ms) = otel.metric_interval_ms {
|
||||
// Int option — emit a bare numeric literal (no quotes). `ms` is a
|
||||
// parsed u64, so it can't inject anything into the rendered nix.
|
||||
|
|
@ -2251,7 +2239,6 @@ mod tests {
|
|||
};
|
||||
unsafe {
|
||||
std::env::remove_var("HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES");
|
||||
std::env::remove_var("HYPERHIVE_OTEL_HEADERS_CREDENTIAL");
|
||||
std::env::set_var("HYPERHIVE_OTEL_ENDPOINT", "https://c.example/otel");
|
||||
std::env::set_var("HYPERHIVE_OTEL_PROTOCOL", "grpc");
|
||||
}
|
||||
|
|
@ -2261,17 +2248,12 @@ mod tests {
|
|||
"HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES",
|
||||
"deployment.environment=prod",
|
||||
);
|
||||
std::env::set_var(
|
||||
"HYPERHIVE_OTEL_HEADERS_CREDENTIAL",
|
||||
"/run/secrets/otel-headers",
|
||||
);
|
||||
}
|
||||
let on_full = render();
|
||||
unsafe {
|
||||
std::env::remove_var("HYPERHIVE_OTEL_ENDPOINT");
|
||||
std::env::remove_var("HYPERHIVE_OTEL_PROTOCOL");
|
||||
std::env::remove_var("HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES");
|
||||
std::env::remove_var("HYPERHIVE_OTEL_HEADERS_CREDENTIAL");
|
||||
}
|
||||
let off = render();
|
||||
|
||||
|
|
@ -2292,10 +2274,6 @@ mod tests {
|
|||
!on_minimal.contains("hyperhive.otel.extraResourceAttributes"),
|
||||
"extraResourceAttributes must not appear when unset:\n{on_minimal}"
|
||||
);
|
||||
assert!(
|
||||
!on_minimal.contains("hyperhive.otel.headersCredential"),
|
||||
"headersCredential must not appear when unset:\n{on_minimal}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
on_full.contains(
|
||||
|
|
@ -2303,10 +2281,6 @@ mod tests {
|
|||
),
|
||||
"extraResourceAttributes must be injected when set:\n{on_full}"
|
||||
);
|
||||
assert!(
|
||||
on_full.contains("hyperhive.otel.headersCredential = \"/run/secrets/otel-headers\";"),
|
||||
"headersCredential must be injected when set:\n{on_full}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!off.contains("hyperhive.otel"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue