CoursesAtlantisServer-side config & control

Server-side config & control

Who can define workflows.

Advanced14 min · lesson 9 of 12

A hotel lets every guest nudge the thermostat in their own room. Facilities sets the hard limits from a locked control room, so nobody gets a room to 35°C no matter which buttons they mash. Atlantis splits the same way. The repo-side atlantis.yaml is the thermostat: handy, sitting right next to the code, editable by anyone who can open a PR (pull request, a proposed change to the code that someone reviews before it lands). The server-side repos.yaml is the control room. Only the operator who runs the Atlantis server can read or write it, and it decides which knobs the thermostat even has.

That split is where the security of the whole system lives. Atlantis holds credentials strong enough to rebuild your infrastructure, and it takes its orders from pull requests. Pull requests are the one thing in your pipeline that contractors, brand-new hires and (on a public repo) complete strangers can create whenever they feel like it. Server-side config is what stops one of those pull requests from rewriting the rules it is supposed to pass.

Two files, one trust boundary

Get the two files straight before anything else. atlantis.yaml sits at the root of a repo and describes *projects*: which folders hold Terraform code, which workspaces exist, when to plan automatically. repos.yaml lives with the server and answers a completely different question. What is this repo *allowed* to configure? (That filename is only a convention. You point the server at the file with --repo-config.) Three keys carry most of the weight. apply_requirements lists what a pull request must satisfy before atlantis apply will run: approved means a reviewer signed off, mergeable means no conflicts and the required checks are green, undiverged means the branch is not sitting behind the branch it targets. allowed_overrides is a whitelist, one key at a time, of the settings a repo's own atlantis.yaml may change. allow_custom_workflows decides whether a repo may write workflow steps of its own, including run steps, which are plain shell commands.

repos.yaml
# Server-side config — lives with the server; only operators can edit it.
repos:
# Baseline for every repo. Order matters: for each key the LAST
# matching entry wins, so the wildcard goes first, exceptions after.
- id: /.*/
branch: /.*/
apply_requirements: [approved, mergeable, undiverged]
allowed_overrides: [workflow] # repos may PICK a workflow...
allowed_workflows: [default, govcloud] # ...from this menu only
allow_custom_workflows: false # ...and may NOT author their own
workflow: default
# Exception: the platform team's own repo. Higher trust, but with a
# compensating control — even planning waits for an approving review.
- id: github.com/acme/platform-infra
allow_custom_workflows: true
plan_requirements: [approved]
# Workflows defined here are operator-reviewed code, not PR content.
workflows:
govcloud:
plan:
steps:
- init
- plan:
extra_args: ["-var-file=gov.tfvars"]
apply:
steps: [apply]

How the server resolves config

The matching rules deserve a slow read, because one small ordering mistake quietly loosens every guardrail you thought you had. Each entry's id is either an exact repo path (github.com/acme/payments) or a regex wrapped in slashes (/.*/). A regex, short for regular expression, is a pattern that matches many strings at once. When a webhook arrives (a webhook is the little message your Git host fires off the moment something happens on a pull request), Atlantis walks the repos list from top to bottom and, for every key, the last matching entry wins. It works like the notes on a shared fridge: the one stuck on top is the one everybody obeys. That is why the wildcard baseline goes first and the exceptions come after. Flip the order and your restrictive baseline flattens the exception. Worse, a broad regex parked at the bottom silently overrides every hardened entry above it.

Once the server has settled on an entry, it reads the repo's atlantis.yaml and filters it. Any key that is not listed in allowed_overrides gets rejected with an error rather than dropped in silence, so the plan fails loudly on the pull request itself. allowed_workflows gives you a useful middle setting: a repo can *pick* from a menu of workflows the operator wrote, without being able to author steps of its own. Then there is plan_requirements, the younger sibling of apply_requirements. It gates planning itself, which matters more than it first sounds. The init step of a plan already downloads whatever provider binaries and modules the pull request's own .tf files name, and the plan that follows runs those providers.

Two files, one trust boundary
Server-side: repos.yaml (the control room, editable only by the operator who runs Atlantis)
apply_requirements
approved · mergeable · undiverged: gates a repo cannot lower on its own
allowed_overrides
key-by-key whitelist of what atlantis.yaml is allowed to change
allow_custom_workflows
false by default; true lets any PR author run code as the server
workflows / allowed_workflows
operator-written steps; repos may only pick from the menu
Repo-side: atlantis.yaml (the thermostat, editable by any pull request author, contractor or stranger)
projects / dirs
which folders hold Terraform
workspaces
which workspaces exist
autoplan
when to plan automatically
override attempts
only keys in allowed_overrides survive; anything else is rejected loudly
Repo-side settings get in only through the allowed_overrides filter. Workflows, requirements and defaults all come from the operator's server-side repos.yaml. That boundary is what stops a pull request from rewriting the rules it has to pass.

Deploying it: Helm, flags, and restarts

On a plain VM (virtual machine, a single server you run yourself) you hand atlantis server the flag --repo-config /etc/atlantis/repos.yaml, or set ATLANTIS_REPO_CONFIG in the environment. On Kubernetes, the official Helm chart does the plumbing for you (Helm is the package manager for Kubernetes): whatever you put in the repoConfig value gets written to a mounted file and wired to that flag. The chart runs Atlantis as a StatefulSet, a pod with its own name and its own disk that survive a restart, because the server keeps plans and locks on that disk between webhook deliveries. Two neighbouring controls belong in the same values file. One is the webhook secret, a shared password that lets Atlantis prove a payload really came from your Git host and not from someone who stumbled onto the endpoint. The other is basic auth for the web UI (user interface, the page you open in a browser) via --web-basic-auth, because that page can throw away locks. The hardening lesson goes further on both.

terminal
helm repo add runatlantis https://runatlantis.github.io/helm-charts
helm upgrade --install atlantis runatlantis/atlantis \
--namespace atlantis --create-namespace -f values.yaml
# Release "atlantis" does not exist. Installing it now.
# NAME: atlantis
# NAMESPACE: atlantis
# STATUS: deployed
# REVISION: 1
# values.yaml — the chart mounts repoConfig as a file and passes --repo-config:
# orgAllowlist: github.com/acme/*
# github:
# user: atlantis-bot
# token: <VCS token — from a Secret in production>
# secret: <webhook secret — authenticates payloads from GitHub>
# repoConfig: |
# ---
# repos:
# - id: /.*/
# apply_requirements: [approved, mergeable, undiverged]
# allowed_overrides: [workflow]
# allow_custom_workflows: false
kubectl -n atlantis get statefulset atlantis
# NAME READY AGE
# atlantis 1/1 2m14s
kubectl -n atlantis logs atlantis-0 | tail -n 1
# {"level":"info","msg":"Atlantis started - listening on port 4141"}

One operational detail bites nearly everyone exactly once. repos.yaml is read at startup and never again. Editing the file, or the Helm value, changes nothing until the pod restarts. Bake a rollout restart into whatever process ships your config changes.

Watching the boundary hold on a pull request

Here is what enforcement looks like from the pull request side. An author whose repo is covered only by the wildcard entry tries two escalations. First they weaken the gate inside atlantis.yaml. Then they try to apply before anyone reviews the change. Both attempts fail with a message that says exactly why, and that is what you want. Loud failures teach a team where the boundary sits.

pull-request thread
# The PR adds this to atlantis.yaml, trying to drop the approval gate:
#
# version: 3
# projects:
# - dir: prod/vpc
# apply_requirements: []
#
# Atlantis replies on the PR — no plan runs at all:
**Plan Error**
parsing atlantis.yaml: repo config not allowed to set 'apply_requirements'
key: server-side config needs 'allowed_overrides: [apply_requirements]'
# The author reverts the override; plan succeeds. Then, before any review:
> atlantis apply
Ran Apply for dir: `prod/vpc` workspace: `default`
**Apply Failed**: Pull request must be approved according to the project's
approval rules before running apply.
# Only after an approving review (and a green mergeability check)
# does `atlantis apply` actually run terraform apply.
allow_custom_workflows: true lets any pull request run code as your server
A custom workflow's run step is plain shell, and it executes as the Atlantis server, holding the server's cloud credentials, the moment a pull request triggers a plan. No approval. No merge. No apply. That is a full infrastructure compromise delivered through a pull request. The exposure also starts earlier than most teams assume: planning already fetches whatever providers and modules the pull request's .tf files name and then runs those providers, and Terraform's external data source can shell out during a plain terraform plan. So treat *plan* as code execution too. Keep allow_custom_workflows: false by default. Enforce apply_requirements on the server so a repo cannot approve itself. Grant allowed_overrides one key at a time. Reserve custom-workflow rights for repos owned by the team that operates Atlantis. The server-side file is the real security control, and the repo-side file is a convenience that lives inside its limits.

Troubleshooting and defaults that hold

When someone tells you the server-side config "doesn't work", it is almost always one of three things. A stale pod, because nobody restarted it after the change. A YAML type error that stopped the server from booting at all. Or an id written as .* instead of /.*/, which without the slashes is read as an exact repo name and therefore matches nothing.

terminal
# "My repos.yaml change did nothing" — three usual suspects:
# 1. Stale pod: the file is parsed ONCE, at boot. Restart after every change.
kubectl -n atlantis rollout restart statefulset atlantis
# 2. Boot failure: a YAML type error stops the server entirely — check logs.
kubectl -n atlantis logs atlantis-0 --previous | tail -n 2
# Error: initializing server: parsing /etc/atlantis/repos.yaml:
# yaml: unmarshal errors: line 5: cannot unmarshal !!str into []string
# 3. Wrong id syntax: regexes need slashes; anything else is an exact match.
# id: /.*/ -> regex, matches every repo
# id: github.com/acme/payments -> exact match only
# id: .* -> exact repo literally named ".*" (never matches)

Know where this model stops, as well. repos.yaml is global and static. There is no per-team delegation short of listing repos one by one, no live lookup against your identity provider, and branch regexes (matched against the branch a pull request targets) are the only granularity you get below the level of a whole repo. The trade-off is real: a strict baseline means a platform-team pull request for every workflow tweak, and teams feel that friction. allowed_workflows menus are how you buy some of that autonomy back without handing out run steps. For defaults that hold up, enforce approved, mergeable and undiverged everywhere, keep allowed_overrides at [workflow] or empty, keep allow_custom_workflows: false, and treat every loosening as a security review rather than a config change.

Server-side config decides *what code* the automation will run on a repo's behalf. The other half of the blast radius is *what power* that code runs with, meaning the cloud credentials sitting inside the Atlantis pod. That is the next lesson: scoping them, storing them and rotating them.

Open-source repos and contractor-heavy ones make this boundary non-negotiable. Anyone who can open a pull request can propose YAML. If that YAML can rewrite run steps, they can walk out with the cloud credentials Atlantis holds. Server-side workflows close that door, and reviewing atlantis.yaml turns back into a question about project routing rather than about privilege escalation.

Write the split down for contributors in one sentence they will actually remember: you may add projects and when_modified, you may not add run steps. Put those same words in CODEOWNERS, the file that says which reviewers a given path requires, so a stray workflow key gets caught during review.

Dump the effective server config into your private ops repo with secrets redacted, so an auditor can read the allowlists, the apply requirements and the workflow names without anyone opening an SSH (secure shell, a remote terminal) session. Drift between the pod that is running and the last values file anyone merged is a recurring incident class. Make the config you intend reviewable.

If a trusted team genuinely needs repo-level workflow overrides, allowlist the specific keys and keep run off the list. Overrides for dir and when_modified usually cover what they actually wanted. The moment run becomes overridable from a pull request branch, you are back to a stranger executing code with your cloud credentials.

Try this

Put the boundary in front of your own eyes. Read what the server says about allow_custom_workflows, the workflow definitions and plan_requirements, then open a pull request that tries to add a workflow of its own. Prove to yourself that a pull request cannot quietly swap out the plan pipeline.

terminal
grep -n "allow_custom_workflows\|workflow\|plan_requirements" /etc/atlantis/repos.yaml
# attempt a PR that adds a custom workflow with curl|bash — expect server to ignore or reject
output
allow_custom_workflows: false
# PR-defined workflow not used
# server workflow "default" steps remain init, plan, conftest

Takeaway

repos.yaml is the control room and atlantis.yaml is the thermostat. A pull request author never gets to redefine the pipeline that guards a production apply.

Next: keep custom workflows switched off unless you would trust the contributor with your cloud account, and keep the apply requirements (approvals, branch protection) in the server config where a pull request cannot reach them.

Quick check
01You add a hardened exact-match entry for github.com/acme/platform-infra near the top of repos.yaml, then append a broad /.*/ baseline at the very bottom of the same list. What does platform-infra end up running under?
Incorrect — Specificity plays no part in the match. Atlantis reads the list in file order, and position is the only thing that decides which value sticks.
Incorrect — First match would make ordering harmless. The server keeps walking past a match instead, so anything lower down replaces what an earlier entry set.
Correct — Each key is resolved on its own and the last matching entry supplies it, so a wildcard sitting at the end of the list quietly undoes the hardening you wrote higher up.
Incorrect — Overlapping matches are ordinary here and get merged rather than rejected. The server starts fine, which is exactly what makes a misordered list easy to miss.
02The platform team's entry pairs allow_custom_workflows: true with plan_requirements: [approved], so even a plan waits for a reviewer. What risk is that gate covering?
Incorrect — The exposure named here is code running, not state being written. What init and the plan execute is provider code that arrived with the branch.
Correct — init fetches the providers and modules the branch asks for and the plan then runs them, so a plan is an execution step and not a preview.
Incorrect — apply_requirements gates the apply directly. plan_requirements exists because the plan carries risk of its own, not as an indirect route to the apply gate.
Incorrect — The plan reaches for providers and runs them well before any apply, so the concern is what executes, not what it bills.
03A repo covered only by the wildcard baseline (allowed_overrides: [workflow]) opens a pull request whose atlantis.yaml sets apply_requirements: [] on prod/vpc. Atlantis answers with a Plan Error naming that key. Where does that leave the pull request?
Correct — The refusal lands before any plan starts, so nothing runs under the weakened rules, and only an operator editing allowed_overrides on the server can grant that key.
Incorrect — Atlantis puts the refusal on the pull request thread where the author sees it, and it stops the plan rather than quietly running one.
Incorrect — That flag decides whether a repo may author its own workflow steps. Which keys a repo may set at all is governed by allowed_overrides, granted one key at a time.
Incorrect — A bad key inside a repo's own file never reaches the server process. Only this pull request errors, and every other repo carries on.

Related