Atlantis v0.49.0
v0.49.0 is a breaking release, for three reasons:
- The Docker images no longer include Terraform or OpenTofu. This was announced in v0.48.0.
- 12 security advisories are fixed here, and several of those fixes change behavior that existing setups may rely on.
- v0.47.1 shipped a breaking change without saying so: the shell-injection fix in #6792. It is listed below so anyone upgrading from v0.47.0 or earlier sees it.
Every behavior change is also documented, in a Behavior change note, on the docs page where you would look for that setting. Read Breaking changes before you upgrade.
Upgrade checklist
- Docker images: set
--default-tf-version(and--default-tf-distribution opentofuif you use OpenTofu) so Atlantis downloads the binary. If downloads are blocked, mount or copy a binary into the image instead. The image no longer ships one. - Upgrading from v0.47.0 or earlier: check workspace names and comment arguments against the v0.47.1 changes below.
- GitHub / GHES: make sure
--gh-hostnamematches the host in your repositories' clone URLs. Webhooks whose clone URL is on a different host are now rejected. - Server-side repo config: if repos use
allow_custom_workflows: truetogether withallowed_workflows, check that every project'sworkflowis in the allow-list. Repo configs that used to pass can now be rejected. - Project dirs: make sure no project
diris a symlink to somewhere outside the repository. - Comment commands: tell users that
-out,-generate-config-out,-state,-state-outand-backupare now blocked after--, and that--blocked-extra-argscan no longer unblock anything. - Comment
-var-file: the allowlist now applies toapply,importandstate rmas well asplan. Paths containing..are refused, and so are repo symlinks that point outside the repository and the allowlist. -
state rm: ifstateis in--allow-commands,state rmnow has to meet the project'sapply_requirementsandimport_requirements. - API: if
--api-secretis set, sendX-Atlantis-TokenonGET /api/locks. Check that/api/planand/api/applycallers send clean, repo-relativepaths[].directoryvalues with no leading/and no globs. - Approvals (GitHub, Gitea):
apply_requirements: [approved]now uses each reviewer's latest review. On Gitea, dismissed and stale approvals no longer count, and any push that changes the diff makes approvals stale. - Apply: plans recorded by v0.49.0 are bound to a hash. A plan file that changes between
planandapplyis rejected and has to be re-planned.
Breaking changes
Docker images
The Docker images no longer include Terraform or OpenTofu
- What changed: the
latest,vX.Y.ZandvX.Y.Z-debianimages no longer bundle Terraform or OpenTofu binaries, and they set no default Terraform version. This was announced in the v0.48.0 release notes. The-alpine-slimand-debian-slimimages from v0.48.0 were the intermediate step toward this and are no longer published. If you pinned one, switch to the unsuffixed,-alpineor-debiantag, which now contain the same thing. If you build the image yourself, use--target alpineor--target debian; the-slimtargets are gone. - Why: every advisory against any bundled Terraform or OpenTofu release showed up in scans of the Atlantis image, whether or not you used that version. Atlantis can't own the security lifecycle of those third-party binaries.
- Who is affected: anyone using the published images who relied on the bundled
terraformortofubinary onPATH, or on the version the image used to set by default. - How to tell: the server refuses to start with
terraform not found in $PATH. Set --default-tf-version .... - What to do:
- Set
--default-tf-version(ATLANTIS_DEFAULT_TF_VERSION, or HelmdefaultTFVersion) so Atlantis downloads that version at startup. Repos can still pin their own version withterraform_version. - For OpenTofu, also set
--default-tf-distribution opentofu(HelmdefaultTFDistribution). - If the server can't reach the release hosts, put a binary on
PATHyourself: mount one, or buildFROM ghcr.io/runatlantis/atlantisand copy one in. Set--tf-download=falseso Atlantis uses it and doesn't try to download. - See Deployment: image variants.
- Set
Carried over from v0.47.1 (#6792)
v0.47.1 fixed a shell-injection vulnerability (GHSA-rjh2-4xgw-p2hj) and changed behavior along the way, but its release notes didn't call that a breaking change. If you are upgrading from v0.47.0 or earlier, these apply to you.
Terraform and conftest commands no longer run through a shell
- What changed: Atlantis used to build Terraform and conftest command lines as strings and run them with
sh -c. Commands now run as argument vectors.- Each
extra_argsentry is passed as exactly one argument. Shell operators (;,&&,|, redirections), globbing and word splitting no longer apply. - Environment references such as
$WORKSPACEand$DIRare still expanded in operator-configuredextra_args, but no longer in arguments typed in a PR comment after--. - For conftest, a policy set
pathand eachextra_argsentry are still split on whitespace. Quote a path that contains a space.
- Each
- Who is affected:
- Workflows whose
extra_argsrelied on shell behavior, for example two flags in one entry, a pipe, a redirection or a glob. - Users who passed
$VARreferences in comment arguments and expected them to expand.
- Workflows whose
- What to do: split multi-flag entries into one argument per entry. Move anything that needs a shell into a
runstep, which still runs with a shell by design.
Workspace names are validated everywhere
- What changed: workspace names from repo
atlantis.yaml, the-wcomment flag, the Terraform Cloud workspace read from HCL, and the API are validated. A name can't contain/,\,..,$, whitespace or control characters, can't be., and can't start with-or~. - Who is affected: projects or comments using such names. A name containing
$never worked, because the shell expanded it. - How to tell: repo config validation fails under the
workspacekey, or the comment is rejected with the offending value and a reason such ascannot contain '/' or '\'. - What to do: rename the workspace to one without those characters.
Server-side repo config
allowed_workflows is now checked for every project (GHSA-56q6-8vmp-v2qm)
- What changed: with
allow_custom_workflows: true, a bug stopped theallowed_workflowscheck at the first project that used a repo-local workflow. Projects after that one were never checked. Now every project is checked. - Who is affected: repos with
allow_custom_workflows: true,allowed_overrides: [workflow]and a non-emptyallowed_workflows, whoseatlantis.yamlhas a later project naming a server-side workflow that is not inallowed_workflows. - How to tell: the repo config fails validation with
workflow '<name>' is not allowed for this repo. - What to do: add the workflow to the repo's
allowed_workflowsin the server-side config, or change the project to use an allowed workflow.
A project dir that symlinks outside the repository is rejected (GHSA-9q39-mgw7-5789)
- What changed: Atlantis now follows symlinks before checking that a project directory is inside the clone. Before, the check only looked at the path as written.
- Who is affected: repos whose project
dir(or a parent directory in its path) is a symlink pointing outside the clone. That includes absolute targets and../targets that leave the repository. Symlinks that stay inside the repository (for exampledir->modules/x) still work. - How to tell: plan, apply, import and state commands fail with
project path traversal detected: .... A path that cannot be resolved for any other reason (for example a file where a directory is expected) now fails withresolving project path: .... Before, Atlantis ignored that case. - What to do: replace the symlink with a real directory inside the repository, or point it at a target inside the repository.
- Also changed: a broken
dirsymlink keeps the existingdir "<dir>" does not existerror. If anything other than a regular file (for example a symlink) sits at the plan file path inside the project directory, Atlantis deletes it before planning.
Comment commands
More Terraform flags are blocked after --, and --blocked-extra-args now adds to the list instead of replacing it (GHSA-chm7-fjpm-927x)
- What changed:
- These flags are now always blocked in comment extra args:
-chdir,-plugin-dir,-out,-generate-config-out,-state,-state-out,-backup. Before, only-chdirand-plugin-dirwere blocked. - Flags are matched by name. One entry blocks both the
-and--spellings and the=valueform. --blocked-extra-argsnow adds to the built-in list. Before, setting it replaced the defaults, so an operator could unblock-chdiror-plugin-dirthat way. That is no longer possible.
- These flags are now always blocked in comment extra args:
- Who is affected:
- Users who pass any of these flags in comments. Watch for
atlantis plan -- -generate-config-out=..., which some teams use to generate config forimportblocks. - Operators who set
--blocked-extra-argsto allow one of the default flags. - Users who pass Terraform's own
-stateor-backuptoatlantis state rmin comments. Those are blocked too.
- Users who pass any of these flags in comments. Watch for
- How to tell: the comment is rejected with
flag "<arg>" is not allowed in extra args. - What to do:
- To use
-generate-config-outor the other flags, put them inextra_argsof a workflow you control (server-side, or a repo workflow the server allows). Workflowextra_argsare not checked. - If you listed both spellings in
--blocked-extra-args, you can drop the--entries. Nothing breaks if you leave them.
- To use
The comment -var-file allowlist covers more commands, spellings and paths (GHSA-chm7-fjpm-927x)
- What changed:
--var-file-allowlist(default:--data-dir) is now enforced onapply,importandstate rm, not onlyplan.- It now catches
--var-fileand values that contain=. - Any value containing a
..element is refused, even if it would resolve inside the repository (for exampleenvs/../common.tfvars). - After cloning, repository-relative paths are resolved with symlinks followed. The result must be inside the repository or an allowlisted directory, so a symlink into an allowlisted directory still works. A symlink whose target does not exist is refused. A plain file that doesn't exist yet is still allowed.
- Only
-var-filein comment extra args is checked. Workflowextra_argsare not.
- Who is affected: users who pass
-var-filein comments on these commands with a path outside the repository and the allowlist, a path containing.., or a path that goes through a committed symlink leading outside the repository. - How to tell: errors include
var file path <p> is not allowed by the current allowlist: [...]var file path <p> must not contain '..' elementsvar file path <p> resolves to <r>, which is not allowed by the current allowlist: [...]var file path <p> is a symbolic link whose target does not exist
- What to do: use a clean path inside the repository, or add the directory to
--var-file-allowlist.
atlantis state rm now enforces apply and import requirements (GHSA-cxvq-66x3-pwhw)
- What changed:
state rmused to skip command requirements entirely. It now has to meet every requirement in the project'sapply_requirementsplus every one in itsimport_requirements. There is no separatestate_rm_requirementskey. - Who is affected: servers with
statein--allow-commandsand projects that setapply_requirementsorimport_requirements(approved,mergeable,undiverged,policies_passed). - How to tell:
state rmis refused with the usual requirement messages, ending inbefore running state. For example:Pull request must be approved according to the project's approval rules before running state. - What to do: get the PR approved, mergeable, etc. before running
state rm, or loosen the requirements for that project. If Atlantis can't fetch the PR status,approvedandmergeablecount as not met.
API
GET /api/locks requires X-Atlantis-Token when an API secret is set (GHSA-xgrg-fpp6-q6fm)
- What changed: this endpoint used to be open even with
--api-secretset. Web basic auth never covered/api/paths. - Who is affected: scripts and dashboards that call
GET /api/lockswithout a token on servers that have--api-secretset. If no API secret is set, the endpoint stays open. - How to tell: HTTP 401 with error code
UNAUTHORIZEDand messageinvalid or missing API token. The 401 body uses the newer API error envelope, not the old top-level lock-list body. - What to do: add the header
X-Atlantis-Token: <api secret>.
paths[].directory is validated on /api/plan and /api/apply (GHSA-mwfx-fvfc-7q5w)
-
What changed: non-empty directories have to be clean, repo-relative paths. The request is now rejected for:
- absolute paths, including a bare
/ ..that leaves the repository- backslashes
- glob characters
* ? [ ] { }
Valid values are cleaned:
./diranddir/both becomedir. An empty directory is still allowed. - absolute paths, including a bare
-
Who is affected: API callers that send any of these. Before v0.49.0:
/modules/vpcran inmodules/vpcbut didn't match that directory'satlantis.yamlproject, so it ran with default project settings.- A bare
/meant the repo root. - A glob such as
envs/*matched configured projects by pattern. The API docs already said globs were unsupported, but the code accepted them. That undocumented behavior is gone.
-
How to tell: HTTP 400 with
invalid directory "<dir>": directory must be a clean repo-relative path. -
What to do: send directories relative to the repo root, with no leading slash. Use
.for the repo root. List each project directory explicitly instead of using a glob.
VCS
GitHub clone URLs must be on the configured GitHub host, and head and base repos must share a host (GHSA-6mjf-4m52-m5cq)
- What changed:
- For GitHub, the host in the base and head repositories' clone URLs must equal
--gh-hostname(defaultgithub.com). The check runs on webhooks, on API plan/apply requests, and when a comment command fetches the pull from GitHub. The comparison ignores case and port. A clone URL with no host is rejected. - For every VCS, a command is not run when the PR's head repository is on a different host than its base repository.
- For GitHub, the host in the base and head repositories' clone URLs must equal
- Who is affected:
- GitHub Enterprise Server or GHE Cloud setups where
--gh-hostnameis not the host GitHub puts in clone URLs. For example: an internal alias or IP address, or a reverse-proxy name that differs from the GHES hostname. - Any provider whose webhooks report a different host for the head and base repositories (not expected for normal setups).
- GitHub Enterprise Server or GHE Cloud setups where
- How to tell:
- GitHub webhooks get HTTP 400 with
parsing pull data: ... expected clone url host "<gh-hostname>" but had "<host>". Check your webhook deliveries in GitHub for 400s after upgrading. - API requests fail validation with
failed to parse repository: ..., and comment commands fail with the same host error. - The head/base host mismatch is silent on the PR. The server logs
head repo host "<x>" does not match base repo host "<y>", not running commandat warn level.
- GitHub webhooks get HTTP 400 with
- What to do: set
--gh-hostnameto the hostname that appears in your repositories' clone URLs.
Approval checks use each reviewer's latest review (GitHub, Gitea) (GHSA-fm5f-jg8p-x2vw)
- What changed: for
apply_requirements: [approved](andimport_requirements/state rm), Atlantis now reads every page of reviews and takes each reviewer's most recent approve or request-changes review.- GitHub: an approval no longer counts if the same reviewer later requested changes. Dismissed approvals were already ignored. Changes requested by a different reviewer don't cancel someone else's approval.
- Gitea: an approval no longer counts if it was dismissed, if Gitea marked it stale, or if the same reviewer later requested changes. Gitea marks every review on a PR stale when a push changes the PR's diff, whether or not "Dismiss stale approvals" is enabled in branch protection. A push that doesn't change the diff doesn't.
- Comment-only reviews still neither grant nor revoke approval.
- GitLab, Bitbucket and Azure DevOps are unchanged.
- Who is affected: GitHub and Gitea users relying on
approved. On Gitea, any push that changes the diff invalidates every existing approval until reviewers approve again. - How to tell: apply is refused with
Pull request must be approved according to the project's approval rules before running apply.even though an old approval is visible on the PR. - What to do: re-approve the PR on its current head.
Apply
Apply is refused when the plan file differs from the one recorded at plan time (GHSA-5vwh-xc92-8h64)
- What changed:
- When a plan stage contains the built-in
planstep, Atlantis records a SHA-256 of the plan file with the project's plan status. applyrejects a plan file that no longer matches that hash, and deletes the file.- A plan whose built-in
planstep leaves no plan file now fails at plan time instead of at apply. - The hash is taken after every plan-stage step has finished, so
runsteps in the plan stage can still post-process the file. - Plan stages made only of custom
runsteps record no hash and are not checked. Plans recorded before the upgrade carry no hash and are still accepted.
- When a plan stage contains the built-in
- Who is affected: workflows,
pre_workflow_hooksor tooling that rewrite or replace the plan file betweenatlantis planandatlantis apply. Workflows whose plan stage includesplanbut somehow don't leave the plan file at the expected path. - How to tell:
- Apply fails with
plan file for dir "<d>" workspace "<w>" project "<p>" does not match the plan that was recorded when it was created; run `atlantis plan`. - Plan fails with
plan step did not write a plan file for dir "<d>" workspace "<w>" project "<p>".
- Apply fails with
- What to do: run
atlantis planagain. Don't modify the plan file after the plan stage finishes. Any post-processing has to happen inside the plan stage, before Atlantis records the hash. - Additive:
PlanHashis a new field in stored project status and inPlanSuccessAPI responses. Old stored data decodes unchanged.
Setup
GitHub App setup flow is bound to the browser that started it (GHSA-rx6x-hrg2-g435)
- What changed:
/github-app/setupnow issues a one-timestatenonce and anHttpOnlycookie, both valid for 10 minutes./github-app/exchange-codeonly accepts a callback that carries both.- Only one exchange runs at a time; other requests get 409.
- More than 16 pending setups get 429.
- Right after a successful exchange, both pages return 400 (
Atlantis already has GitHub credentials) in the same process. Before, that only happened after a restart with credentials configured, so record the credentials shown before leaving the page.
- Who is affected: only operators running the one-time GitHub App setup (no app configured yet). Servers that already have an app configured are not affected. Setup now fails if it is split across browsers, has cookies disabled, takes longer than 10 minutes, or is scripted by calling the exchange URL directly.
- How to tell: HTTP 400 on the exchange page with
GitHub app setup state is not valid for this browser, start again from /github-app/setuporGitHub app setup state expired, start again from /github-app/setup. - What to do: reload the setup page and complete the flow in one go, in the same browser. Also pass
--web-basic-authwhile setting up.
Non-breaking fixes
- GHSA-48vm-qqx3-7w83: web basic-auth username and password are compared in constant time. Valid and invalid credentials behave as before.
- GHSA-jxcj-v552-f5q4:
output: hideon customrunandmultienvsteps now also hides output when the step fails, in both the PR comment and the job message. Themultienvparse error no longer echoes the offending line; it now reads likeinvalid environment variable definition: misquoted at offset 8.envsteps are unchanged.
Security fixes
| Advisory | Severity | Summary | Reported by |
|---|---|---|---|
| GHSA-6mjf-4m52-m5cq | Critical | Clone URL host was not checked against the configured VCS host, so the fork check could be bypassed and untrusted code run. | @watsa01 |
| GHSA-56q6-8vmp-v2qm | High | A later project in a multi-project atlantis.yaml could skip the server's allowed_workflows check.
| @bcarikci-dev |
| GHSA-5vwh-xc92-8h64 | High | Apply did not bind to the reviewed plan, so a replaced plan file could be applied under an existing approval. | @luantq0 |
| GHSA-9q39-mgw7-5789 | High | A symlink in a pull request could make Terraform run, or write its plan file, outside the checkout. | @zer0d4y5, @Hama1cco, @avrlab233, @manus-use |
| GHSA-fm5f-jg8p-x2vw | High | Dismissed Gitea approvals (and superseded GitHub approvals) still met apply_requirements: [approved].
| @danieltk76 |
| GHSA-mwfx-fvfc-7q5w | High | /api/plan and /api/apply did not validate paths[].directory at the API boundary. (The authentication half was fixed in v0.45.0; constant-time basic auth is GHSA-48vm.)
| @SnailSploit |
| GHSA-xgrg-fpp6-q6fm | High | GET /api/locks exposed lock metadata without an API token.
| @SDSZEHTZ, @hatjwe |
| GHSA-48vm-qqx3-7w83 | High | Web basic auth credentials were compared in non-constant time. | @iaohkut-from-NightWolf-Team |
| GHSA-chm7-fjpm-927x | Medium | The comment -var-file allowlist could be bypassed, allowing arbitrary file reads on the Atlantis server.
| @dashingDragon, @arpitjain099, @kah-ja, @luantq0, @dead1nfluence |
| GHSA-cxvq-66x3-pwhw | Medium | atlantis state rm ignored apply and import requirements.
| @luantq0 |
| GHSA-jxcj-v552-f5q4 | Medium | output: hide on custom run and multienv steps leaked output into PR comments when the step failed.
| @bcarikci-dev |
| GHSA-rx6x-hrg2-g435 | Low | The GitHub App setup callback had no CSRF protection (state parameter).
| @geo-chen |
Thank you to everyone who reported these issues, and to @chenrui333 and @pseudomorph for reviewing every fix.
Other changes
- API:
parallel_planis honored for API plans and drift detection (#6963). - GitLab: Atlantis no longer adopts commit pipelines it doesn't own (#6932).
- Images: Debian curl and openssl pins moved to the trixie-security versions (#6990).
- Docs: pre- and post-workflow hook pages warn against re-evaluating PR-supplied environment variables, and Command Requirements explains that an approval is not tied to a plan (#6965). CDKTF references now point to CDK Terrain (#6902). Architecture governance and feature lifecycle ADRs proposed (#6923).
What's Changed
Breaking Changes 🛠
- feat!: stop bundling Terraform and OpenTofu in the Docker images by @jamengual in #7014
Provider GitLab
- fix(gitlab): don't adopt commit pipelines Atlantis doesn't own by @bronto-rikstv in #6932
Security changes
Documentation
- docs: replace CDKTF with CDK Terrain (CDKTN) by @vincenthsh in #6902
- chore(deps): Bump source-map-js from 1.2.1 to 1.2.2 by @dependabot[bot] in #6958
- chore(deps): update dependency vite to v8.3.3 in package.json (main) by @renovate[bot] in #6969
- chore(deps): update dependency vite to v8.3.4 in package.json (main) by @renovate[bot] in #6971
- chore(deps): update dependency vue to v3.5.43 in package.json (main) by @renovate[bot] in #6970
- chore(deps): update dependency vue to v3.5.44 in package.json (main) by @renovate[bot] in #6986
- feat(api): honor parallel_plan for API plans and drift detection by @lnutimura in #6963
- docs: warn about re-evaluating hook variables and untied approvals by @jamengual in #6965
Dependencies
- chore(deps): update go in testing/dockerfile (main) by @renovate[bot] in #6929
- chore(deps): update alpine docker tag to v3.24.2 in dockerfile (main) by @renovate[bot] in #6931
- chore(deps): update redis:8.6-alpine docker digest to ac2da09 in docker-compose.yml (main) by @renovate[bot] in #6937
- chore(deps): update crate-ci/typos action to v1.50.3 in .github/workflows/website.yml (main) by @renovate[bot] in #6943
- chore(deps): update dependency hashicorp/terraform to v1.16.4 in testdrive/utils.go (main) by @renovate[bot] in #6946
- chore(deps): update dependency open-policy-agent/conftest to v0.70.1 in dockerfile (main) by @renovate[bot] in #6950
- chore(deps): update docker/dockerfile:1 docker digest to 4edf897 in dockerfile (main) by @renovate[bot] in #6954
- chore(deps): update dependency hashicorp/terraform to v1.16.5 in testdrive/utils.go (main) by @renovate[bot] in #6967
- chore(deps): update dependency hashicorp/terraform to v1.16.6 in testdrive/utils.go (main) by @renovate[bot] in #7005
- fix(deps): update aws-sdk-go-v2 monorepo in go.mod (main) by @renovate[bot] in #7008
- fix(deps): update module github.com/bmatcuk/doublestar/v4 to v4.10.3 in go.mod (main) by @renovate[bot] in #7009
Other Changes 🔄
- docs: propose architecture governance and feature lifecycle ADRs by @pseudomorph in #6923
- fix(docker): bump debian curl and openssl pins to trixie-security versions by @jamengual in #6990
- chore(security): add OSS Scanner config and security contact by @jamengual in #6994
- fix(oss-scanner): include the whole checkout in the scanner build context by @jamengual in #7006
New Contributors
- @vincenthsh made their first contribution in #6902
- @lnutimura made their first contribution in #6963
- @bronto-rikstv made their first contribution in #6932
Full Changelog: v0.48.2...v0.49.0