cellos-supervisor 0.5.1

CellOS execution-cell runner — boots cells in Firecracker microVMs or gVisor, enforces narrow typed authority, emits signed CloudEvents.
Documentation
//! Composition root: wires host backend, secret broker, and event sinks from environment,
//! then hands control to [`Supervisor::run`].

mod command_runner;
mod composition;
#[allow(dead_code)]
mod destruction_evidence;
mod event_signing;
// `dns_proxy` is exercised by `lib.rs` consumers (tests + the dataplane
// module surface) and re-declared here so the binary build participates in
// the same module tree. The supervisor binary itself does not call
// `dns_proxy::run_one_shot` directly today — Phase 1 wires the activation
// predicate via `Supervisor::maybe_emit_dns_proxy_status` and exercises the
// proxy module end-to-end through the integration test under `tests/`.
// The `setns(2)` spawn that puts a real proxy thread inside the cell's
// network namespace is the runtime-integration follow-up slice.
#[allow(dead_code)]
mod dns_proxy;
// FC-38 Phase 2 — eBPF/nflog real-time per-flow listener (scaffolding).
// The binary does not call any of this module's surface today (Phase 2
// implementation is a future slice); the scaffold is reachable through
// `lib.rs` for unit tests. Mirrors the `dns_proxy` / `sni_proxy` pattern
// of feature-flagged modules that need to participate in the binary's
// module tree without contributing to the hot path.
mod ebpf_flow;
#[allow(dead_code)]
mod linux_cgroup;
mod linux_isolation;
#[cfg(target_os = "linux")]
mod linux_mount;
#[cfg(target_os = "linux")]
mod linux_net;
#[cfg(target_os = "linux")]
mod linux_seccomp;
mod network_policy;
mod nft_counters;
// Real-time per-flow events (nflog backend). Wired into
// `supervisor.rs::linux_run_cell_command_isolated` alongside dns_proxy and
// sni_proxy. Like those modules, the binary build only directly references
// a subset of the public surface; the rest is exercised through lib.rs +
// integration tests under `tests/`.
mod per_flow;
mod proxy_activation;
// SEC-21 Phase 3h.1 — `dns_proxy::dnssec` reuses `TrustAnchors` and
// `resolve_with_ttl_validated` from `resolver_refresh` (re-export, not
// copy), so the binary must include `resolver_refresh` in its module
// tree just like `lib.rs` does — without this, the binary build fails
// on `crate::resolver_refresh` paths in `dns_proxy/dnssec.rs`. The
// `#[allow(dead_code)]` mirrors the `dns_proxy` / `sni_proxy` pattern
// above: most of the surface is exercised through the lib by
// integration tests, not from `main.rs` directly.
#[allow(dead_code)]
mod resolver_refresh;
mod runtime_secret;
#[allow(dead_code)]
mod sni_proxy;
mod spec_input;
mod supervisor;
mod supervisor_helpers;
mod trust_keyset_load;
mod trust_plane_observability;

use std::ffi::OsString;
use std::path::PathBuf;

use anyhow::Context;
use cellos_core::{
    enforce_derivation_scope_policy, validate_execution_cell_document, verify_authority_derivation,
    ExecutionCellDocument,
};
use composition::{
    build_supervisor, emit_startup_banner, enforce_authority_derivation_requirement,
    enforce_deployment_profile, enforce_isolation_default, enforce_kubernetes_namespace_placement,
    enforce_telemetry_declared, load_authority_keys, resolve_deployment_mode,
};
use spec_input::{read_cell_spec, spec_sha256};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // HIGH-B5 (red-team wave-1, fixed wave-2.5): redacted filter on the fmt
    // layer suppresses reqwest/hyper TRACE events whose message bodies
    // include bearer tokens (the supervisor uses reqwest for OIDC token
    // fetch and Vault unwrap — both authorization-bearing flows).
    //
    // Output still goes to stderr — stdout stays clean for any structured
    // operator output a future subcommand prints. The I4 integration test
    // (`supervisor_i4_hardened_default`) asserts the banner lands on stderr.
    use tracing_subscriber::layer::SubscriberExt;
    use tracing_subscriber::util::SubscriberInitExt;
    use tracing_subscriber::Layer;

    let fmt_layer = tracing_subscriber::fmt::layer()
        .with_writer(std::io::stderr)
        .with_filter(cellos_core::observability::redacted_filter());

    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .with(fmt_layer)
        .init();

    // ── Deployment profile: collapse three independent hardening opt-ins
    //    (CELLOS_AUTHORITY_KEYS_PATH, CELL_OS_REQUIRE_JETSTREAM,
    //    CELLOS_REQUIRE_SCOPED_DERIVATION_TOKENS) into a single
    //    CELLOS_DEPLOYMENT_PROFILE=hardened decision. Fails fast if the
    //    profile's preconditions aren't met. Must run before any code that
    //    reads those env vars (broker selection, derivation scope policy).
    enforce_deployment_profile()?;

    // ── I4: hardened-vs-portable startup banner + CELLOS_REQUIRE_ISOLATION=1
    //    as the 1.0 default. Refuse to start without unshare/seccomp/cgroup
    //    support unless CELLOS_DEPLOYMENT_PROFILE=portable is named explicitly.
    let deployment_mode = resolve_deployment_mode()?;
    enforce_isolation_default(deployment_mode)?;
    emit_startup_banner(deployment_mode);

    let args: Vec<OsString> = std::env::args_os().skip(1).collect();
    if let Some(subcommand) = args.first().and_then(|s| s.to_str()) {
        match subcommand {
            "secret" => return runtime_secret::cli_main(&args[1..]).await,
            "--version" | "-V" => {
                println!("cellos-supervisor {}", env!("CARGO_PKG_VERSION"));
                return Ok(());
            }
            _ => {}
        }
    }

    // I4 — `--validate <spec>` runs spec parse + structural validation +
    // hardened/portable admission gates and exits 0 without entering the
    // supervisor run loop. Used by operators to lint a spec offline and by
    // CI to confirm a host's deployment-profile posture without launching a
    // workload.
    let validate_only = args
        .first()
        .and_then(|s| s.to_str())
        .map(|s| s == "--validate")
        .unwrap_or(false);
    let spec_arg_index = if validate_only { 1 } else { 0 };

    // ── Parse spec ────────────────────────────────────────────────────────
    let spec_path = args
        .get(spec_arg_index)
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("contracts/examples/execution-cell-minimal.valid.json"));

    let raw = read_cell_spec(&spec_path)?;
    let raw_spec_hash = spec_sha256(&raw);
    let doc: ExecutionCellDocument =
        serde_json::from_str(&raw).context("parse ExecutionCellDocument")?;
    validate_execution_cell_document(&doc).map_err(|e| anyhow::anyhow!("{e}"))?;

    // ── SEAM-3: when CELLOS_REQUIRE_AUTHORITY_DERIVATION is set, refuse to
    //    admit specs that declare egress or secrets without a derivation
    //    token. Runs after spec validation (which has no env access) and
    //    before signature verification, so the operator sees a clear
    //    "missing token" error instead of the spec slipping through into
    //    `build_supervisor`. Auto-enabled by `CELLOS_DEPLOYMENT_PROFILE=hardened`.
    enforce_authority_derivation_requirement(&doc)?;

    // ── F4a: when CELLOS_REQUIRE_TELEMETRY_DECLARED is set, refuse to admit
    //    specs that omit the `spec.telemetry` block. Auto-enabled by
    //    `CELLOS_DEPLOYMENT_PROFILE=hardened`. Runs immediately after the
    //    authority-derivation gate so operators see telemetry-intent
    //    enforcement on the same admission boundary.
    enforce_telemetry_declared(&doc)?;

    // ── T11-5: enforce Kubernetes-namespace placement. When
    //    `CELLOS_K8S_NAMESPACE` is set, any spec declaring
    //    `spec.placement.kubernetesNamespace` must match the runner's
    //    configured namespace exactly. Specs without that field still pass
    //    (a portable spec is admissible everywhere); unset env var disables
    //    the gate entirely.
    enforce_kubernetes_namespace_placement(&doc)?;

    // I4 — `--validate` exits here. The supervisor has now confirmed the
    // host's isolation posture (banner emitted), parsed + structurally
    // validated the spec, and applied the hardened/portable admission
    // gates. Anything past this point is run-loop wiring (build_supervisor
    // / supervisor.run), which is not what `--validate` should exercise.
    if validate_only {
        let validate_run_id = std::env::var("CELLOS_RUN_ID").unwrap_or_else(|_| "validate".into());
        let summary = composition::build_validation_summary(
            &doc,
            &validate_run_id,
            &raw_spec_hash,
            &spec_path,
        );
        println!(
            "{}",
            serde_json::to_string_pretty(&summary).unwrap_or_default()
        );
        return Ok(());
    }

    // ── Authority derivation: verify the grantor's ED25519 signature ──────
    // when a derivation token is attached. Keys come from
    // `CELLOS_AUTHORITY_KEYS_PATH` (operator-managed). Specs without a token
    // pass unchanged.
    let authority_keys = load_authority_keys()?;
    // I6 / O6: parentRunId scope policy. The 1.0 default is STRICT — a
    // token with `parentRunId: null` is rejected. Operators that need the
    // legacy permissive behaviour MUST opt out explicitly by setting
    // `CELLOS_REQUIRE_SCOPED_DERIVATION_TOKENS=0` (or `false`/`no`/`off`).
    // Permissive accepts emit a structured CloudEvent in `Supervisor::run`.
    let allow_universal = scoped_tokens_permissive_mode_enabled();
    let mut universal_token_accepted_in_permissive_mode = false;
    if let Some(token) = doc.spec.authority.authority_derivation.as_ref() {
        verify_authority_derivation(&doc.spec, token, &authority_keys)
            .map_err(|e| anyhow::anyhow!("{e}"))?;
        enforce_derivation_scope_policy(token, allow_universal)
            .map_err(|e| anyhow::anyhow!("{e}"))?;
        if allow_universal && token.parent_run_id.is_none() {
            universal_token_accepted_in_permissive_mode = true;
        }
    }

    // Lane B: hand the same loaded key map to the supervisor so the
    // `derivationVerified` field on `lifecycle.started` reflects the real
    // ED25519 verification result, not a structural-only stub. `Arc` so the
    // map is shared cheaply across the supervisor's lifetime.
    let authority_keys = std::sync::Arc::new(authority_keys);

    let run_id = std::env::var("CELLOS_RUN_ID").unwrap_or_else(|_| "run-local-001".into());

    // ── Build runtime wiring and run ─────────────────────────────────────
    let mut supervisor = build_supervisor(&doc, &run_id, authority_keys).await?;
    // I6: hand the supervisor the permissive-mode acceptance flag so it can
    // emit a single structured warning CloudEvent during `run()`.
    supervisor.universal_token_accepted_in_permissive_mode =
        universal_token_accepted_in_permissive_mode;
    supervisor.run(&doc, &run_id, Some(&raw_spec_hash)).await
}

/// I6 / O6: parse `CELLOS_REQUIRE_SCOPED_DERIVATION_TOKENS` with a
/// **strict-by-default** policy.
///
/// Returns `true` iff the operator EXPLICITLY opted out of strict mode by
/// setting the flag to one of: `0`, `false`, `no`, `off`. Any other value
/// (including unset, empty, `1`, `true`, `yes`, `on`) keeps the strict
/// default — universal (`parentRunId: null`) tokens are rejected.
fn scoped_tokens_permissive_mode_enabled() -> bool {
    match std::env::var("CELLOS_REQUIRE_SCOPED_DERIVATION_TOKENS") {
        Ok(raw) => {
            let t = raw.trim().to_ascii_lowercase();
            matches!(t.as_str(), "0" | "false" | "no" | "off")
        }
        Err(_) => false, // unset → strict (1.0 default)
    }
}