Skip to content

Repository files navigation

tfvault — Universal Terraform credentials helper

tfvault

A universal Terraform credentials helper with pluggable secret backends and per-file account isolation.

Terraform asks the helper for a token whenever it talks to a Terraform-native service — Terraform Cloud/Enterprise, Scalr, Spacelift, private module/provider registries — keyed by hostname. tfvault answers from the backend you configure:

Backend Storage Writable
keyring OS keyring (macOS Keychain, Linux Secret Service) yes
pass pass / gopass password store yes
op 1Password via the op CLI yes
env environment variables (TF_TOKEN_* encoding) read-only

Planned: AWS Secrets Manager, SSM Parameter Store, HashiCorp Vault. Each backend is one Go package + one registration line.

Supported platforms: macOS and Linux, amd64 and arm64.

Install

Homebrew (macOS and Linux):

brew install tedilabs/tap/tfvault

Or mise, via either the github or the aqua backend (both resolve the same release archives; the aqua backend adds checksum verification through the aqua registry):

mise use -g github:tedilabs/tfvault
# or
mise use -g aqua:tedilabs/tfvault

tfvault install   # link the helper into ~/.terraform.d/plugins

For mise installs, tfvault install detects the mise-managed path and writes a small wrapper that execs the mise shim instead of symlinking the versioned binary: upgrading tfvault via mise never leaves a stale plugin link, and per-directory version pins are honored.

Or the install script:

curl -fsSL https://raw.githubusercontent.com/tedilabs/tfvault/main/install.sh | sh

The script installs the tfvault binary into ~/.local/bin and runs tfvault install, which symlinks it into ~/.terraform.d/plugins/ as terraform-credentials-tfvault — the name Terraform discovers helpers by. Or manually: download a release archive, verify checksums.txt, put tfvault on your PATH and run tfvault install. From source:

go build ./cmd/tfvault
install -m 0755 tfvault ~/.local/bin/
tfvault install

Quick start

Add to ~/.terraformrc:

credentials_helper "tfvault" {
  args = []
}

That's it — no config file needed. Tokens go into your OS keyring under the service name tfvault:

terraform login app.terraform.io    # store a token
terraform logout app.terraform.io   # forget it

tfvault status shows whether the plugin link and .terraformrc are wired up and which profile and backend requests resolve to.

The helper works for any Terraform-native service hostname, not just Terraform Cloud.

Multiple accounts on one machine

The core feature: different .terraformrc files can use different credential sets via profiles. Define profiles in ~/.config/tfvault/config.yaml:

default_profile: personal

profiles:
  personal:
    backend: keyring
    options:
      service: tfvault-personal
  customer-a:
    backend: keyring
    options:
      service: tfvault-customer-a
  customer-b:
    backend: pass
    options:
      binary: gopass
      prefix: customers/b/terraform
      store_dir: ~/.password-store-customer-b
  ci:
    backend: env
    options:
      prefix: CI_TF_TOKEN_

Create one .terraformrc per account:

# ~/.terraformrc-customer-a
credentials_helper "tfvault" {
  args = ["--profile", "customer-a"]
}

Then select it per shell, per direnv, or per invocation:

export TF_CLI_CONFIG_FILE=~/.terraformrc-customer-a
terraform plan

Or run a single command against a profile without a per-account .terraformrc at all:

tfvault exec --profile customer-a -- terraform plan

The same hostname (e.g. app.terraform.io) resolves to different tokens in different profiles because each profile points at its own storage location.

Configuration reference

Config file lookup order:

  1. --config <path> (set via args in the credentials_helper block)
  2. $TFVAULT_CONFIG
  3. $XDG_CONFIG_HOME/tfvault/config.yaml, falling back to ~/.config/tfvault/config.yaml

If no config file exists, the implicit default profile uses the keyring backend with service: tfvault. Requesting any other named profile without a config file is an error — a named profile implies isolation you set up on purpose, so tfvault never falls back to shared storage.

Top-level settings besides default_profile:

color: false # disable colored output from auxiliary commands (optional)
editor: vim # editor for "tfvault config edit"; takes precedence over $EDITOR (optional)

Each entry under profiles names exactly one backend and passes the keys under options to it:

keyring

profiles:
  example:
    backend: keyring
    options:
      service: tfvault # keyring service name (default "tfvault")

Entries are stored as (service, hostname). On Linux this requires a running Secret Service daemon (gnome-keyring, KWallet); on headless machines use the pass backend instead.

pass

profiles:
  example:
    backend: pass
    options:
      binary: pass # or "gopass", or an absolute path (default "pass")
      prefix: terraform # entry path: <prefix>/<hostname> (default "terraform")
      store_dir: ~/.password-store # sets PASSWORD_STORE_DIR for per-profile stores (optional)

Tokens are exchanged with the child process via stdin/stdout only, never argv. Both pass and gopass are supported and integration-tested.

op (1Password)

profiles:
  example:
    backend: op
    options:
      vault: Work # 1Password vault name (optional; default vault when omitted)
      account: my.1password.com # for multiple 1Password accounts (optional)
      prefix: tfvault/ # item title prefix: <prefix><hostname> (default "tfvault/")
      binary: op # or an absolute path (default "op")

Requires the 1Password CLI (v2) with any of its auth methods: the desktop-app integration, OP_SERVICE_ACCOUNT_TOKEN, or op signin. Entries are stored as "API Credential" items tagged tfvault, and tokens are exchanged with the op process via stdin/stdout only — never argv. Different profiles can point at different vaults or accounts for per-client isolation.

env (read-only)

profiles:
  example:
    backend: env
    options:
      prefix: TF_TOKEN_ # default

Looks up <prefix><encoded-hostname> where . becomes _ and - becomes __ (Terraform's native TF_TOKEN_* encoding), e.g. TF_TOKEN_app_terraform_io. terraform login against an env profile fails with a clear error since the backend cannot write.

Note: Terraform ≥ 1.2 reads TF_TOKEN_* variables natively without any helper. The env backend is useful for the prefix override case (CUSTOMER_A_TF_TOKEN_*) where profiles select among variable sets.

Auxiliary commands

tfvault install                     # symlink the helper into ~/.terraform.d/plugins
tfvault status                      # plugin link, terraformrc and profile resolution
tfvault exec --profile a -- tf plan # run a command with the helper wired to a profile
tfvault config show                 # effective configuration and where each value comes from
tfvault config edit                 # open the config file in your editor
tfvault profiles                    # list profiles, default marked with *
tfvault --profile customer-b list   # hostnames with stored credentials
tfvault completion bash             # completion script (bash, zsh, fish)
tfvault version

Shell completions complete commands, flags and profile names (the latter by invoking tfvault profiles):

# bash (~/.bashrc)
eval "$(tfvault completion bash)"
# zsh
tfvault completion zsh > "${fpath[1]}/_tfvault"
# fish
tfvault completion fish > ~/.config/fish/completions/tfvault.fish

install refuses to overwrite anything at the link path it does not recognize as its own (a symlink or a tfvault-written shim wrapper — e.g. a binary copied by an old installer); pass -f/--force to replace it.

status reads the Terraform CLI config ($TF_CLI_CONFIG_FILE, else ~/.terraformrc) and reports the credentials_helper registration, explicit credentials blocks that bypass the helper, and the profile, backend and stored hostnames the current setup resolves to. It also flags token sources Terraform consults before any helper — TF_TOKEN_* environment variables and plaintext tokens left in ~/.terraform.d/credentials.tfrc.json by terraform login — and, for backends that execute an external CLI (pass, op), checks the binary is actually present.

exec runs one command against one profile. It writes a temporary Terraform CLI config — a copy of the config that would otherwise apply, with the credentials_helper block replaced by one carrying --profile — points TF_CLI_CONFIG_FILE at it and runs the command, then removes it. Terraform asks the helper per hostname exactly as with a permanent registration, so no token ever enters the command's environment, and the temporary file holds nothing but a profile name. Copying the existing config keeps plugin_cache_dir, provider_installation and everything else in effect. Unrelated helper args and the profile registered in your .terraformrc are preserved unless --profile overrides them; the command's exit code becomes tfvault's, and SIGINT/SIGTERM/SIGHUP/SIGQUIT are forwarded to it.

For tools that cannot read the Terraform CLI config (anything built on go-tfe, deploy scripts, curl), --export-host additionally puts a token into the command's environment:

tfvault exec --profile customer-a --export-host app.terraform.io -- ./deploy.sh

That sets TF_TOKEN_app_terraform_io, TFE_HOSTNAME and TFE_TOKEN. The flag is repeatable; every host named gets its TF_TOKEN_* variable, while the TFE_* pair follows the first one. Environment variables are inherited by every process the command spawns — including Terraform's provider plugins and any local-exec — so reach for --export-host only when the tool leaves no choice.

config show prints the effective configuration — the config file with command line flags applied on top — annotating each value with where it came from. config edit opens the config file (created with mode 0600 when missing) in the config editor setting, else $EDITOR, else vi.

list is supported by the pass, op and env backends; OS keyrings cannot enumerate entries. Token values are never printed by any auxiliary command.

Auxiliary command output is colorized when stdout is a terminal. Disable it with --no-color, the NO_COLOR environment variable, or color: false in the config file. Protocol (get/store/forget) and list output is always plain, since other programs consume it.

Caveats

  • Explicit credentials blocks win. Terraform prefers a credentials "<host>" { token = ... } block in the CLI config over the helper. Remove such blocks for hosts the helper should manage.
  • The helper is asked about every Terraform-native service host. Returning {} (no credentials) is normal for public registries like registry.terraform.io; anonymous access continues to work.
  • gopass is CLI-compatible with pass and honors PASSWORD_STORE_DIR; both were verified against real stores.

Security notes

  • Tokens never appear in logs, argv, or child process environments. The one exception is opt-in and explicit: tfvault exec --export-host <host> exports the token for the hosts you name.
  • On protocol commands, stdout carries only protocol JSON; all diagnostics go to stderr.
  • Hostnames are validated before being used as paths, env var names or keyring accounts (no path traversal / argv injection).
  • Ambiguous backend errors on get fail with a nonzero exit instead of returning an empty {} that would mask a broken setup.
  • A world-readable config file produces a warning (it holds no secrets, but paths and service names are better kept private).
  • On macOS, the keyring backend stores items through the security CLI, whose items any process running as the same user can read back without a prompt — comparable to file permissions, weaker than a per-app keychain ACL. Migrating to the native Keychain API together with signed binaries is tracked in #14 and #13.

Development

go test ./...                       # unit + protocol compliance tests
go test -tags integration ./...    # real pass/gopass round trips (needs gpg)
go run github.com/goreleaser/goreleaser/v2@latest release --snapshot --clean

The Terraform credentials helper protocol is documented at developer.hashicorp.com/terraform/internals/credentials-helpers.

Related projects

tfvault is not the first helper in this space, and it is better for it — the maintainers below generously shared production lessons that directly shaped this project's roadmap:

  • terracreds — the broadest helper around: Windows Credential Manager plus cloud vault providers (AWS, Azure, GCP, HashiCorp Vault) across TFC/TFE, Scalr, Spacelift and env0. If you need Windows or a cloud vault today, start there.
  • terraform-credentials-keychain — a focused macOS Keychain helper with signed and notarized release binaries.

Reach for tfvault when you want pluggable local backends with per-profile multi-account isolation on macOS and Linux.

License

Apache License 2.0

About

♥️ A Terraform credentials helper to fetch secrets from several secret backends (OS keyring, environment variables, pass).

Topics

Resources

Code of conduct

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages