Rules and permissions
Per-project RBAC and required approvals in repos.yaml, apply requirements, per-command permissions, and what a repo's own teriac.yaml may add.
Rules live in repos.yaml on the server (TERIAC_REPOS_CONFIG, default /data/repos.yaml), or in
the repos value of the Helm chart. Because they're on the server, a pull request can't change them.
repos.yaml
repos:
- id: github.com/acme/*
require_apply_project: true # same as the env var, per repo
apply_requirements: [mergeable]
projects:
- match: prod/**
permissions:
apply: [team:platform-admins] # read|write|maintain|admin, team:x, user:x, codeowners, anyone
apply_destroy: [admin] # plans that delete or replace
state_read: [team:platform] # state show, show, output (optional)
approvals:
- { from: team:platform-admins, count: 2 }
- { from: team:security, count: 1 }
- match: "**"
approvals: [{ from: write, count: 1 }]idis one repo (github.com/acme/infra) or a glob (github.com/acme/*). Only repos matching an entry (orTERIAC_REPO_ALLOWLIST) are handled.- A repo uses only the first entry that matches. Entries don't combine, so list exact repos before globs, and give each entry everything that repo needs. Drift schedules need an exact repo.
projects[].matchis a glob on the project's folder or name. The first rule that matches a project applies.
Every key is listed in the repos.yaml reference.
Who
Who can do something is a list of any of:
| Principal | Means |
|---|---|
read, write, maintain, admin |
At least this role on the repo |
team:<name> |
Members of a team (team:platform or team:acme/platform) |
user:<login> |
One person |
codeowners |
The owners of the project's folder in CODEOWNERS |
anyone |
Anyone who can comment |
Roles and teams come from your Git platform, so there's no separate user list to keep up.
Approvals
approvals:
- { from: team:platform-admins, count: 2 }
- { from: team:security, count: 1 }- Every entry must be met: here, two platform admins and one security reviewer.
- Only approvals of the current commit count. A new push needs new approvals.
- The author's own approval never counts.
- Each project needs only its own approvals, so one PR can touch a dev project (no approvals) and a prod project (two) at once.
Commands that edit the state directly (outside a plan and apply) also need the project's approvals, like an apply.
Re-approval, rebases and out-of-date branches
New in 0.2.0.
repos:
- id: github.com/acme/*
reapprove_on_change: true # default true: a code change needs a fresh approval
keep_approval_on_rebase: true # default false: a rebase or "Update branch" keeps approvals
stale_base: warn # off | warn (default) | block
verify_plan: true # default true: apply only the exact plan file reviewed- Re-approval. With
reapprove_on_change(on by default), a push that changes the PR's code needs a fresh approval. - Rebases. teriac fingerprints the PR's own changes: the files it changes and their exact
contents, compared with where the branch left the base. When an approval was given on an earlier
commit and the fingerprint is the same now, only the base moved, so with
keep_approval_on_rebase: truethe approval still counts, and the comment says whose approval was kept. A rebase where conflicts were resolved changes the fingerprint and needs a new approval. When teriac can't compare (for example, more than 300 files changed), it asks for a new approval. - Out-of-date branches. At plan and apply, teriac checks whether the base branch changed any
file a project uses (its folder, local modules, Terragrunt dependencies, watched files) since the
PR branched. Applying such a branch could undo those changes.
warnshows the commits on the PR and in the apply reply;blockrefuses the apply until the branch is updated;offskips the check. - Reviewed plan only. With
verify_plan(on by default), the plan file's SHA-256 is saved when it's made and checked again before apply. A changed or missing file is refused, audited, and must be planned again.
A repo's teriac.yaml can make these stricter (reapprove_on_change: true,
keep_approval_on_rebase: false, a stricter stale_base), never looser.
Apply requirements
apply_requirements adds conditions on the PR itself:
| Requirement | Means |
|---|---|
approved |
At least one approval from someone else (when no approvals are set) |
mergeable |
GitHub says the PR can be merged (no conflicts, required checks passing) |
undiverged |
The PR is up to date with its base branch |
Set them on a repo, or on a project rule.
Default permissions
When no rule applies:
| Permission | Covers | Default | Not set for a project? Uses |
|---|---|---|---|
plan |
plans, validate, providers, fmt, state list |
write | |
apply |
applies | write | |
apply_destroy |
applies that delete or replace | maintain | |
unlock |
releasing another PR's lock | maintain | |
state_read |
state show, show, output |
write | plan |
state_write |
import, taint, untaint |
write | apply |
state_remove |
state rm, state mv, force-unlock |
maintain | apply_destroy |
The last three cover the Terraform commands. When a project doesn't set them, they use the rule they
extend, so they're never looser: limit apply_destroy to admins and state rm is admin-only too.
force_unlockis notteriac unlock.teriac force-unlockrunsterraform force-unlock: it releases Terraform's lock in the state backend (S3/DynamoDB, GCS, …) left behind by a crashed run, the one behind Error acquiring the state lock. Releasing it while a run is writing can corrupt the state, so many teams setforce_unlock: [].teriac unlockonly frees teriac's own lock on a PR's projects, and never touches Terraform or the backend.
Per-command permissions
Each command also has its own key, for exceptions: state_list, state_show, show, output,
validate, providers, fmt, import, taint, untaint, state_rm, state_mv,
force_unlock. A command that isn't set uses its group; [] means nobody.
permissions:
state_remove: [admin] # state rm, state mv, force-unlock: admins
state_mv: [maintain] # except state mv: maintainers too
force_unlock: [] # and nobody may release Terraform state locksRepo switches
Per repo in repos.yaml:
| Key | Default | |
|---|---|---|
autoplan |
true |
Plan on every push. false: only when someone comments teriac plan |
plan_drafts |
false |
Also autoplan draft PRs |
allow_targets |
true |
Allow -t targeted plans |
allow_extra_args |
false |
Allow teriac plan -- -var=x |
allow_state_changes |
true |
Allow import, taint, state rm/mv, force-unlock |
allow_repo_config |
true |
Read the repo's teriac.yaml |
allow_repo_schedules |
true |
Read drift schedules from teriac.yaml |
two_person_rule |
false |
The PR author can't apply |
A repo's own teriac.yaml
A repo's teriac.yaml can name projects, pin versions, and add stricter rules, but never looser
ones: both the server's rules and the repo's must pass. It's read from the base branch, so a PR
can't change its own rules.
# teriac.yaml
require_apply_project: true
projects:
- name: prod-network
dir: live/prod/network
approvals: [{ from: team:network, count: 1 }]
rules:
- match: live/prod/**
apply_requirements: [undiverged]Turn it off for a repo with allow_repo_config: false. All keys are in the
teriac.yaml reference.
In Helm values
The chart takes the same rules under repos:
repos:
repos:
- id: github.com/acme/infra
projects:
- match: prod/**
permissions:
apply: [team:platform-admins]
apply_destroy: [admin] # also covers state rm / state mv / force-unlock
force_unlock: [] # single commands can be set too; [] = nobody
approvals: [{ from: team:platform-admins, count: 2 }]Recipes
Prod needs two admins, everything else one reviewer:
projects:
- match: "prod/**"
approvals: [{ from: team:platform-admins, count: 2 }]
- match: "**"
approvals: [{ from: write, count: 1 }]Code owners approve their own folders:
projects:
- match: "**"
approvals: [{ from: codeowners, count: 1 }]Only platform can destroy, and no one can force-unlock:
projects:
- match: "**"
permissions:
apply_destroy: [team:platform]
force_unlock: []Policy checks and the two-person rule go in the same file; see policy checks and safety.