A Nix shell hook for automatically decrypting SOPS-encrypted files and exporting their contents as environment variables.
- Secure: Built-in protection against command injection and path traversal
- Fast: Efficient decryption and parsing
- Flexible: Supports multiple file formats (dotenv, JSON)
- Protected: System variables (PATH, HOME) are never overwritten by default
- Tested: Comprehensive test suite with real SOPS encryption
This project provides a Nix function that creates shell hooks for decrypting SOPS-encrypted files in development environments. The implementation includes built-in security hardening with configurable options.
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
sops-decrypt-hook.url = "github:brittonr/sops-decrypt-hook";
};
outputs = { self, nixpkgs, sops-decrypt-hook, ... }:
let
system = "x86_64-linux"; # or "aarch64-darwin" for M1 Macs
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.${system}.default = pkgs.mkShell {
buildInputs = [ pkgs.sops pkgs.jq ]; # jq needed for JSON format
shellHook = (sops-decrypt-hook.lib.mkSopsDecryptHook {
sopsFiles = [ ./secrets.enc.env ];
}).shellHook;
};
};
}The mkSopsDecryptHook function accepts the following options:
sopsFiles: List of SOPS-encrypted files to decrypt
protectedVars: List of environment variables that cannot be overwritten (default: system variables)validatePaths: Validate file paths for security (default:true)validateKeys: Validate variable names (default:true)maxFileSize: Maximum file size in bytes (default: 10MB)
failOnError: Exit if decryption fails (default:false)verbose: Show detailed output (default:false)allowOverwrite: Allow overwriting existing environment variables (default:false)globalPrefix: Add prefix to all exported variables (default:"")
shellHook = (sops-decrypt-hook.lib.mkSopsDecryptHook {
sopsFiles = [ ./secrets.enc.env ./database.enc.env ];
failOnError = true;
verbose = true;
globalPrefix = "APP_";
}).shellHook;The hook supports multiple file formats:
# Create secrets.env
API_KEY=secret123
DATABASE_URL=postgresql://localhost/mydb
# Comments are supported
EMPTY_VALUE=
QUOTED="value with spaces"
# Encrypt with SOPS
sops -e secrets.env > secrets.enc.env{
"api_key": "secret123",
"database": {
"host": "localhost",
"port": 5432,
"credentials": {
"user": "admin",
"password": "secret"
}
}
}# Encrypt with SOPS
sops -e secrets.json > secrets.enc.jsonNested JSON objects are automatically flattened with underscore separators:
api_key→api_keydatabase.host→database_hostdatabase.credentials.user→database_credentials_user
Note: Use the keyTransform option to convert to uppercase if needed.
shellHook = (sops-decrypt-hook.lib.mkSopsDecryptHook {
sopsFiles = []; # Use fileConfigs instead for format specification
fileConfigs = [
{ path = ./secrets.enc.env; format = "dotenv"; }
{ path = ./config.enc.json; format = "json"; prefix = "APP_"; }
];
}).shellHook;Note: JSON format requires jq to be available in your shell environment.
The project includes comprehensive tests implemented as Nix derivations.
# Run all checks
nix flake check
# Run specific test category
nix build .#checks.x86_64-linux.security
nix build .#checks.x86_64-linux.quick
nix build .#checks.x86_64-linux.unitTests
# Run nix-unit tests
nix run .#test-nix-unit
# Run all tests
nix run .#test-all- Quick Tests: Fast smoke tests for basic functionality
- Security Tests: Validates security protections
- Unit Tests: Mock SOPS tests for functionality
- Integration Tests: Real SOPS encryption tests with age keys
- Edge Cases: Special characters, injection attempts
- Performance: Performance benchmarks
# Default development environment
nix develop
# With sops-nix integration
nix develop .#withSopsNix
# Testing environment
nix develop .#testing{
imports = [ sops-decrypt-hook.nixosModules.default ];
services.sopsDecryptHook = {
enable = true;
files = [ /etc/secrets/app.env ];
services = [ "myapp" "database" ];
};
}{
imports = [ sops-decrypt-hook.homeManagerModules.default ];
programs.sopsDecryptHook = {
enable = true;
files = [ ~/.secrets/personal.env ];
shells = [ "bash" "zsh" ];
};
}Symptom: DATABASE_URL=postgresql://user:pass@localhost/db becomes just postgresql://user
Solution: The current implementation properly handles cut -d '=' -f 2- to preserve values with equals signs.
Symptom: Shell becomes unusable after loading secrets
Solution: System variables are protected by default. Use protectedVars option to customize the list.
Symptom: Secrets aren't loaded but no error is shown
Solution: Use verbose: true and failOnError: true options for debugging.
- Validate SOPS files before adding to your devShell
- Use appropriate failure modes:
failOnError: truefor CI/CD pipelinesfailOnError: falsefor development (default)
- Use prefixes to namespace variables:
globalPrefix = "MYAPP_" - Regular testing - Run the test suite after updates
- Keep secrets files small - Default max size is 10MB
The implementation includes built-in protection against:
- Command injection via malicious variable names or values
- Path traversal attacks
- Overwriting critical system variables
- Loading of suspiciously large files
- Invalid variable names that could cause shell issues
Protected variables by default include:
- System paths:
PATH,LD_LIBRARY_PATH - Shell variables:
HOME,USER,SHELL,IFS - Security-sensitive:
LD_PRELOAD,BASH_ENV,PYTHONPATH