Policy¶
Policy decides what may execute. It is evaluated before every operation, before authentication, and it is never optional and never skippable.
Location: <Runtime Home>/policy-config.yaml, or $RUNTIME_POLICY_FILE.
Ownership: yours. Seeded once, never overwritten by an upgrade.
Policy has no environment-variable overrides. An env var must never be able
to widen governance — point RUNTIME_POLICY_FILE at a different file instead.
Two governance surfaces¶
| Surface | Governs | Block |
|---|---|---|
| Provider Governance | runtime <provider> <operation> |
providers: |
| Binary Governance | runtime command run <binary> |
allowed_binaries: + command_policy: |
When a provider chooses the CLI transport for an operation, both apply —
the provider rule first, then allowed_binaries and command_policy on top.
Provider Governance¶
enabled: true
providers:
github:
enabled: true
denied:
- api DELETE
files:
enabled: true
denied:
- delete
How rules match¶
Operations are discovered from the Provider that owns them, not from a list here. The consequences:
- A provider with no entry is fully allowed.
runtime github repo listworks without a policy edit, and a new operation in a later runtime version never requires one. deniedprefixes match against"<operation> <args...>". A rule can deny a whole operation (delete) or one narrow case (workflow run deploy-prod.yml,api DELETE).allowed, when non-empty, restricts the provider to those prefixes.enabled: falsedisables the provider entirely.- Rules are evaluated on the operation, never the transport. A denial cannot be sidestepped by a provider moving an operation from REST to the CLI.
Governance is evaluated on what will actually execute
If a provider normalizes an argument, policy matches the canonical
form. This closed a real bypass: the github provider upper-cases an api
method, so api delete once slipped past the api DELETE denial and sent
a genuine DELETE. Any normalization performed after the policy check is a
bypass by definition.
A rule must name an operation that exists¶
The seeded policy denies api DELETE rather than repo delete — because
there is no repo delete operation. That rule would read like governance
while enforcing nothing, and the destructive path is actually the api escape
hatch.
The report ends with policy warnings for any rule that can never match: a
providers.<name> rule naming an operation the provider doesn't expose, a
block for an unregistered provider, or a command_policy.rules.<binary> entry
for a binary absent from allowed_binaries.
A denial that matches nothing is worse than no denial — it produces false confidence. Check the warnings after every policy edit.
Locking a provider down to an allow-list¶
Anything not prefixed by one of those is refused.
Allow-list → deny-list was a deliberate change
Earlier versions used a global allowed_commands list, which no longer
exists. Installations that need an explicit allow-list must set
providers.<name>.allowed. See Migration Guides.
Binary Governance¶
allowed_binaries:
- gh
- git
- terraform
- gcloud
- kubectl
- oc
- aws
- az
- vault
- helm
- flux
- gsutil
- bq
- docker
- podman
- kustomize
- argocd
- istioctl
- pulumi
- packer
- sops
Only these binaries may execute at all through runtime command run. A
binary absent from this list is refused, whatever the subcommand rules say.
Subcommand rules¶
command_policy:
# Refused outright even if they were ever added to allowed_binaries
denied_binaries:
- rm
- sudo
- chmod
- curl
- wget
- ssh
rules:
git:
denied:
- push --force
- push -f
- reset --hard
- clean -fd
- filter-branch
- update-ref -d
terraform:
denied:
- destroy
kubectl:
denied:
- delete namespace
helm:
denied:
- uninstall
Matching semantics, which differ between the two lists:
| List | Matching |
|---|---|
denied |
A contiguous run of argument tokens anywhere in the command |
allowed |
Prefix match |
Denials always win.
Because denied matches anywhere, a leading global flag cannot bypass a rule:
git -C /some/dir push --force is caught exactly like a bare
git push --force.
Subcommand rules are evaluated before Runtime Context injection, so an
injected flag (--project=, -n <namespace>) can never be used to slip past a
denial.
What the seeded rules protect¶
The shipped defaults target one thing consistently: irreversible actions.
| Binary | Denied | Why |
|---|---|---|
git |
force-push, hard reset, clean, filter-branch, ref deletion | Rewrites or discards history |
terraform, pulumi |
destroy |
Tears down real infrastructure |
helm, istioctl |
uninstall |
Removes a release or the mesh control plane |
flux |
delete, uninstall |
Removes GitOps-managed resources or Flux itself |
kubectl / oc |
delete namespace / delete project |
Bulk resource destruction |
gcloud |
project and service-account deletion | Identity and account loss |
aws |
s3 rb --force, ec2 terminate-instances, iam delete-user, rds delete-db-instance |
Bulk data and identity deletion |
az |
vm delete, group delete, ad user delete, keyvault delete |
Same profile as aws |
vault |
kv delete, kv metadata delete, secrets disable |
Permanent secret removal |
gsutil |
rm -r, rb |
Recursive object and bucket deletion |
bq |
rm -f, rm -r -f, rm -f -r |
-f skips the confirmation prompt |
docker, podman |
system prune -a, volume rm, run --privileged |
Bulk pruning; privileged run is a container-escape risk |
argocd |
app delete, cluster rm, repo rm |
Detaches or deletes GitOps state |
kustomize, packer and sops carry no denial rules — none of them has a
subcommand that destroys existing infrastructure. kustomize only renders YAML
to stdout, packer only builds new images, and sops encrypts and decrypts
files in place.
Denials are audited¶
A denial is recorded exactly like a success or a failure. That is the point: the audit log answers "was this attempted and refused", not just "what happened".
The runtime tells you when it is the one refusing:
Common tasks¶
After any edit:
New policy defaults never reach an existing install
policy-config.yaml is user-owned, so a release adding allowed_binaries
entries or new deny rules changes nothing for a machine that already has
the file. See the upgrade trap.