github runatlantis/atlantis v0.49.0

2 hours ago

Atlantis v0.49.0

v0.49.0 is a breaking release, for three reasons:

  1. The Docker images no longer include Terraform or OpenTofu. This was announced in v0.48.0.
  2. 12 security advisories are fixed here, and several of those fixes change behavior that existing setups may rely on.
  3. 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 opentofu if 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-hostname matches 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: true together with allowed_workflows, check that every project's workflow is in the allow-list. Repo configs that used to pass can now be rejected.
  • Project dirs: make sure no project dir is a symlink to somewhere outside the repository.
  • Comment commands: tell users that -out, -generate-config-out, -state, -state-out and -backup are now blocked after --, and that --blocked-extra-args can no longer unblock anything.
  • Comment -var-file: the allowlist now applies to apply, import and state rm as well as plan. Paths containing .. are refused, and so are repo symlinks that point outside the repository and the allowlist.
  • state rm: if state is in --allow-commands, state rm now has to meet the project's apply_requirements and import_requirements.
  • API: if --api-secret is set, send X-Atlantis-Token on GET /api/locks. Check that /api/plan and /api/apply callers send clean, repo-relative paths[].directory values 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 plan and apply is 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.Z and vX.Y.Z-debian images 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-slim and -debian-slim images from v0.48.0 were the intermediate step toward this and are no longer published. If you pinned one, switch to the unsuffixed, -alpine or -debian tag, which now contain the same thing. If you build the image yourself, use --target alpine or --target debian; the -slim targets 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 terraform or tofu binary on PATH, 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 Helm defaultTFVersion) so Atlantis downloads that version at startup. Repos can still pin their own version with terraform_version.
    • For OpenTofu, also set --default-tf-distribution opentofu (Helm defaultTFDistribution).
    • If the server can't reach the release hosts, put a binary on PATH yourself: mount one, or build FROM ghcr.io/runatlantis/atlantis and copy one in. Set --tf-download=false so Atlantis uses it and doesn't try to download.
    • See Deployment: image variants.

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_args entry is passed as exactly one argument. Shell operators (;, &&, |, redirections), globbing and word splitting no longer apply.
    • Environment references such as $WORKSPACE and $DIR are still expanded in operator-configured extra_args, but no longer in arguments typed in a PR comment after --.
    • For conftest, a policy set path and each extra_args entry are still split on whitespace. Quote a path that contains a space.
  • Who is affected:
    • Workflows whose extra_args relied on shell behavior, for example two flags in one entry, a pipe, a redirection or a glob.
    • Users who passed $VAR references in comment arguments and expected them to expand.
  • What to do: split multi-flag entries into one argument per entry. Move anything that needs a shell into a run step, which still runs with a shell by design.

Workspace names are validated everywhere

  • What changed: workspace names from repo atlantis.yaml, the -w comment 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 workspace key, or the comment is rejected with the offending value and a reason such as cannot 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 the allowed_workflows check 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-empty allowed_workflows, whose atlantis.yaml has a later project naming a server-side workflow that is not in allowed_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_workflows in 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 example dir -> 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 with resolving 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 dir symlink keeps the existing dir "<dir>" does not exist error. 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 -chdir and -plugin-dir were blocked.
    • Flags are matched by name. One entry blocks both the - and -- spellings and the =value form.
    • --blocked-extra-args now adds to the built-in list. Before, setting it replaced the defaults, so an operator could unblock -chdir or -plugin-dir that way. That is no longer possible.
  • 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 for import blocks.
    • Operators who set --blocked-extra-args to allow one of the default flags.
    • Users who pass Terraform's own -state or -backup to atlantis state rm in comments. Those are blocked too.
  • How to tell: the comment is rejected with flag "<arg>" is not allowed in extra args.
  • What to do:
    • To use -generate-config-out or the other flags, put them in extra_args of a workflow you control (server-side, or a repo workflow the server allows). Workflow extra_args are not checked.
    • If you listed both spellings in --blocked-extra-args, you can drop the -- entries. Nothing breaks if you leave them.

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 on apply, import and state rm, not only plan.
    • It now catches --var-file and values that contain =.
    • Any value containing a .. element is refused, even if it would resolve inside the repository (for example envs/../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-file in comment extra args is checked. Workflow extra_args are not.
  • Who is affected: users who pass -var-file in 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 '..' elements
    • var 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 rm used to skip command requirements entirely. It now has to meet every requirement in the project's apply_requirements plus every one in its import_requirements. There is no separate state_rm_requirements key.
  • Who is affected: servers with state in --allow-commands and projects that set apply_requirements or import_requirements (approved, mergeable, undiverged, policies_passed).
  • How to tell: state rm is refused with the usual requirement messages, ending in before 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, approved and mergeable count 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-secret set. Web basic auth never covered /api/ paths.
  • Who is affected: scripts and dashboards that call GET /api/locks without a token on servers that have --api-secret set. If no API secret is set, the endpoint stays open.
  • How to tell: HTTP 401 with error code UNAUTHORIZED and message invalid 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: ./dir and dir/ both become dir. An empty directory is still allowed.

  • Who is affected: API callers that send any of these. Before v0.49.0:

    • /modules/vpc ran in modules/vpc but didn't match that directory's atlantis.yaml project, 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 (default github.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.
  • Who is affected:
    • GitHub Enterprise Server or GHE Cloud setups where --gh-hostname is 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).
  • 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 command at warn level.
  • What to do: set --gh-hostname to 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] (and import_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 plan step, Atlantis records a SHA-256 of the plan file with the project's plan status.
    • apply rejects a plan file that no longer matches that hash, and deletes the file.
    • A plan whose built-in plan step 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 run steps in the plan stage can still post-process the file.
    • Plan stages made only of custom run steps record no hash and are not checked. Plans recorded before the upgrade carry no hash and are still accepted.
  • Who is affected: workflows, pre_workflow_hooks or tooling that rewrite or replace the plan file between atlantis plan and atlantis apply. Workflows whose plan stage includes plan but 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>".
  • What to do: run atlantis plan again. 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: PlanHash is a new field in stored project status and in PlanSuccess API 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/setup now issues a one-time state nonce and an HttpOnly cookie, both valid for 10 minutes. /github-app/exchange-code only 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/setup or GitHub 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-auth while 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: hide on custom run and multienv steps now also hides output when the step fails, in both the PR comment and the job message. The multienv parse error no longer echoes the offending line; it now reads like invalid environment variable definition: misquoted at offset 8. env steps 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_plan is 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

Security changes

  • chore(deps): update module golang.org/x/net to v0.60.0 [security] (main) by @renovate[bot] in #6968

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

Full Changelog: v0.48.2...v0.49.0

Don't miss a new atlantis release

NewReleases is sending notifications on new releases.