Please do not open a public GitHub issue for security vulnerabilities.
Report security issues by emailing the maintainers directly or via GitHub's private security advisory feature: https://github.com/marcuspat/Sentinel/security/advisories/new
Include:
- Description of the vulnerability
- Steps to reproduce
- Affected versions
- Proposed fix (optional)
You will receive a response within 5 business days. We target a fix within 30 days for critical/high findings and 90 days for medium/low.
| Version | Supported |
|---|---|
| 0.2.x | Yes |
| 0.1.x | No — several of the issues fixed in 0.2.0 are exploitable; upgrade |
Sentinel is designed to operate with elevated system privileges. Its security model relies on multiple layers of defense:
- Deny-by-default: All requests are denied unless explicitly allowed by a matching rule.
- Kill switch: When activated, blocks all capabilities regardless of risk tier or kind. This is an emergency stop — it does not pass ReadOnly requests through.
- Resource guards: System directories (/etc, /boot, /sys, /proc, /dev, /bin, /sbin, /lib,
/lib64, /usr/bin, /usr/sbin, /usr/lib, /run/systemd) and critical services (sshd/ssh,
systemd and systemd-*, docker, containerd) are protected from mutating capabilities by
default. Guards check every string argument at any depth, after lexical path normalisation
(
..,//,.) and with unit suffixes removed (sshd.service→sshd). They do not resolve symlinks; the Landlock profile is the control for those. - Policy files (ADR-018):
--policy FILE/$SENTINEL_POLICY. In the defaulttightenmode a file can only make a decision stricter (allowis rejected at load time);mode = "replace"is the only way to loosen policy. The kill switch and the built-in guards stay in force in both modes. Parsing is strict, a bad file stops the command, and a file writable by group or other is refused. - Risk tiers: Low, Medium, High, Critical — with distinct routing (allow / require-approval / deny).
- Time restrictions: Evaluated against server wall-clock (
Utc::now()), not client-supplied timestamps.
Every production path runs commands through HardenedExecutor (ADR-016):
- Exact-match allowlist: only the programs the built-in capabilities spawn
(
BUILTIN_COMMANDS). No shells, interpreters orsudo./tmp/evil/lsdoes not match"ls". - No shell: Commands are spawned directly via
tokio::process::Commandwith explicit argument vectors — no shell expansion. - Timeout + output cap: 15-minute default with SIGTERM → SIGKILL; a child is also killed when the step that started it is cancelled.
- rlimits: file descriptors (256), core dumps disabled.
no_new_privs: children cannot gain privilege via setuid/setgid binaries or file capabilities.- Clean environment: children inherit nothing. They get a fixed
PATHand the C locale, so allowlisted names always resolve to system binaries;LD_*,PATHand similar overrides are refused. - No network: a seccomp filter denies Internet and raw sockets to every command except the package managers (Linux x86_64 / aarch64).
- Landlock filesystem confinement (Linux ≥ 5.13): each command a built-in capability
spawns may write only where that capability needs to — nothing for read-only
capabilities, the target directory for
log_vacuumandcache_prune.package_upgradeis unconfined by design.SENTINEL_LANDLOCK=requirefails closed on kernels without Landlock; the default warns and continues. See ADR-016 for the full table.
- PID guard: ProcessKill rejects PID <= 1. PID 1 (init/systemd) cannot be signalled.
- Signal allowlist: Only TERM, KILL, HUP, INT, QUIT, USR1, USR2, CONT, STOP are accepted.
- Path validation: LogVacuum and CachePrune validate paths against a blocked prefix list before executing find/rm operations.
- Hash chain: SHA-256 chained log — every event includes the hash of the prior event.
Tampering (deletion, reordering, modification) is detectable via
sentinel verify-audit. - Signed checkpoints (opt-in, ADR-015): the hash chain is unkeyed, so on its own it
cannot detect a log that was rewritten wholesale with a consistent chain. Set
SENTINEL_AUDIT_KEY(fromsentinel audit-keygen) to sign the chain head after every event; verify withsentinel verify-audit PATH --pubkey KEY --require-signature. Keep the key readable only by a dedicated user, and ship<log>.sigoff-host if you need to detect truncation of log and signatures together. - Atomic writes: Each JSONL line is written in a single
write_allcall to reduce the crash-window for partial writes.
- One executor (ADR-019):
sentinel run, the TUI andsentinel executeshare it. Policy is re-evaluated for every step, aFailureresult halts the plan, and completed steps are rolled back in reverse order through the same policy check (--no-rollbackopts out). A rollback is audited only when it succeeded.
- No approve or execute tool (ADR-013): an MCP client can list, dry-run policy, run read-only capabilities and propose plans. Approval needs an interactive terminal.
- Content hash: a plan modified after approval is refused.
- Locked store: per-plan
flock, an atomicApproved → Executingclaim andfsynced writes, so an approved plan cannot be executed twice.
sentinel fleet runs one capability on many hosts over SSH (ADR-022):
- Policy twice: evaluated per host on the controller, then again on each host under
that host's own policy by
sentinel agent-exec. Nothing is dispatched when approval is required and--approveis absent. The run is audited. - Input validation: host specs, users, key paths and capability ids are validated;
sshis called with--before the destination and every part of the remote command is quoted. - mTLS is not in this path. The mutual-TLS and certificate-pinning code described in ADR-008 exists in the crate (pins are compared in constant time) and is exercised only by its tests.
- Policy gate: Every capability invocation in every phase (investigation and execution)
passes through
PolicyEvaluator::evaluatebefore running. - Approval gate: Plan execution requires
plan.approvalto be set to an approved state via an explicit call beforeexecute_planwill proceed. - Untrusted output (ADR-014): capability output is wrapped in a fence carrying a fresh random nonce and the fence marker is neutralised inside the data, so output cannot close or forge the fence. This lowers the odds of prompt injection; the policy and approval gates are what bound its effect.
- Native tool use (ADR-017): with Anthropic and api.openai.com the model requests
capabilities through the provider's tool-call channel, validated against each
capability's argument schema; free text is not parsed for requests on that path.
SENTINEL_NATIVE_TOOLS=offrestores the text protocol. - Deadlines and budgets (ADR-020): 120 s per attempt, at most 4 attempts, and a per-session budget (default 100 calls, 2,000,000 tokens). Response bodies are capped.
- Capability ID validation: LLM-supplied capability IDs are validated against
[a-zA-Z0-9._-]and cross-checked against the capability registry before any invocation.
- Dependencies are checked against the RustSec advisory database, a permissive
licence allow-list and a crates.io-only source policy on every push
(
cargo deny,deny.toml). - CI actions are pinned to commit SHAs and run with a read-only token.
- Release binaries are built
--lockedwith the toolchain CI tested, and are published with a SHA-256 file, a CycloneDX SBOM and a build provenance attestation:gh attestation verify sentinel-<target> --repo marcuspat/Sentinel. - Details and limits: docs/SUPPLY_CHAIN.md.
- Landlock confines writes only. It does not restrict reads, signals, or connections to existing Unix sockets, and on kernels without it the default is to warn, not refuse.
- Network denial blocks socket families, not a network namespace: Unix-socket access to local daemons remains.
- The fixed child
PATHassumes a conventional filesystem layout (not NixOS/Guix). sentinel fleettrusts SSH authentication to carry the operator's approval to each host, and usesStrictHostKeyChecking=accept-new(trust on first use). Every approved host is contacted in parallel — staged rollout is not wired in — and the mTLS controller protocol is not implemented (roadmap 14f).- Resource guards compare paths lexically and do not resolve symlinks.
- The audit log does not record which policy file was in force (roadmap 14c).
- A process killed mid-run leaves its plan in
Executing; there is no operator command to fail it yet (roadmap 14d). The plan-store lock is advisory and does not cover NFS. - The LLM token budget can be overshot by one response, and a provider that reports no usage counts as zero tokens.
- Provider wire formats are tested against mock servers only, never the live APIs.
- Spans are emitted through
tracing; no OTLP exporter is bundled (roadmap 14e). - Fuzzing is property-based (
proptest); there are no coverage-guidedcargo-fuzztargets. - The release workflow's SBOM and attestation steps have not yet run end to end (they run only on a tag).
GENESIS_HASHis defined as 64 zero characters (an arbitrary sentinel value), notSHA-256(""). External audit tools must use the same convention.