Skip to content

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.yaml policies

Why Encrypt?

  • Commit secrets safely - Encrypted .env files 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

envdrift encrypt .env.production --check

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

envdrift encrypt .env.production

If encryption.dotenvx.auto_install is enabled, envdrift installs dotenvx and encrypts the file.

For SOPS:

envdrift encrypt .env.production --backend sops --age age1example

Decrypt for Development

envdrift decrypt .env.production

For SOPS, ensure your key source is available (for example, set SOPS_AGE_KEY_FILE=keys.txt) and run:

envdrift decrypt .env.production --backend sops

How dotenvx Works

  1. 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)
  2. Encryption - Uses public-key (ECIES) encryption so encrypted .env files can be committed; only the private key in .env.keys can decrypt
  3. Key management - Keys stored in .env.keys (never commit this!)

Dotenvx File Structure

After encryption:

.env.production          # Encrypted (safe to commit)
.env.keys                 # Private keys (NEVER commit!)

Your .gitignore should include:

.env.keys

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:

export SOPS_AGE_KEY_FILE=keys.txt
envdrift decrypt .env.production --backend sops

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:

envdrift encrypt .env.production --check --schema config.settings:Settings

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:

# .env.keys
DOTENV_PRIVATE_KEY_PRODUCTION="abc123..."

CI/CD

Pass the key as an environment variable:

# GitHub Actions
env:
  DOTENV_PRIVATE_KEY_PRODUCTION: ${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}

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:

Input string must contain hex characters in even length

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:

# Check if binary exists
ls .venv/bin/dotenvx

To let envdrift download the binary, enable auto-install in envdrift.toml and run a real (non---check) encrypt:

[encryption.dotenvx]
auto_install = true
envdrift encrypt .env.production

Otherwise, install dotenvx manually following the install instructions envdrift prints when the binary is missing.

"Decryption failed"

  1. Check .env.keys exists
  2. Verify the key matches the encrypted file
  3. Check DOTENV_PRIVATE_KEY_* environment variable is set

"sops not found"

Install SOPS and retry:

brew install sops

"SOPS decryption failed"

  1. Confirm SOPS_AGE_KEY_FILE (or SOPS_AGE_KEY) is set for age
  2. Verify access to your KMS/PGP keys
  3. Ensure .sops.yaml rules match the file you're decrypting