Secure, profile-based environment variable management with HOCON configuration, PGP decryption, and optional GCP Secret Manager integration for retrieving PGP private keys.
- 🔐 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
git clone https://github.com/cchexcode/secenv
cd secenv
cargo build --release
The binary will be at target/release/secenv
.
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
).
# 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.
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
- 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
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
version = "<semver>"
profiles = {
<name> = {
files = { ... } # optional
env = {
keep = [<regex>], # optional
vars = { ... } # required
}
}
}
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.<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>"
}
}
- plain: Inline string value via
literal
orbase64
- 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
.
Global options:
-e, --experimental
– enable experimental features
Commands:
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
, printsKEY=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 inkeep
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
Render the manual pages or markdown help.
secenv man --out <directory> --format <manpages|markdown>
Generate shell completion scripts.
secenv autocomplete --out <directory> --shell <bash|zsh|fish|elvish|powershell>
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
, andvars
as shown in the examples.
- 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 tolatest
).
- "Profile '' not found": Verify
profiles.<name>
exists in the config. - "Failed to parse HOCON config": Validate HOCON syntax and file path.
- "File '' already exists": A temporary file defined in the profile conflicts with an existing file. Use
--force
to overwrite or remove the existing 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
cargo run -- unlock -- env
The password for all private keys for testing is test
.
git clone https://github.com/cchexcode/secenv
cd secenv
cargo build
cargo test
MIT – see LICENSE.