teriac server Rules and permissions

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 }]
  • id is one repo (github.com/acme/infra) or a glob (github.com/acme/*). Only repos matching an entry (or TERIAC_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[].match is 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: true the 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. warn shows the commits on the PR and in the apply reply; block refuses the apply until the branch is updated; off skips 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_unlock is not teriac unlock. teriac force-unlock runs terraform 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 set force_unlock: []. teriac unlock only 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 locks

Repo 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.