Skip to content

SOPS Backend Guide

envdrift supports two encryption backends: dotenvx (default) and SOPS. This guide is the end-to-end reference for the SOPS path — how it differs from dotenvx, why SOPS users don't use envdrift's Vault Sync, and exactly how to set it up against a cloud KMS (with a full Azure Key Vault walkthrough).

If you're choosing between backends first, read Encryption Backends. If you want the shared encrypt/decrypt mechanics, see Encryption.

The core difference: who distributes the decrypt key

This is the single most important thing to understand, because it changes which commands you use.

dotenvx SOPS
Decrypt key lives in a portable .env.keys file (DOTENV_PRIVATE_KEY_<ENV>) your KMS / age / PGP — there is no .env.keys
How teammates get decrypt access envdrift Vault Sync pushes the key to a cloud vault and pulls it back you grant access in KMS/age/PGP itself (e.g. an Azure RBAC role on the key)
Team commands sync, pull, vault-push, vault-pull, decrypt --verify-vault not needed — use encrypt / decrypt

dotenvx has one secret artifact (.env.keys) that must reach every teammate, so envdrift gives you Vault Sync to distribute it. SOPS has no such portable secret: it wraps each file's data key with your KMS/age/PGP key, and that system already decides who can decrypt. So with SOPS you never push a key anywhere — you add a teammate to the KMS key (or hand them an age key) and they can decrypt immediately.

SOPS does not use Vault Sync — and doesn't need to

The entire Vault Sync family (vault-push, vault-pull, sync, and decrypt --verify-vault) is built around the dotenvx .env.keys artifact and is dotenvx-only. decrypt --verify-vault explicitly rejects non-dotenvx backends. SOPS access control lives in your KMS/age/PGP, not in an envdrift vault mapping.

Two ways to use vault clouds — don't confuse them

The word "vault" means two different things here:

  1. envdrift Vault Sync — pushes/pulls the dotenvx .env.keys private key using your cloud vault's secrets API (e.g. Azure Key Vault secrets). dotenvx only.
  2. A cloud KMS as the SOPS key — SOPS wraps the file's data key with a cloud key (e.g. Azure Key Vault keys, AWS KMS, GCP KMS). This is SOPS-native.

So SOPS absolutely uses cloud vaults — just through the keys API via SOPS's own config, not through envdrift's key-pushing commands. The permission sets are different: dotenvx Vault Sync needs Secrets Get/List; SOPS + Azure Key Vault needs Keys crypto permissions (see below).

Install

pip install envdrift          # SOPS support needs no extra envdrift extras

You also need the sops binary on PATH. Either install it yourself (brew install sops, or see the SOPS releases) or let envdrift fetch it by setting auto_install = true (below). The auto-install download is bounded by a 60-second timeout, and a failed attempt reports its cause (e.g. the unreachable URL) alongside the manual install instructions.

Configuration

Set SOPS as the backend in envdrift.toml (or [tool.envdrift] in pyproject.toml). All SOPS options live under the [encryption.sops] subsection:

[encryption]
backend = "sops"

[encryption.sops]
auto_install = false              # set true to let envdrift download the sops binary
config_file = ".sops.yaml"        # optional: path to a SOPS policy file
age_recipients = "age1example..." # age public key(s) for encryption
age_key_file = "keys.txt"         # age private key for local decryption (sets SOPS_AGE_KEY_FILE)
# kms_arn  = "arn:aws:kms:us-east-1:123456789:key/abc-123"
# gcp_kms  = "projects/p/locations/global/keyRings/r/cryptoKeys/k"
# azure_kv = "https://my-vault.vault.azure.net/keys/my-key/<version>"

Use the [encryption.sops] subsection — not flat keys

Options are read from the [encryption.sops] table (config_file, age_recipients, azure_kv, …). Flat keys like sops_config_file under [encryption] are ignored by the parser.

The simple path: encrypt / decrypt

For SOPS this is all you need — no vault mappings, no extra config sections.

# Encrypt in place (backend + key resolved from envdrift.toml)
envdrift encrypt .env.production

# Or pick the backend/key explicitly, ignoring config
envdrift encrypt .env.production --backend sops --age age1example...

# Decrypt in place (backend auto-detected from the file's SOPS metadata)
envdrift decrypt .env.production

SOPS encrypts values while keeping keys readable and appends its own metadata:

DATABASE_URL=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
API_KEY=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
sops_version=3.13.1

Flags differ by direction (both override config):

  • encrypt--backend, --sops-config, --age, --age-key-file, --kms, --gcp-kms, --azure-kv. The key-recipient flags (--age/--kms/--gcp-kms/ --azure-kv) choose who can decrypt and are encryption-only.
  • decrypt--backend, --sops-config, --age-key-file. Decryption needs no key-recipient flags: SOPS reads the recipients from the file's metadata and unwraps the data key using your credentials (az login, KMS access, or the age key).

See encrypt and decrypt.

Re-running encrypt on an already-encrypted file

envdrift encrypt verifies the post-state instead of trusting the SOPS metadata header:

  • Fully encrypted, recipients unchanged — a clean no-op: exit 0 with already encrypted (no change) (not a misleading Encrypted banner).
  • A plaintext value survives (e.g. a line appended after encryption) — the command fails and names the offending key(s). SOPS itself refuses to re-encrypt a file that already carries metadata, so the value would otherwise leak silently. Recover with sops edit <file>, or move the plaintext line aside, envdrift decrypt, re-add it, then envdrift encrypt. envdrift lock applies the same check and will not report "ready to commit" over surviving plaintext.
  • A requested recipient is missingencrypt --age <new key> (or --kms / --gcp-kms / --azure-kv) on an already-encrypted file fails instead of silently ignoring the flag (which would leave the new recipient unable to decrypt). Matching is exact: a key that is merely a prefix of a recorded recipient, or an Azure Key Vault URL whose components only appear inside unrelated metadata, does not count as present. envdrift lock runs the same check against the recipients configured in envdrift.toml, so adding a key to age_recipients and re-running lock fails loudly until the file's metadata actually includes it. Add recipients with sops rotate --add-age <key> -i --input-type dotenv --output-type dotenv <file>, or sops updatekeys after editing .sops.yaml.
  • Encrypted by the other backendencrypt refuses to double-encrypt a dotenvx-encrypted file with SOPS (and vice versa); decrypt with the original backend first.

Key resolution precedence

An explicit --age-key-file flag or TOML age_key_file overrides an ambient SOPS_AGE_KEY_FILE environment variable. Exporting the env var for day-to-day work never disables an explicitly supplied key. (An inline age_key — a programmatic backend parameter, not a TOML key — likewise outranks an ambient SOPS_AGE_KEY.)

Azure Key Vault walkthrough (verified end-to-end)

This sets up SOPS to wrap your data keys with an Azure Key Vault key. envdrift authenticates to Azure via SOPS, which uses the standard Azure credential chain (az login, env vars, or managed identity).

1. Permissions — Key Vault keys, not secrets

SOPS uses the vault's keys API. This is a different RBAC role than dotenvx Vault Sync (which uses secrets). On the vault, grant:

Task Built-in role
Create the key (one-time) Key Vault Crypto Officer
Encrypt / decrypt files (everyone) Key Vault Crypto User
# Replace <user-or-principal> and the scope with your vault's resource ID
SCOPE=$(az keyvault show --name my-vault --query id -o tsv)

az role assignment create --assignee "<user-or-principal>" \
  --role "Key Vault Crypto Officer" --scope "$SCOPE"   # to create keys
az role assignment create --assignee "<user-or-principal>" \
  --role "Key Vault Crypto User"   --scope "$SCOPE"   # to encrypt/decrypt

RBAC changes take ~30–60s to propagate. If you only hold Key Vault Secrets Officer (the dotenvx Vault Sync role), az keyvault key create will fail with Forbidden (ForbiddenByRbac) — that's the secrets-vs-keys distinction in action.

2. Create the key

az keyvault key create --vault-name my-vault --name sops-key \
  --kty RSA --size 2048 --query "key.kid" -o tsv
# -> https://my-vault.vault.azure.net/keys/sops-key/<version>

3. Point envdrift at it

[encryption]
backend = "sops"

[encryption.sops]
azure_kv = "https://my-vault.vault.azure.net/keys/sops-key/<version>"

4. Encrypt and decrypt

envdrift encrypt .env.production       # wraps the data key with your Azure key
envdrift decrypt .env.production       # calls Azure to unwrap, restores plaintext

The encrypted file records which key was used, so decryption needs no extra config — only az login (or any Azure credential with Key Vault Crypto User on the key):

sops_azure_kv__list_0__map_vault_url=https://my-vault.vault.azure.net
sops_azure_kv__list_0__map_name=sops-key
sops_azure_kv__list_0__map_version=<version>

Granting a teammate decrypt access

No key push. Give them Key Vault Crypto User on the key (or vault) and they can envdrift decrypt immediately:

az role assignment create --assignee teammate@example.com \
  --role "Key Vault Crypto User" --scope "$SCOPE"

Using pull and lock with SOPS

Partly — and this trips people up, so here is the precise behavior.

pull and lock are part of the Vault Sync family. They do two things:

  1. A key-sync step (fetch/verify the dotenvx .env.keys private key from a vault) — this is dotenvx-only. pull runs it by default; lock runs it only with --verify-vault or --sync-keys.
  2. An encrypt/decrypt step that uses whichever backend your config resolves to — this does support SOPS.

Consequences for SOPS:

  • lock and pull refuse to start without a [vault.sync] section (and a [vault] provider block) — they derive their file work-list and build a vault client up front, regardless of backend. The provider block needs that provider's own required field: vault_url for Azure/HashiCorp, region for AWS (defaulted), project_id for GCP. A pure-SOPS project has none of this.
  • lock does not hit Vault Sync by default (it only encrypts), so lock --force works for SOPS once the config above exists. Plain envdrift pull, however, runs the key-sync step, looks for a dotenvx secret, and fails for SOPS — use envdrift pull --skip-sync to go straight to decryption.
  • pull hands a mixed SOPS file (metadata plus a plaintext value appended after encryption) to sops instead of skipping it as "not encrypted": the run fails loudly (sops reports the integrity error) rather than exiting 0 with the values still ciphertext.

Recommendation: for SOPS, use envdrift encrypt / envdrift decrypt. They need no vault scaffolding and are the intended SOPS workflow. Only reach for lock/pull if you specifically want their batch file-discovery, in which case add a minimal [vault.sync]/[vault] block and run pull --skip-sync.

# Minimal scaffolding ONLY if you want lock/pull to drive SOPS encrypt/decrypt.
# No key is ever pushed/pulled here — access is your KMS/age/PGP.
[vault]
provider = "azure"

[vault.azure]
vault_url = "https://my-vault.vault.azure.net/"

[vault.sync]
default_vault_name = "my-vault"

[[vault.sync.mappings]]
secret_name = "unused-for-sops"   # never read for SOPS; only gives a work-list
folder_path = "."
environment = "production"
envdrift lock --force          # encrypts mapped files with SOPS
envdrift pull --skip-sync      # decrypts mapped files with SOPS (no secrets call)

What SOPS does not support

  • envdrift Vault Sync (vault-push, vault-pull, sync, decrypt --verify-vault) — dotenvx-only by design.
  • Partial encryption (push / pull-partial) — dotenvx-only.

See also