Security Model — MVP 1.0
This document states, truthfully, what MVP 1.0 does and does not defend against. A release claim that exceeds this document is false and must be corrected before it ships.
Supported platform boundary
MVP 1.0 evidence comes from three distinct classes (HORO-1011 scope amendment; see ADR 0006). Compatibility is not protection — a platform being functionally supported never by itself means device-level enforcement is claimed for it:
- E1 — Fake/simulated (CI). Deterministic, no real accelerator, no device-level claim of any kind.
- E2 — Apple Silicon (physical MacBook Pro M3 Max). Real Metal
accelerator functional evidence only: discovery, observation,
attribution, and a full ALLOW/DENY application-level flow through a
real GPU compute path.
DeviceEnforce/DeviceRevokeareUnsupported/NotEvaluatedon this platform by definition — this class never claims system-wide or kernel-level GPU protection. - E3 — Bare-metal Linux + NVIDIA. The sole device-level enforcement
evidence class. No containers-as-the-enforcement-boundary claim, no
VMs. No AMD/Intel. This is the mandatory hard security gate
(HORO-841/844, F-M1-007) and is not replaceable by E2 evidence — an
Apple-only PASS is
BLOCKED ON E3, neverREADY.
Single-host, single-user-session scope for MVP 1.0 on every platform. Multi-tenant fleet/enterprise scenarios are explicitly future stages (HORO-777+).
Threat levels
Adapted for MVP 1.0’s Happy Path scope. Each level states what MVP 1.0 claims to resist, and what it explicitly does not yet resist.
No claim below is validated yet. No physical bare-metal Linux/NVIDIA hardware evidence exists as of this document’s initial version — every “Claimed: denied” here is the target F-M1-007 is built to, pending HORO-841’s hardware spike. Treat every claim in this section as provisional until §“Enforcement mechanism” below is updated with real hardware evidence.
- L1 — Unauthenticated/naive access. A workload with no authorization path at all attempting to use the protected GPU. Claimed (pending hardware validation): denied.
- L2 — Same-user, non-adversarial misuse. A legitimate local user
running a workload without going through the authorized launch path
(
eltanin run). Claimed (pending hardware validation): denied, on the supported enforcement boundary (new device-node opens after enforcement is active). - L3 — Same-user, mildly adversarial. A user or process attempting to work around the CLI (e.g. invoking the underlying binary directly, reusing an inherited file descriptor). Partially claimed — new opens are denied; behavior for already-open handles inherited across cgroup/process boundaries is exactly what HORO-841’s hardware spike must characterize before F-M1-007 claims anything here. Until that evidence exists, this document must not claim inherited-handle revocation.
- L4 — Privileged/root adversarial, kernel-level bypass, hardware attestation bypass. Explicitly not claimed for MVP 1.0. A root user can disable the agent, unload the eBPF program, or bypass enforcement outright. MVP 1.0 does not defend against a fully privileged local attacker; it defends against unauthorized use through the normal compute path by processes that do not already have root.
What is NOT security in this system
- A backend reporting
Capability::ControlledLaunchasSupportedis not a device-enforcement claim.ResourceCapabilities(HORO-1011, ADR 0006) evaluates each of nine capability dimensions independently; functional launch/observation support and device-level enforce/revoke support are separate dimensions with separateSupportStates precisely so one can never be silently read from the other. OnlyCapability::DeviceEnforce/DeviceRevokeatSupportState::Supported, backed by real E3 (Linux/NVIDIA) hardware evidence, is a device-level protection claim. - GPU utilization (“nvidia-smi shows activity”) is not authorization evidence and never overrides a deny decision.
- A previously-issued lease’s mere existence does not extend past its expiry, regardless of whether the underlying device handle is still technically open.
- Process metadata (PID, path, parent, UID) is a signal into workload
identity/provenance (F-M1-003), never a bypass of policy evaluation
(F-M1-004). Implemented as
eltanin_core::identity::Evidence<T>(HORO-831): every field carries an explicit source (kernel-observed / best-effort / self-asserted) and can beMissing/Unsupported— there is no type-level way to treat a field as “trusted” without inspecting its actual source, and PID reuse has defined semantics (WorkloadIdentity::compare_process) rather than an implicit assumption.
Local IPC trust boundary (F-M1-006, HORO-838)
The unprivileged client talking to the local authorization agent over
IPC is never trusted to assert its own identity or authorization
state. crates/eltanin-protocol’s wire types carry zero identity or
evidence fields — no request or response names a WorkloadIdentity,
ExecutionContext, or Evidence<T> — so there is no wire shape a client
could populate to claim a UID, executable path, or process ancestry
that overrides what the agent observes about the connecting peer itself.
Concretely (HORO-839): the agent reads the peer’s SO_PEERCRED
credential (effective uid/gid, via rustix’s safe wrapper — never
hand-written unsafe libc getsockopt) and cross-checks it against a
fresh /proc/<pid>/status read through eltanin-linux’s collector
before treating the peer as authorizable; the two are reconciled rather
than naively compared, since SO_PEERCRED reports the peer’s
effective uid while /proc/<pid>/status’s first Uid: field is the
real uid, and a setuid/setgid peer legitimately differs between
them. A client can name a resource/action (a request parameter
default-deny policy can only narrow, never widen) and a lease_id (a
lookup key, not a capability), and nothing else.
AgentResponse derives Deserialize, which structurally forbids
embedding ComputeLease, PolicyDecision, DecisionReason, or
LeaseValidity in any response — all four are Serialize-only by
construction elsewhere in this codebase. A client that fabricates a
LeaseGranted-shaped value has fabricated a display string; real
enforcement happens agent-side (cgroup device-BPF, F-M1-007), never by a
client presenting a response it holds. This closes the “no permanent
plaintext local bearer credential merely for convenience” requirement by
construction rather than by convention.
Discharged by HORO-840: ReleaseLease is a wire operation naming
another lease by id. eltanin-core’s LeaseIssuer::revoke alone only
checks issuer identity and sequence range — sequence numbers are
enumerable, so nothing in eltanin-core alone stops a client from naming
a lease it doesn’t own. eltanin-agent::authz::AuthorizationHandler‘s
release path calls LeaseIssuer::validate (which itself calls
compare_process and compare_executable, both required Same) before
ever calling revoke, and reports every non-Valid case — unknown
lease id, foreign issuer, cross-client mismatch, execve substitution,
already revoked, expired — identically as ReleaseOutcome::Refused, so
the wire response can never be used to enumerate other clients’ leases.
See docs/architecture/domain-model.md’s “Local authorization agent
integration” section and crates/eltanin-agent/tests/authz_release.rs.
Enforcement mechanism (validated, not assumed)
Linux cgroup v2 device-BPF (BPF_PROG_TYPE_CGROUP_DEVICE /
BPF_CGROUP_DEVICE) denies device-file access with EPERM. This is the
enforcement substrate — not CUDA API interception or LD_PRELOAD,
which are explicitly rejected as the security root (they are
user-space-bypassable by design).
The exact node/major/minor lifecycle for a supported NVIDIA GPU, and the
precise behavior for already-open handles, cgroup movement, and cleanup
on process/agent failure, must be proven on real hardware by HORO-841
before F-M1-007’s real enforcement implementation claims any of it.
No physical hardware evidence exists yet as of this document’s initial
version — see docs/development/campaign-state.md for current status.
This document will be updated with the actual validated boundary once
HORO-841 completes; until then, §“Threat levels” above states the
intended, not-yet-hardware-verified target.
Audit & explain evidence (F-M1-009, HORO-824) — eltanin-audit
Audit is evidence, not authority. crates/eltanin-audit writes one
append-only NDJSON record per AuthorizationHandler decision
(eltanin-agent::authz::event::EventSink), and eltanin-explain
(crates/eltanin-audit/src/bin/eltanin-explain.rs) reads that log back to
answer “what happened for this decision.” Neither side may become part of
the ALLOW/DENY/release decision path itself:
- The writer never reads.
eltanin-agentcalls onlyEventSink::record(&self, event) -> ()— a signature that returns nothing and cannot fail the caller.eltanin-agentnever callseltanin_audit::explain::{read_log, select, Selector}; this is mechanically enforced bycrates/eltanin-agent/tests/audit_reader_isolation.rs, which fails the build if any agent source file names the reader API. - A written record cannot become an authorization input. Every
authority-bearing type this crate mirrors (
ComputeLease,PolicyDecision,AuthorizationOutcome, …) is deliberatelySerialize-only or has noserdederive at all ineltanin-core— seedocs/architecture/domain-model.md’s “Compute Lease” and “Authorization policy” sections.eltanin-audit::recordmirrors each one with a hand-writtenRecorded*struct/enum that isDeserialize, but nothing in the codebase ever constructs aComputeLeaseorPolicyDecisionfrom aRecordedLease*/RecordedPolicyDecision— editing or replaying a log line changes whateltanin-explainreports, never what the agent authorizes. - Best-effort durability is an explicit MVP 1.0 limitation, by founder
decision. An audit write failure is reported to the caller
(
AuditSinkError) and logged to stderr byeltanin-agent’sAuditEventSinkadapter, and counted viaAuditFileSink::failed_writes()— but it never changes an already-computedAgentResponse. ReopeningEventSink::record’s-> ()contract to make audit I/O authoritative was explicitly rejected: an agent that denies protected compute because its disk is unhappy is a new, separately-designed failure mode, not an MVP 1.0 requirement.failed_writes()is a plain accessor, not wired into any wire-visibleAgentResponse/AgentStatusViewfield — reopening that QA-PASSed protocol schema (HORO-838/839) was equally out of scope for this ticket. A future mode such asrequire_durable_audit_before_commitmay make durability authoritative for deployments that need it, but is explicitly not implemented here. - Evidence-exists vs. not-found vs. possibly-lost is honestly
distinguishable, where it’s actually knowable.
AuditFileSink::appendreserves its sequence number before attempting the write and never reuses it on failure, so a failed write leaves a permanent, structural gap in the sequence space rather than a silently reused id.eltanin_audit::explain::LogScan::gapsdetects these gaps per issuer instance, ranging from sequence0(where every instance’s sequence space starts) up to the highest sequence actually observed;select()reportsSelectionResult::Found(the record exists),PossiblyLost(the id falls inside an observed gap — it may have failed to persist), orNotFoundotherwise — which ordinarily means never issued, but not always: an id beyond the highest sequence an instance was ever observed to reach also reportsNotFound, even if it was in fact reserved and lost, because nothing in a purely local log can tell “reserved then lost at the very end of the log” apart from “never issued” without an independent liveness signal. This is the honest limit of what a local, best-effort log can tell you: it cannot prove a record was never issued if persistence itself is what failed, and this limit is sharpest at an instance’s trailing edge. - Redaction / log-sensitivity contract. An audit record may contain
process ancestry, executable paths, and cgroup/namespace hints — the
same kernel-observed
ExecutionContextevidence the authorization decision itself used — but never a lease’s reusable authorization material, raw protected payload content, or secrets. Structurally: nocrates/eltanin-linuxcollector reads/proc/<pid>/cmdlineor/proc/<pid>/environ(onlystat/status/exe/cgroup), so workload argv/env can never reach a record via that evidence path in the first place; seecrates/eltanin-audit/tests/redaction.rs. The log file is created at mode0600(Unix) — seecrates/eltanin-audit/tests/sink_append.rs.
Regression coverage
(crates/eltanin-agent/tests/audit_sink.rs::an_audit_write_failure_never_changes_the_already_computed_response)
proves both a grant and a denial remain unaffected when every audit write
fails, via a deterministic always-failing Write implementation injected
through AuditEventSink::from_writer — filesystem-based fault injection
(deleting/replacing the log file under an open file descriptor) proved
unreliable across platforms and was rejected as the test mechanism.
Non-goals (explicit, not oversights)
Windows/macOS, AMD/Intel, hardware attestation, retroactive revoke of already-open GPU handles unless directly proven otherwise by HORO-841, production-grade L3/L4 bypass resistance, enterprise SSO/RBAC, SaaS control plane in the per-compute hot path. See HORO-772 for the full non-goal list.