Encryption with dotenvx and SOPS¶
envdrift supports encrypted .env files with dotenvx (default)
and SOPS.
Supported backends:
- dotenvx: Simple CLI-managed encryption with
.env.keys - SOPS: KMS/age/PGP-backed encryption with
.sops.yamlpolicies
Why Encrypt?¶
- Commit secrets safely - Encrypted
.envfiles can be committed to git - No more secret sharing - Team members decrypt locally with their keys
- Audit trail - Git history shows who changed what
Quick Start¶
Check Encryption Status¶
Output:
╭────────────────────────── envdrift encrypt --check ──────────────────────────╮
│ Encryption Status: .env.production │
╰──────────────────────────────────────────────────────────────────────────────╯
File is partially encrypted
Variables:
Encrypted: 3
Plaintext: 3
Empty: 0
Encryption ratio: 50%
PLAINTEXT SECRETS DETECTED:
* AWS_ACCESS_KEY_ID
WARNINGS:
* 'AWS_ACCESS_KEY_ID' has a value that looks like a secret
Recommendation:
Run: envdrift encrypt .env.production
Encrypt a File¶
If encryption.dotenvx.auto_install is enabled, envdrift installs dotenvx and encrypts the file.
For SOPS:
Decrypt for Development¶
For SOPS, ensure your key source is available (for example, set
SOPS_AGE_KEY_FILE=keys.txt) and run:
How dotenvx Works¶
- dotenvx binary - envdrift can auto-install the dotenvx binary to
.venv/bin/when enabled; the download is SHA256-verified against the upstream release checksums (fail closed) - Encryption - Uses public-key (ECIES) encryption so encrypted
.envfiles can be committed; only the private key in.env.keyscan decrypt - Key management - Keys stored in
.env.keys(never commit this!)
Dotenvx File Structure¶
After encryption:
Your .gitignore should include:
Dotenvx Encrypted File Format¶
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
DOTENV_PUBLIC_KEY_PRODUCTION="03abc123..."
# .env.production
DATABASE_URL=encrypted:BDQE1234567890abcdef...
API_KEY=encrypted:BDQEsecretkey123456...
DEBUG=encrypted:BDQEfalse1234567890...
#/----------------------------------------------------------/
Dotenvx encrypts every value in the file, including non-secret configuration
such as DEBUG. Use envdrift's partial encryption flow
when selected values must remain readable.
SOPS Encrypted File Format¶
SOPS encrypts values in place while keeping keys readable:
DATABASE_URL="ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]"
API_KEY="ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]"
SOPS relies on your chosen key management system (age, KMS, PGP, etc.) and a
.sops.yaml configuration in the repo.
SOPS Configuration¶
Use envdrift config to set SOPS defaults. Auto-install is opt-in; set
auto_install = true if you want envdrift to download the binary for you
(downloads are SHA256-verified against the upstream release checksums and the
install fails closed when verification is not possible):
[encryption]
backend = "sops"
[encryption.sops]
auto_install = false
config_file = ".sops.yaml"
age_key_file = "keys.txt"
age_recipients = "age1example"
# kms_arn = "arn:aws:kms:..."
# gcp_kms = "projects/.../locations/.../keyRings/.../cryptoKeys/..."
# azure_kv = "https://myvault.vault.azure.net/keys/my-key"
When decrypting locally, set your age private key:
Schema Integration¶
Mark sensitive fields in your schema for better detection:
from pydantic import Field
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = Field(json_schema_extra={"sensitive": True})
API_KEY: str = Field(json_schema_extra={"sensitive": True})
DEBUG: bool = False # Not sensitive
Then check:
Pre-commit Hook¶
Block unencrypted secrets from being committed:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: envdrift-encryption
name: Check env encryption
entry: envdrift encrypt --check
language: system
files: ^\.env\.(production|staging)$
pass_filenames: true
- id: envdrift-guard
name: Guard staged env files
entry: envdrift guard --staged --native-only --ci
language: system
always_run: true
pass_filenames: false
Use the guard hook when you rely on [vault.sync].mappings.env_file; it reads
the TOML mapping and blocks custom dotenv filenames such as postgresql.env.
Dotenvx Key Management¶
Development¶
Store keys locally in .env.keys:
CI/CD¶
Pass the key as an environment variable:
Production¶
Use a secrets manager (Azure Key Vault, AWS Secrets Manager, etc.) to store the private key:
import os
from envdrift.vault import get_vault_client
vault = get_vault_client("azure", vault_url="https://myvault.vault.azure.net/")
key = vault.get_secret("dotenv-private-key-production")
# Set as environment variable before running app
os.environ["DOTENV_PRIVATE_KEY_PRODUCTION"] = key.value
SOPS does not use .env.keys; key management lives in your SOPS setup
(age key files, KMS, or PGP) and the .sops.yaml policy.
Troubleshooting¶
Windows: .env.local encryption (resolved in dotenvx v2)¶
Older dotenvx (v1) had a Windows bug where a file named exactly .env.local
failed with:
because it parsed the LOCAL suffix as a hex string
(dotenvx#724).
The pinned dotenvx v2 fixes this. .env.local — and other suffixes that
previously tripped the bug — now encrypt and decrypt natively on Windows, with no
rename workaround required.
Cross-Platform Line Endings¶
envdrift automatically normalizes line endings (CRLF → LF) before encryption to ensure files encrypted on Windows can be decrypted on Linux/macOS and vice versa.
"dotenvx not found"¶
If auto-install is enabled, the binary is downloaded automatically; if that fails:
To let envdrift download the binary, enable auto-install in envdrift.toml and run
a real (non---check) encrypt:
Otherwise, install dotenvx manually following the install instructions envdrift prints when the binary is missing.
"Decryption failed"¶
- Check
.env.keysexists - Verify the key matches the encrypted file
- Check
DOTENV_PRIVATE_KEY_*environment variable is set
"sops not found"¶
Install SOPS and retry:
"SOPS decryption failed"¶
- Confirm
SOPS_AGE_KEY_FILE(orSOPS_AGE_KEY) is set for age - Verify access to your KMS/PGP keys
- Ensure
.sops.yamlrules match the file you're decrypting