Security Model
sparkrun runs shell commands derived from recipes — pre_exec inside
containers, post_exec inside containers after a health check,
post_commands on the control machine. Trust gating exists so a recipe
fetched from a third-party registry can’t silently execute arbitrary commands
when you launch it.
When trust is automatic
Section titled “When trust is automatic”A recipe is trusted (hooks run without prompting) when any of these hold:
- The user passed
--truston the CLI. - The recipe was loaded from a local path — anything outside the
registry cache, e.g. a file you wrote,
./recipes/, or~/.config/sparkrun/recipes/. - The recipe came from a registry your
registries.yamlmarkstrusted: true. Every registry sparkrun ships by default is first-party and ships trusted. - You marked its registry trusted yourself, with
sparkrun registry trust <name>orsparkrun registry add --trust <url>.
Any registry you add is untrusted until you say otherwise.
Trust is a local decision recorded in your own
~/.config/sparkrun/registries.yaml. A manifest in the source repository
cannot grant itself trust.
Otherwise the recipe is untrusted and sparkrun prompts before each hook
surface runs. The same trust decision is shared between pre_exec
(pre-launch) and post_exec / post_commands (post-launch) — you confirm
once per recipe per session.
The --trust flag
Section titled “The --trust flag”sparkrun run --trust <recipe> skips the prompt for any third-party recipe.
Use it only when you’ve reviewed the recipe and intend to run its hook
content. The flag is hidden from --help by design: it should be a
conscious operator override, not the default workflow.
What trust does NOT cover
Section titled “What trust does NOT cover”Trust covers more than hooks. An untrusted recipe is also refused these container-escape surfaces:
| Surface | Untrusted recipes |
|---|---|
executor_config: privileged, cap_add, security_opt, devices, user, volumes | Rejected — each maps to a docker run flag that defeats the rootless hardening or exposes host state |
executor: selection | Restricted to docker — the rootless, namespaced container is the sandbox that justifies running a registry recipe’s serve command at all. local runs natively via setsid with no container, i.e. arbitrary host code execution |
Host bind-mounts (including the undocumented cluster_config launch overrides) | Rejected |
Innocuous resource knobs are deliberately not gated: shm_size, ipc,
network, memory_limit, ulimit, restart_policy, auto_remove, labels.
A denylist applies regardless of trust: mounting the host root, the Docker
control socket, SSH keys, or kernel pseudo-filesystems is refused outright,
even for a trusted recipe. The guard validates the literal path shape
(absolute, no .., not under a forbidden subtree) because the mount happens on
a remote host whose symlink layout differs from your control machine’s.
Recipe env values are also no longer expanded against your environment, so a
third-party recipe cannot exfiltrate ${AWS_SECRET_ACCESS_KEY} into a
container it controls.
Registry safety rails
Section titled “Registry safety rails”sparkrun enforces three additional rails on the registry surface:
- Git URL allowlist:
sparkrun registry addaccepts onlyhttps://...,git@host:org/repo,ssh://..., andfile://.... Anything else is rejected beforegit cloneis invoked. - Reserved name prefixes: registry names starting with
sparkrun,official,arena,spark-arena(and similar) can only be claimed by repositories hosted under approved GitHub organizations. This prevents a third party from impersonating an official source. - Trust is opt-in per registry: a registry added by URL is untrusted until
you mark it so. Only the bundled registries that ship
trusted: trueskip the prompt out of the box.
Operator checklist
Section titled “Operator checklist”When adding a third-party registry:
- Inspect every recipe in the registry for
pre_exec,post_exec,post_commands,executor_config.cap_add,devices, andsecurity_opt. - Confirm the registry URL matches one of the four allowed schemes.
- Run untrusted recipes with
--dry-runfirst; the trust prompt makes the per-launch posture explicit before any side effects. - Use
--trustonly when the review is complete and you intend to run the recipe’s privileged content. - Reserve
sparkrun registry trust <name>for registries you operate or review continuously — it removes the prompt for every recipe in them, now and in future updates.
Other hardening worth knowing
Section titled “Other hardening worth knowing”- The proxy bind address is explicit. An unconfigured proxy still binds
0.0.0.0for backward compatibility but warns loudly on every start, and escalates the warning when no master key is set. Proxy state files carrying the master key and upstream API keys are written0600. See proxy. - Sudoers interpolation is validated. Usernames and the
cache_dirpath are checked against a conservative charset before being written into the scoped sudoers rules the wizard installs. - Secrets are masked in debug output. Env values whose names contain
token,key,password, orsecretare masked in the Docker executor’s DEBUG logs. - Arena OAuth is CSRF-bound. The callback binds to a per-flow random state nonce and rejects a mismatched or missing state before storing a token.
For the full security narrative (utils/shell.py quoting, sudoers
validation, OAuth callback CORS tightening, trtllm host-key strictness, the
delegated-copy path), see
docs/SECURITY.md.