E5C4 SOPS Encryption · sysid/rs-env Wiki · GitHub
[go: up one dir, main page]

Skip to content

SOPS Encryption

sysid edited this page Jan 15, 2026 · 5 revisions

SOPS Encryption

rsenv integrates with SOPS to encrypt sensitive files in your vault. This enables secure backup, version control, and sharing of encrypted vaults.

Overview

SOPS (Secrets OPerationS) encrypts file contents while preserving structure. rsenv provides batch operations:

Command Purpose
rsenv sops encrypt Encrypt files matching configured patterns
rsenv sops decrypt Decrypt .enc files
rsenv sops clean Delete plaintext files (after encryption)
rsenv sops status Show encryption state
rsenv sops gitignore-sync Sync .gitignore with encryption patterns
rsenv sops gitignore-status Show gitignore sync status
rsenv sops gitignore-clean Remove rsenv-managed gitignore section
rsenv hook install Install pre-commit hook in vault's git repo
rsenv hook remove Remove pre-commit hook
rsenv hook status Show hook installation status

Hash-Based Staleness Detection

rsenv uses content-addressed encryption to detect when plaintext files change after encryption:

Filename format: {name}.{hash}.enc where hash is SHA-256 of plaintext content.

Example:

config.env                    # Plaintext file
config.env.a1b2c3d4e5f6.enc   # Encrypted (hash embedded in name)

Status categories:

Status Meaning
current Encrypted file exists with matching hash (up-to-date)
stale Encrypted file exists but hash differs (needs re-encryption)
pending_encrypt Plaintext exists, no encrypted version
orphaned Encrypted file exists, no plaintext

This enables the pre-commit hook to detect modified files that weren't re-encrypted.

Setup

1. Install SOPS

# macOS
brew install sops

# Linux
curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
chmod +x sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops

2. Create GPG Key (if needed)

# Generate key
gpg --gen-key

# List keys (note the fingerprint)
gpg --list-keys --keyid-format long

3. Configure rsenv

# Create global config
rsenv config init --global

Edit ~/.config/rsenv/rsenv.toml:

[sops]
gpg_key = "60A4127E82E218297532FAB6D750B66AE08F3B90"  # Your GPG fingerprint

# File extensions to encrypt
file_extensions_enc = ["env", "envrc", "yaml", "yml", "json"]

# Specific filenames to encrypt
file_names_enc = ["dot_pypirc", "dot_pgpass", "kube_config"]

# Extensions to decrypt (encrypted files)
file_extensions_dec = ["enc"]

Content-Addressed Encryption

rsenv uses content-addressed filenames for encrypted files. The hash of the plaintext content is embedded in the filename:

{filename}.{hash8}.enc

Examples:
  secrets.env           → secrets.env.a1b2c3d4.enc
  config.envrc          → config.envrc.e5f6g7h8.enc
  guarded/dot.env       → guarded/dot.env.b9c0d1e2.enc

Why Content-Addressed?

This design prevents accidental data loss:

  1. Staleness detection: If you edit a plaintext file, rsenv detects the hash mismatch and marks it as "stale"
  2. Safe cleanup: rsenv sops clean only deletes plaintext when the hash matches - never deletes unsaved changes
  3. Pre-commit enforcement: The --check flag enables git hooks to block commits with stale/unencrypted files

Hash Specification

  • Algorithm: SHA-256 of plaintext content
  • Length: First 8 hex characters (32 bits)
  • Collision resistance: 4 billion combinations - sufficient for any vault

Commands

Encrypt Files

# Encrypt single file
rsenv sops encrypt secrets.env

# Encrypt files in project's vault (default)
rsenv sops encrypt

# Encrypt all vaults
rsenv sops encrypt --global

# Encrypt files in specific directory
rsenv sops encrypt --dir /path/to/dir

Arguments:

  • FILE: Single file to encrypt (optional)

Options:

  • --global / -g: Encrypt all vaults
  • --dir / -d: Encrypt specific directory
  • --vault-base: Override vaults directory (requires --global)

Without arguments or options, encrypts all matching files in the current project's vault.

What happens:

  1. Finds files matching file_extensions_enc and file_names_enc
  2. Computes SHA-256 hash of each plaintext file
  3. Encrypts each: file.envfile.env.{hash8}.enc
  4. Removes any old file.env.*.enc with different hashes
  5. Original plaintext files remain (use clean to remove)

Decrypt Files

# Decrypt single file
rsenv sops decrypt secrets.env.enc

# Decrypt .enc files in project's vault (default)
rsenv sops decrypt

# Decrypt all vaults
rsenv sops decrypt --global

# Decrypt in specific directory
rsenv sops decrypt --dir /path/to/dir

Arguments:

  • FILE: Single file to decrypt (optional)

What happens:

  1. Finds files matching file_extensions_dec (default: .enc)
  2. Parses hash from filename: file.env.{hash8}.enc
  3. Decrypts each: file.env.{hash8}.encfile.env
  4. Verifies decrypted content matches the hash (integrity check)
  5. Encrypted files remain

Clean Plaintext

# Remove plaintext files in project's vault
rsenv sops clean

# Clean all vaults
rsenv sops clean --global

What happens:

  1. Finds files matching encryption patterns
  2. Computes hash of each plaintext file
  3. Only deletes if a matching {name}.{hash}.enc exists (hash must match!)
  4. Refuses to delete stale files (protects unsaved changes)
  5. Only encrypted files remain after successful clean

Safety guarantee: If you've edited a file since last encryption, clean will NOT delete it. You must run encrypt first to update the encrypted version.

Check Status

# Status for project's vault
rsenv sops status

# Status for all vaults
rsenv sops status --global

# Check mode (for scripts/hooks) - exits 0 if clean, 1 if needs attention
rsenv sops status --check

Options:

  • --check: Exit with code 1 if any files need encryption (for CI/hooks)

File Categories:

Category Meaning Action Needed
Pending encrypt New file, no .enc exists Run encrypt
Stale Plaintext changed since encryption Run encrypt
Current Hash matches, up-to-date None (can clean)
Orphaned .enc exists but no plaintext Can delete

Output:

SOPS Status for /home/user/.rsenv/vaults/myproject-abc123:

Pending encryption (1):
  envs/new-secrets.env

Stale (1):
  envs/local.env (current: e5f6g7h8, encrypted: a1b2c3d4)

Current (2):
  envs/prod.env.b9c0d1e2.enc
  envs/staging.env.f3a4b5c6.enc

Orphaned (1):
  envs/old-config.env.deadbeef.enc

Exit codes for scripting:

  • rsenv sops status --check: Returns exit code 1 if any files need encryption (pending or stale), 0 otherwise

Workflow

Standard Encryption Workflow

# 1. Work on plaintext files
vim $RSENV_VAULT/envs/local.env

# 2. Check status (optional - see what changed)
rsenv sops status

# 3. Encrypt when done
rsenv sops encrypt

# 4. Remove plaintext (optional, for sharing)
rsenv sops clean  # Safe: won't delete if hash mismatch

# 5. Commit encrypted files
cd $RSENV_VAULT
git add *.enc
git commit -m "Update encrypted configs"  # Hook blocks if stale

With pre-commit hook installed: Steps 2-3 can be enforced automatically - the hook blocks commit if you forget to encrypt.

Standard Decryption Workflow

# 1. Decrypt to work on files
rsenv sops decrypt

# 2. Edit plaintext
vim $RSENV_VAULT/envs/local.env

# 3. Re-encrypt
rsenv sops encrypt

# 4. Clean up
rsenv sops clean

.gitignore Management

When you run rsenv sops encrypt on the vault directory, rsenv automatically updates .gitignore:

# ---------------------------------- rsenv-sops-start -----------------------------------
*.env  # sops-managed 2024-01-15 10:30:45
*.envrc  # sops-managed 2024-01-15 10:30:45
*.yaml  # sops-managed 2024-01-15 10:30:45
dot_pgpass  # sops-managed 2024-01-15 10:30:45
# ---------------------------------- rsenv-sops-end -----------------------------------

Patterns come from:

  • file_extensions_enc*.{ext} patterns
  • file_names_enc → exact filename patterns

Benefits:

  • Prevents accidental commit of plaintext secrets
  • Updated automatically during encryption
  • Preserved across encrypt/decrypt cycles

gitignore Commands

Manage the rsenv-managed section explicitly:

# Show status for current vault's gitignore
rsenv sops gitignore-status

# Show status for global gitignore only
rsenv sops gitignore-status --global

# Sync patterns to current vault's .gitignore
rsenv sops gitignore-sync

# Sync patterns to global gitignore only
rsenv sops gitignore-sync --global

# Remove rsenv section from vault's .gitignore
rsenv sops gitignore-clean

# Remove rsenv section from global gitignore only
rsenv sops gitignore-clean --global

Options:

  • --global: Operate on global gitignore only (not per-vault)
  • -y, --yes: Skip confirmation prompt (gitignore-sync only)

Note: Without --global, these commands only affect the current vault's gitignore. To sync both vault and global, run the command twice (with and without --global).

File Format Handling

SOPS auto-detects file formats:

Extension Handling
.json Structured (keys encrypted, structure visible)
.yaml, .yml Structured
.env dotenv format (--input-type dotenv)
.envrc Default SOPS format (shell scripts, not dotenv)
Other Binary (entire file encrypted)

dotenv Files

For .env files only, rsenv tells SOPS to preserve the dotenv format:

# Plaintext
DATABASE_URL=postgres://localhost/mydb
API_KEY=sk-secret-123

# Encrypted (structure preserved)
DATABASE_URL=ENC[AES256_GCM,data:...,tag:...]
API_KEY=ENC[AES256_GCM,data:...,tag:...]

Configuration Reference

Full SOPS Configuration

# ~/.config/rsenv/rsenv.toml

[sops]
# GPG key fingerprint (required for GPG encryption)
gpg_key = "60A4127E82E218297532FAB6D750B66AE08F3B90"

# Age public key (alternative to GPG)
# age_key = "age1..."

# Extensions to encrypt
file_extensions_enc = [
    "env",
    "envrc",
    "yaml",
    "yml",
    "json",
    "p12",       # PKCS#12 certificates
    "keystore",  # Java keystores
]

# Specific filenames to encrypt
file_names_enc = [
    "dot_pypirc",
    "dot_pgpass",
    "kube_config",
]

# Extensions to decrypt (typically just "enc")
file_extensions_dec = ["enc"]

# Specific filenames to decrypt (usually empty)
file_names_dec = []

Environment Variables

Override config via environment:

export RSENV_SOPS_GPG_KEY="fingerprint"
export RSENV_SOPS_FILE_EXTENSIONS_ENC="env,yaml,json"

Parallel Processing

rsenv uses parallel execution (via rayon) for batch operations:

  • Default: 8 parallel threads
  • Speeds up encryption/decryption of many files

Age Backend (Alternative to GPG)

SOPS also supports Age encryption:

# Generate Age key
age-keygen -o key.txt

# Configure
[sops]
age_key = "age1..."

Age is simpler than GPG but less widely supported.

Git Hooks

rsenv can install a pre-commit hook in your vault's git repository to prevent committing with unencrypted or stale files.

Install Pre-commit Hook

# Install hook in vault's .git/hooks/pre-commit
rsenv hook install

# Overwrite existing hook
rsenv hook install --force

Remove Hook

rsenv hook remove

Check Hook Status

rsenv hook status

What the Hook Does

The pre-commit hook runs rsenv sops status --check before each commit:

  • Blocks commit if any files need encryption (pending or stale)
  • Allows commit only when all files are current
  • Shows clear error message with instructions

Example blocked commit:

$ git commit -m "Update configs"
ERROR: Unencrypted or stale files in vault.
Run 'rsenv sops encrypt' to update encryption.

Bypassing the Hook

In emergencies, you can bypass with --no-verify:

git commit --no-verify -m "Emergency fix"

Warning: This defeats the safety mechanism. Use sparingly.

Security Model

Defense in Depth

  1. Vault location: Outside git repository
  2. Symlinks: Git only sees symlinks, not actual secrets
  3. Encryption: Vault contents encrypted at rest
  4. gitignore: Auto-excludes plaintext files
  5. Content-addressed filenames: Hash in filename detects stale encryption
  6. Pre-commit hook: Blocks commits with unencrypted changes

Typical Security Flow

# Work locally (plaintext)
rsenv sops decrypt
vim $RSENV_VAULT/envs/prod.env

# Before backup/share (encrypted)
rsenv sops encrypt
rsenv sops clean

# Vault now contains only *.enc files

Troubleshooting

"gpg: decryption failed: No secret key"

Your GPG key isn't available:

# List available keys
gpg --list-secret-keys

# Import if needed
gpg --import key.asc

"Could not find SOPS configuration"

Ensure config is set:

rsenv config show | grep sops

"No files match patterns"

Check your patterns:

# Show what would be encrypted
rsenv sops status

# Verify config
rsenv config show

Encrypted file won't decrypt

SOPS stores key info in the encrypted file. Verify you have the right key:

# Check what key was used
sops --decrypt --verbose file.enc 2>&1 | grep -i key

Pre-commit Hook

rsenv provides a git pre-commit hook to prevent committing when files need encryption.

Install Hook

# Install at default location (base_dir, typically ~/.rsenv)
rsenv hook install

# Install at specific git repository
rsenv hook install --dir /path/to/repo

# Force overwrite existing hook
rsenv hook install --force

How It Works

The hook runs rsenv sops status --global --check before each commit:

  • Exit 0: All files encrypted and current → commit allowed
  • Exit 1: Files need encryption (pending or stale) → commit blocked

Hook Commands

Command Purpose
rsenv hook install Install pre-commit hook
rsenv hook remove Remove pre-commit hook
rsenv hook status Show hook installation status

Options:

  • --dir <PATH>: Target git repository (default: base_dir)
  • --force / -f: Overwrite existing hook (install only)

Remove Hook

rsenv hook remove

# Remove from specific location
rsenv hook remove --dir /path/to/repo

Related

rsenv Documentation

Getting Started
Features
Reference
Upgrading

Clone this wiki locally

0