Readme
secenv
Secure, profile-based environment variable management with HOCON configuration, PGP decryption, and optional GCP Secret Manager integration for retrieving PGP private keys.
Features
🔐 PGP decryption : Decrypt PGP-encrypted values using a private key provided via file, literal, GPG keyring (by fingerprint), or GCP Secret Manager
☁️ GCP Secret Manager : Fetch the PGP private key at runtime via gcloud
🗂️ Profiles : Organize variables by profile (dev, staging, prod, …)
📄 Temporary files : Create temporary files with plain or encrypted content that are automatically cleaned up after command execution
🧪 Docs & tooling : Built-in manual and shell completion generators
⚡ Fast & safe : Rust-based CLI
Installation
From source
git clone https://github.com/cchexcode/secenv
cd secenv
cargo build -- release
The binary will be at target/ release/ secenv .
Quick start
1) Create secenv. conf (JSON or HOCON)
version = " 0.0.0" # Must be semver and compatible with the CLI version
profiles. default {
# Optional: Define temporary files that will be created before command execution
# and automatically cleaned up afterwards
files {
# Plain file example
# " ./config.json" . plain. literal = ' { " key" : " value" } '
# Secure file with PGP - encrypted content
# " ./credentials.key" . secure {
# secret. pgp. gpg. fingerprint = " 1E1BAC706C352094D490D5393F5167F1F3002043"
# value. base64 = " <base64-encoded ASCII-armored PGP message>"
# }
}
env {
# Optional regex patterns of variables to keep when executing a command.
# If set, the child environment is cleared first, then only matching host vars are kept.
# If omitted, the full host environment is kept.
# keep = [ " ^PATH$" , " ^SHELL$" , " ^LC_.*" ]
vars {
# Plain inline values
APP_NAME . plain. literal = " myapp"
# DB_HOST . plain. base64 = " bG9jYWxob3N0" # " localhost"
# Secure PGP - decrypted value using a GPG key from local keyring by fingerprint
SECRET_TOKEN . secure {
secret. pgp. gpg. fingerprint = " 1E1BAC706C352094D490D5393F5167F1F3002043"
value. base64 = " <base64-encoded ASCII-armored PGP message>"
}
# Secure PGP - decrypted value using a private key stored in GCP Secret Manager
# secret. pgp. gcp. secret must be a fully qualified resource:
# projects/ < project> / secrets/ < name> [ / versions/ < version> ]
# version defaults to " latest" if omitted.
SERVICE_TOKEN . secure {
secret. pgp. gcp. secret = " projects/123456789/secrets/pgp-private-key"
# secret. pgp. gcp. version = " latest" # optional
value. literal = " " "
-----BEGIN PGP MESSAGE-----
...
-----END PGP MESSAGE-----
" " "
}
}
}
}
Notes:
The config file can be in JSON or HOCON format (HOCON is a superset of JSON).
Use secenv init to generate a JSON example file, or write your own in HOCON format.
The version field is validated against the CLI version. The config cannot be newer than the CLI, and major versions must match.
Supported secret sources for PGP private keys: secret. pgp. literal , secret. pgp. file , secret. pgp. gpg. fingerprint , secret. pgp. gcp. secret (+ optional . version).
2) Unlock variables and manage temporary files
# Print key=value pairs for the default profile
secenv unlock
# Use a specific profile and config path
secenv unlock --config /path/to/secenv.conf --profile production
# Load into current shell (bash/zsh/fish)
eval "$(secenv unlock --profile production)"
To run a command with the variables set:
# Run a program inheriting host environment (default behavior)
secenv unlock --profile production -- env | sort
# With keep configured in the profile, only matching host vars are preserved
secenv unlock --profile production -- printenv | sort
# Execute a command (temporary files defined in the profile will be created and cleaned up)
secenv unlock --profile production -- make deploy
# Overwrite existing files if they already exist
secenv unlock --profile production --force -- make deploy
Output format when printing:
APP_NAME = myapp
SECRET_TOKEN = ...
Note : When executing a command, any files defined in profiles. < profile> . files are created before the command runs and automatically deleted after the command completes. Use --force to overwrite existing files.
Temporary files
The temporary files feature allows you to create files with sensitive content that are automatically managed by secenv . This is useful for:
Credential files : Create temporary credential files (e.g., . kubeconfig, . aws/ credentials, service account keys) that are needed by commands but should not persist on disk
Configuration files : Generate temporary config files with decrypted secrets
SSH keys : Provide temporary SSH keys for deployment scripts
Certificate files : Supply temporary SSL/TLS certificates and keys
How it works
Definition : Files are defined in the profiles. < profile> . files section of the config
Content types : Files can contain plain text or PGP-encrypted content (same as environment variables)
Creation : Before running a command (or printing env vars), all files are created with their decrypted content
Directory creation : Parent directories are automatically created if they don't exist
Conflict handling : If a file already exists, the operation fails unless --force is used
Cleanup : After the command completes (or env vars are printed), all created files are automatically deleted
Error handling : If cleanup fails, an error is printed to stderr, but the original command's exit code is preserved
Example use case
profiles. production {
files {
# Temporary kubeconfig for kubectl commands
" ./kubeconfig" . secure {
secret. pgp. gcp. secret = " projects/myproject/secrets/pgp-key"
value. base64 = " <base64-encoded-pgp-message>"
}
# Temporary service account key
" ./service-account.json" . secure {
secret. pgp. gpg. fingerprint = " 1E1BAC706C352094D490D5393F5167F1F3002043"
value. base64 = " <base64-encoded-pgp-message>"
}
}
env. vars {
KUBECONFIG . plain. literal = " ./kubeconfig"
GOOGLE_APPLICATION_CREDENTIALS . plain. literal = " ./service-account.json"
}
}
Then run:
secenv unlock -- profile production -- kubectl get pods
# The kubeconfig and service account files are created, kubectl runs, then files are deleted
Configuration reference (JSON/HOCON)
Top-level
version = " <semver>"
profiles = {
< name> = {
files = { ... } # optional
env = {
keep = [ < regex> ] , # optional
vars = { ... } # required
}
}
}
Profiles and temporary files
profiles. < profile> . files {
# Define temporary files that will be created before command execution
# and automatically cleaned up afterwards
# Plain file content
" /path/to/file" . plain. literal = " file content"
" /path/to/file" . plain. base64 = " <base64-encoded content>"
# Secure file content ( PGP - decrypted)
" /path/to/secure.key" . secure {
# Use any of the same PGP secret sources as environment variables
secret. pgp. file = " /path/to/private.key"
# or secret. pgp. literal, secret. pgp. gpg. fingerprint, secret. pgp. gcp. secret
value. literal = " -----BEGIN PGP MESSAGE-----..."
# or value. base64 = " <base64-encoded-ASCII-armored-message>"
}
}
Profiles and environment
profiles. < profile> . env. keep = [ " ^PATH$" , " ^LC_.*" ] # optional
profiles. < profile> . env. vars { # required
# Plain values ( inline only)
KEY . plain. literal = " value"
KEY . plain. base64 = " <base64-encoded string>"
# Secure values ( PGP - decrypted)
KEY . secure {
# One of the following PGP private key sources:
# Inline ( EncodedValue) : choose one encoding
# secret. pgp. literal. literal = " " "
# -----BEGIN PGP PRIVATE KEY BLOCK-----
# ...
# -----END PGP PRIVATE KEY BLOCK-----
# " " "
# or
# secret. pgp. literal. base64 = " <base64-encoded ASCII-armored private key>"
# OR
# secret. pgp. file = " /path/to/private.key"
# OR
# secret. pgp. gpg. fingerprint = " <fingerprint>"
# OR
# secret. pgp. gcp. secret = " projects/<project>/secrets/<name>"
# secret. pgp. gcp. version = " latest" # optional
# Encrypted value to decrypt ( ASCII - armored PGP message)
value. literal = " -----BEGIN PGP MESSAGE-----..."
# or
# value. base64 = " <base64-encoded-ASCII-armored-message>"
}
}
Providers
plain : Inline string value via literal or base64
secure : Decrypts a PGP message using a provided PGP private key (secret.pgp.* )
Important:
Reading plain values from files or environment variables is not supported in the new manifest. Provide plain values inline via literal /base64 .
Decryption via GPG keyring is supported only by specifying a fingerprint .
CLI reference
Global options:
- e, - - experimental – enable experimental features
Commands:
unlock
Unlock values, create temporary files, and optionally execute a command with the variables set.
secenv unlock [ OPTIONS] [ --] [ COMMAND...]
Options:
-c, -- config < path> Path to config (default: secenv.conf )
-p, -- profile < name> Profile name (default: default )
-f, -- force Overwrite existing files defined in the manifest
Behavior:
Without COMMAND , prints KEY = VALUE lines to stdout. If the profile defines temporary files, they are created and immediately cleaned up.
With COMMAND , executes it with variables set and temporary files created. Files are automatically cleaned up after the command completes.
If env. keep is set in the profile, the child environment is cleared first and only host variables matching any regex in keep are preserved; otherwise, the full host environment is kept.
Temporary files defined in profiles. < profile> . files are created before command execution:
Parent directories are automatically created if they don't exist
If a file already exists, the command fails unless --force is specified
Files support both plain and secure (PGP-encrypted) content
All created files are automatically removed after command execution or when printing environment variables
man
Render the manual pages or markdown help.
secenv man -- out < directory> - -format < manpages | markdown >
autocomplete
Generate shell completion scripts.
secenv autocomplete -- out < directory> - -shell < bash | zsh | fish | elvish | powershell >
init
Initialize a new config file.
secenv init [ -- path <path>] [ -- force]
Notes:
Creates an example file at --path (default: secenv. conf ) in JSON format.
The config file can be in JSON or HOCON format (HOCON is a superset of JSON, so both work).
Review and adapt the generated file to add your version , profiles , and vars as shown in the examples.
GCP requirements
Install and authenticate gcloud (gcloud auth login or service account with suitable permissions).
Ensure the identity has access to the relevant secrets (e.g., Secret Manager Secret Accessor).
Accepted secret identifier format: projects/ < project> / secrets/ < name> (optional /versions/<version> ; defaults to latest ).
Troubleshooting
"Profile '' not found": Verify profiles. < name> exists in the config.
"Failed to parse HOCON config": Validate HOCON syntax and file path.
"File '
GCP access errors: Check gcloud authentication, project, permissions, and secret name.
PGP decryption errors: Ensure the private key is valid ASCII‑armored and corresponds to the message.
File cleanup errors: If temporary file cleanup fails after command execution, an error message will be printed to stderr, but the command exit code will still be preserved.
For verbose logs:
RUST_LOG = debug secenv unlock
Testing
cargo run -- unlock -- env
The password for all private keys for testing is test .
Contributing
Development
git clone https://github.com/cchexcode/secenv
cd secenv
cargo build
cargo test
License
MIT – see LICENSE .