Check OpenTofu provider-cache symlinks before running init

John Burns

An infrastructure repository can look harmless because every .tf file came from version control. That is not the whole execution boundary for tofu init. Before OpenTofu downloads a provider, it examines the local data directory, which is normally .terraform under the root module. If that directory arrived with an untrusted archive, a copied workspace, or a stale CI directory, an existing symlink can redirect writes away from the module tree.

OpenTofu addressed one specific provider-installation case in versions 1.11.7, 1.10.10, and 1.12.0. The project advisory explains that older releases could follow a pre-existing symlink at the package directory OpenTofu was about to populate. The fixed releases instead reject a conflicting existing cache entry. The update is the primary remediation. A small preflight check is still useful around untrusted working directories because it makes the local filesystem condition visible before the initialization step begins.

This guide adds a read-only check for symlinks below the local provider cache, uses a disposable directory to demonstrate both outcomes, and explains the limits of that check. The successful outcome is a pipeline that rejects a prepared cache tree containing a symlink, while an ordinary empty cache tree passes. The commands do not invoke a provider, contact a cloud account, or modify infrastructure.

Identify the data directory before scanning it

OpenTofu keeps working data in .terraform by default, including provider installation data. That location is not fixed when the runner sets TF_DATA_DIR; in that case OpenTofu uses the configured directory instead. Scanning only ./.terraform while CI uses TF_DATA_DIR creates a false sense of coverage.

Start by printing the effective choice and checking the installed binary:

tofu version
printf 'TF_DATA_DIR=%s\n' "${TF_DATA_DIR:-<unset; .terraform is used>}"

Use a maintained release line containing the advisory fix. The upstream advisory names 1.11.7 and 1.10.10 as patched releases and notes that the fix was included in 1.12.0. Do not infer that any newer-looking binary is safe from its filename alone: record the result from tofu version in the job log and use the version policy already approved for the repository.

The scan below intentionally accepts the data directory as an argument. That makes a CI configuration explicit and avoids silently checking a developer’s current directory when the actual tofu init command is pointed elsewhere.

#!/usr/bin/env sh
set -eu

data_dir=${1:-"${TF_DATA_DIR:-.terraform}"}
provider_dir="$data_dir/providers"

if [ ! -e "$provider_dir" ]; then
  printf 'PASS: provider cache does not exist yet: %s\n' "$provider_dir"
  exit 0
fi

first_link=$(find "$provider_dir" -type l -print -quit)
if [ -n "$first_link" ]; then
  printf 'FAIL: provider-cache symlink found: %s\n' "$first_link" >&2
  exit 1
fi

printf 'PASS: no provider-cache symlink found below %s\n' "$provider_dir"

Save this as scripts/check-tofu-provider-cache-symlinks.sh in the infrastructure repository, make it executable, and run it immediately before initialization:

chmod 0755 scripts/check-tofu-provider-cache-symlinks.sh
scripts/check-tofu-provider-cache-symlinks.sh "${TF_DATA_DIR:-.terraform}"
tofu init -input=false

The command uses find -type l, which reports symbolic links without resolving them. -print -quit limits output to the first finding, so a CI log does not turn an unexpected filesystem tree into a detailed inventory. A missing provider directory passes because a first initialization has no local provider cache to inspect. A finding exits before tofu init runs.

This is deliberately narrower than a generic “no symlinks in the repository” rule. OpenTofu’s advisory says that the affected location is the provider cache, and legitimate projects can use links for other build inputs. If a repository has a stronger rule about its checkout contents, enforce that separately and describe its intended exceptions.

Exercise both sides in a disposable tree

Do not test a guard by placing a link in a working infrastructure directory. A temporary directory is enough to prove that the detector distinguishes a normal cache layout from the condition it is meant to block.

Create an empty cache-shaped directory first:

work=$(mktemp -d)
mkdir -p "$work/clean/.terraform/providers/registry.opentofu.org/example/demo/1.0.0/linux_amd64"
./scripts/check-tofu-provider-cache-symlinks.sh "$work/clean/.terraform"

The validation run on Linux amd64 printed:

PASS: no provider-cache symlink found below /tmp/.../clean/.terraform/providers

That output proves only that the guard accepts directories. It does not prove that OpenTofu can initialize a real module or that a provider package is trustworthy. Those are separate checks for a repository’s dependency lock file, provider source constraints, and normal plan validation.

For the rejection case, create a synthetic link inside a second disposable cache tree. Point it at a temporary path; do not point it at a home directory, system path, or a repository containing data you care about.

mkdir -p "$work/link/.terraform/providers/registry.opentofu.org/example/demo/1.0.0"
ln -s /tmp "$work/link/.terraform/providers/registry.opentofu.org/example/demo/1.0.0/linux_amd64"
./scripts/check-tofu-provider-cache-symlinks.sh "$work/link/.terraform"

The same validation run stopped with the expected result:

FAIL: provider-cache symlink found: /tmp/.../link/.terraform/providers/registry.opentofu.org/example/demo/1.0.0/linux_amd64

The nonzero exit is the desired behavior. In CI, let it fail the job rather than deleting the link automatically. Automatic removal can hide why a workspace was contaminated, can destroy a legitimate cache arrangement that needs review, and can make a later retry difficult to investigate. Capture the repository revision, runner image, OpenTofu version, and the redacted failing path in the job log, then start from a fresh trusted checkout.

Remove only the temporary validation directory when you are finished:

rm -rf "$work"

The variable is created by mktemp -d in the same shell. Do not adapt that cleanup command to a copied path or a shared CI workspace.

Put the check in the right order

For a typical automation job, checkout and trust decisions come first. Run the filesystem preflight before any command that initializes or upgrades providers. Keep credentials out of this job stage when possible; provider installation and backend initialization can have different network and authentication requirements.

- name: Reject unexpected OpenTofu provider-cache links
  run: scripts/check-tofu-provider-cache-symlinks.sh "${TF_DATA_DIR:-.terraform}"

- name: Initialize providers
  run: tofu init -input=false

If the pipeline deliberately preserves TF_DATA_DIR between jobs, treat that directory as an artifact with an owner and retention policy. A cache is not a substitute for a trusted checkout. The OpenTofu advisory specifically calls out directories whose contents an attacker can influence, so a reused workspace deserves the same provenance review as a downloaded build artifact.

The guard also has an important limit. The advisory notes that higher-level symlinks in the provider-cache hierarchy remain a separate concern: a check restricted to the final package directory is not a complete defense against every path arrangement. This script scans every link below providers, which gives a broader local signal, but it cannot establish that all parent paths, global caches, provider mirrors, or the checkout itself are trustworthy. Keep the OpenTofu upgrade in place and run initialization only from a trusted root module.

Verify the operational result

A useful preflight produces one of three understandable outcomes. A missing provider directory is normal for a fresh checkout. A provider tree with no links passes and permits the normal tofu init step. Any detected link fails before initialization and leaves a short path for investigation.

After a normal initialization, rerun the check once to understand whether the organization’s selected cache configuration creates links intentionally. If it does, do not weaken the script and call that a fix. Review the cache design, the OpenTofu version, and the trust boundary for the directory. The project advisory explains that links can be legitimate when a local cache refers to a configured global cache, which is exactly why a preflight must be paired with an explicit policy instead of used as a blind cleanup tool.

The practical result is modest but valuable: tofu init does not become the first command to discover a suspicious provider-cache layout. A maintained OpenTofu release rejects the advisory’s conflicting-entry condition, and a small read-only guard gives CI an earlier, reviewable failure when a supposedly clean workspace contains a provider-cache symlink.

Sources