hugobatista/secret-tool-run

GitHub: hugobatista/secret-tool-run

Stars: 1 | Forks: 0

[![GitHub Tag](https://img.shields.io/github/v/tag/hugobatista/secret-tool-run?logo=github&label=latest)](https://go.hugobatista.com/gh/secret-tool-run/releases) # secret-tool-run 🔐 **Execute commands with secrets fetched encrypted from your keyring — never stored on disk.** ## Quick Example ![Demo: file & FD modes](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/063cef1e56101806.gif) Instead of this (storing secrets on disk): # ❌ Dangerous: secrets exposed on filesystem cat .env # DATABASE_PASSWORD=super_secret python app.py Do this (secrets from keyring): # ✅ Secure: secrets loaded from keyring, never persisted to disk secret-tool-run python app.py **Under the hood:** secret-tool-run retrieves your secrets from the system keyring (encrypted and managed by the OS) through secret-tool and passes them to your command — no permanent `.env` files on disk. It has three modes: **file mode** (default) writes a temp `.env` with secure permissions and deletes it after; **file descriptor mode** (`@SECRETS@`) passes secrets via an in-memory FD with zero disk writes; **source mode** (`--source`) exports secrets as real environment variables without writing any file. ## Why You Need This **Three threats this eliminates at once. Security and usability — no trade-off.** **1. File-harvesting malware.** Supply-chain attacks and post-exploitation tools scan disk for `.env` files and exfiltrate them. With `--source` or `@SECRETS@`, the file never exists on disk — nothing to steal. **2. The `.env` in git accident.** One wrong `git add .` and credentials are in your repository history forever. No `.env` file on disk means nothing to stage, commit, or push. **3. `export $(cat .env | xargs)` process leaks.** This common pattern spawns `cat`, `xargs`, and `/bin/echo` subprocesses whose command-line arguments are the actual secret values — visible to any user running `ps aux`. `--source` uses bash builtins only: no subprocesses, no command-line arguments, no process-table leaks. secret-tool-run --source ansible-playbook site.yml # no file = no malware, no git risk secret-tool-run --source ./deploy.sh # no subprocess = no ps leaks secret-tool-run --source npm run dev # all three, every time ## Prerequisites - **Linux** with a keyring service (GNOME Keyring, KWallet, etc.) - **bash** (4.0+) - **secret-tool** from `libsecret-tools` package ## Installation ### One-liner (Recommended) Installs secret-tool-run to `~/.local/bin`: curl -fsSL https://go.hugobatista.com/ghraw/secret-tool-run/main/install.sh | sh ### Or clone and install locally Download the repository and run the installer: git clone https:/go.hugobatista.com/gh/secret-tool-run.git cd secret-tool-run ./install.sh The installer will: 1. Check for dependencies 2. Let you choose between system-wide (`/usr/local/bin`) or user-local (`~/.local/bin`) installation 3. Set up the `secret-tool-run` command 4. Verify the installation ## Usage secret-tool-run [OPTIONS] COMMAND [ARGS...] ### Options | Option | Description | |--------|-------------| | `--file FILE`, `-f FILE` | Secrets file path (default: `.env`) | | `--app APP`, `-a APP` | Keyring app identifier (default: current folder name) | | `--source`, `-s` | Source and export `.env` vars into the environment | | `--password[=PASSWORD]` | Encrypt secrets with a password (AES-256-CBC via openssl). If omitted, resolves from `SECRET_TOOL_PASSWORD` env var or prompts. Stored under a separate keyring key (`app_name-encrypted`). | | `--plaintext` | Disable encryption, store/retrieve secrets as plaintext (default: encryption enabled). | | `--help`, `-h` | Show help message | ### Environment | Variable | Description | |----------|-------------| | `SECRET_TOOL_PASSWORD` | Encryption password used automatically for encrypt/decrypt when set, unless overridden by `--password=PASSWORD`. | ## Modes of Operation secret-tool-run has three modes for passing secrets to your command: | Mode | How to enable | How secrets arrive | Writes to disk? | |------|--------------|-------------------|-----------------| | **File** (default) | No flag | Temp `.env` file, `SECRETS_FILE` points to it | Temp file, auto-deleted | | **File Descriptor** | `@SECRETS@` token in args | In-memory FD as `/dev/fd/9`, `SECRETS_FILE=/dev/fd/9` | Never | | **Source** | `--source` / `-s` flag | Exported as real environment variables via `set -a` | Never | ## Examples ### Example 1: Python development with uv secret-tool-run uv run pywrangler dev **What happens:** ### Example 2: Python project with hatch secret-tool-run hatch run dev Perfect for running development servers where you need environment variables but don't want them persisted on disk. ### Example 3: Ansible playbook with environment variables secret-tool-run --source ansible-playbook site.yml **Before secret-tool-run:** source .env && ansible-playbook site.yml **What happens with `--source`:** Useful for any tool that expects secrets as environment variables — Ansible, Terraform, custom scripts, etc. ### Example 4: GitHub Actions local testing with act secret-tool-run --file .secrets act --secret-file .secrets **What happens:** 1. Uses custom file name `.secrets` instead of `.env` 2. Loads or prompts for secrets under that filename 3. Runs `act` with the secrets file 4. Cleans up `.secrets` after execution This is especially useful for testing GitHub Actions workflows locally while keeping production secrets secure. ### Example 5: Multiple environments with custom app names # Development environment secret-tool-run --app myproject-dev npm start # Production environment secret-tool-run --app myproject-prod npm start Each `--app` name is a separate keyring entry, allowing you to manage different secret sets (dev, staging, prod) for the same project. ### Example 6: Docker commands secret-tool-run docker-compose up Great for docker-compose files that source `.env` for configuration. ### Example 7: Just viewing the secrets file path secret-tool-run env | grep SECRETS_FILE The `SECRETS_FILE` environment variable contains the absolute path to the secrets file created by secret-tool-run. ### Example 8: File descriptor mode (no disk I/O) secret-tool-run act --secret-file @SECRETS@ **What happens:** 1. Detects `@SECRETS@` token in arguments 2. Loads secrets from keyring into memory 3. Creates file descriptor at `/dev/fd/9` (no disk write) 4. Replaces `@SECRETS@` with `/dev/fd/9` 5. Runs `act` which reads secrets from the file descriptor 6. FD automatically closes - no cleanup needed **Perfect for:** - GitHub Actions local testing with `act` - Docker with `--env-file` - Any tool that can read from file descriptors **Won't work for:** - Shell sourcing (`source $SECRETS_FILE`) - Tools that verify file exists with stat checks - Tools that need to read the file multiple times ### Example 9: Docker with file descriptor mode secret-tool-run docker run --env-file @SECRETS@ myimage Secrets are loaded from keyring and passed to Docker without ever touching the disk. The `@SECRETS@` token automatically enables zero-disk-I/O mode. ## Advanced Features ### Preventing Auto-Cleanup (File Mode Only) In **file mode** (the default), secret-tool-run deletes the temporary `.env` file after your command finishes. Create a `.keep` file to prevent this: touch .env.keep secret-tool-run your-command # .env will remain after execution This is useful for: - Debugging secrets content - Running multiple commands without reloading - IDE integration where the editor expects a persistent file ### Custom Secrets File Locations # Use a different file name secret-tool-run --file .env.production npm run build # Use a path in a different directory secret-tool-run --file /tmp/my-secrets ./deploy.sh ### SECRETS_FILE Environment Variable In **file mode** and **FD mode**, your command receives `SECRETS_FILE` pointing to the secrets source: # File mode: points to temp .env secret-tool-run bash -c 'echo "Secrets are at: $SECRETS_FILE"' # FD mode: points to /dev/fd/9 secret-tool-run bash -c 'echo "Secrets are at: $SECRETS_FILE"' --secret-file @SECRETS@ In **source mode** (`--source`), `SECRETS_FILE` is not set — the secrets are already in the environment. ### File Descriptor Mode (No Disk I/O) For maximum security, use the `@SECRETS@` token in your command to pass secrets via file descriptor without writing to disk: secret-tool-run act --secret-file @SECRETS@ **How it works:** - secret-tool-run detects the `@SECRETS@` token in your command arguments - Loads secrets from keyring into memory only - Creates file descriptor at `/dev/fd/9` (no disk write) - Replaces `@SECRETS@` token with `/dev/fd/9` in all arguments - Your command reads from the FD as if it were a file - No temp file created, no cleanup needed - FD automatically closes when command completes **Security benefits:** - Zero disk I/O - secrets never touch the filesystem - No directory entry visible in `ls` - Automatic cleanup (pipe closes on exit) - No permission race conditions - No accidental `.keep` file keeping secrets around - Simple, explicit syntax - just use `@SECRETS@` where you need it **Compatibility:** ✅ **Works with these tools:** secret-tool-run act --secret-file @SECRETS@ secret-tool-run docker run --env-file @SECRETS@ image Replaced tokens work just like file paths: secret-tool-run mycommand --config @SECRETS@ --output results.txt # All @SECRETS@ tokens are replaced with /dev/fd/9 ### Source Mode (Environment Variable Export) For tools that expect secrets as actual environment variables (like Ansible, shell scripts, or tools that call `os.getenv`), use the `--source` flag: secret-tool-run --source ansible-playbook site.yml **How it works:** - Before running your command, secret-tool-run sources the secrets using `set -a` (allexport) - This exports every `KEY=VALUE` pair as a real environment variable - Your command sees them exactly as if you had run `source .env` manually - **No temp file is written** — secrets are loaded directly from keyring into memory - Works with `@SECRETS@` too — sources from keyring directly into env without touching disk - When a local `.env` file already exists (not loaded from keyring), it is sourced directly from disk **Which tools benefit from `--source`?** | Tool | Without --source | With --source | |------|-----------------|---------------| | Ansible | `source .env && ansible-playbook ...` | `secret-tool-run --source ansible-playbook ...` | | Terraform | `source .env && terraform plan` | `secret-tool-run --source terraform plan` | | Shell scripts | `source .env && ./deploy.sh` | `secret-tool-run --source ./deploy.sh` | | Any `os.getenv`/`$VAR` consumer | needs vars in environment | vars are exported automatically | **Key difference:** without `--source`, secrets are written to a temp file and `SECRETS_FILE` env var is set. With `--source`, secrets are loaded directly into memory — no temp file, no `SECRETS_FILE`, just real env vars. **Combined with `@SECRETS@`:** secret-tool-run --source ansible-playbook --vault-password-file @SECRETS@ site.yml This both sources secrets into the environment AND passes one via file descriptor — maximum flexibility with zero disk writes. ### Encrypted Mode (Default, Password-Protected Secrets) Encryption is **enabled by default**. All secrets are encrypted with AES-256-CBC via openssl before being stored in the keyring: # Default: prompts for password (with confirmation) on first use secret-tool-run npm start # Password from environment variable SECRET_TOOL_PASSWORD=hunter2 secret-tool-run npm start # Explicit password (visible in ps — use with care) secret-tool-run --password=hunter2 npm start # Opt out of encryption secret-tool-run --plaintext npm start **How it works:** 1. Encrypted entries are stored under a separate keyring key: `app_name-encrypted` (distinct from the plaintext key `app_name`). 2. On lookup, the tool tries the encrypted key first. If found, it resolves the password and decrypts. 3. On first run (no existing entry), you'll be prompted for a password (with confirmation) unless `SECRET_TOOL_PASSWORD` or `--password=VALUE` is set. 4. Existing plaintext entries remain readable with a warning: `ℹ Found plaintext entry — not encrypted`. New entries will be encrypted. 5. Use `--plaintext` to disable encryption entirely (e.g., for CI/CD scripts that can't provide a password). **Password resolution priority** (encrypt and decrypt): | Priority | Source | |----------|--------| | 1 | `--password=VALUE` (explicit) | | 2 | `SECRET_TOOL_PASSWORD` env var | | 3 | Interactive prompt (with confirmation when storing) | **Password confirmation:** When prompted interactively to create a new encrypted entry, the password is asked twice to prevent typos. The decrypt path (loading existing entries) prompts once without confirmation. **Auto-detection:** If an encrypted entry exists and no password flags are passed, the tool resolves via env var or prompt automatically. **Security:** Encrypted secrets resist D-Bus `GetSecret` attacks — an attacker who enumerates the keyring gets ciphertext, not plaintext. The decryption key is never stored in the keyring. **Dependency:** Requires `openssl` (installed by default on most Linux distributions). **Migrating plaintext entries to encrypted:** Existing entries stored under the plaintext key (`app_name`) remain readable (with a warning). To upgrade them to encrypted: # ⚠️ Backup your secrets first! This permanently removes the keyring entry. secret-tool lookup app "myapp" > /tmp/myapp-backup.env secret-tool clear app "myapp" # remove old plaintext entry secret-tool-run npm start # re-store as encrypted (will prompt for password) rm /tmp/myapp-backup.env # clean up backup **CI/CD note:** If you run `secret-tool-run` in automation without a password, you must add `--plaintext` or set `SECRET_TOOL_PASSWORD`: # Before (worked with plaintext default): secret-tool-run deploy.sh # After (encryption is default — choose one): secret-tool-run --plaintext deploy.sh # OR SECRET_TOOL_PASSWORD=$(cat /etc/secret.txt) secret-tool-run deploy.sh ### First-Run Setup On first use (when secrets aren't in keyring): 1. secret-tool-run prompts: "Paste your secrets content..." 2. Paste your `.env` content (KEY=VALUE format) 3. Press `Ctrl-D` to finish (or `Ctrl-C` to cancel) 4. Secrets are encrypted and stored in system keyring 5. Future runs load automatically ## Security Notes - **Keyring encryption**: Secrets stored in your system's encrypted keyring service - **File permissions** (file mode): Temporary files created with `600` permissions (owner read/write only) - **Short-lived exposure** (file mode): Files on disk exist only during command execution - **Zero disk I/O**: Use `@SECRETS@` (FD mode) or `--source` (source mode) — secrets never touch disk - **No git commits**: No `.env` files left behind to commit accidentally - **Session isolation**: Each terminal session can use different secrets with `--app` flag - **Encrypted payloads** (AES-256-CBC): Secret content is encrypted with AES-256-CBC before keyring storage by default, protecting against D-Bus `GetSecret` enumeration attacks. See the section below for details. Use `--plaintext` to disable. ### Why Encrypted Mode Matters: The Keyring Enumeration Attack The Linux Secret Service API (D-Bus) that powers GNOME Keyring, KWallet, and similar services is accessible to **any process running under your user account** — no authentication required. This means any code on your machine (malware, a compromised `pip install`, a malicious Node.js package, or even a curious colleague) can enumerate every item in your keyring: import secretstorage bus = secretstorage.dbus_init() col = secretstorage.get_default_collection(bus) for item in col.get_all_items(): print(f' Label: {item.get_label()}') print(f' Attributes: {item.get_attributes()}') print(f' Secret: {item.get_secret().decode(errors="replace")}') print() This is **not a vulnerability** in the keyring — it is by design. The keyring service provides session-level isolation (secrets are encrypted at rest when the session is locked), but once you unlock your keyring at login, any process sharing your D-Bus session can retrieve every secret in plaintext. **This is why secret-tool-run encrypts by default.** When using encrypted mode (AES-256-CBC, enabled by default): - An enumerating attacker sees only **ciphertext** — meaningless without the decryption password - The decryption password is **never stored in the keyring** (resolved via interactive prompt, environment variable, or `--password` flag) - Even if an attacker dumps every keyring entry, your secrets remain confidential **When `--plaintext` is used**, secrets in the keyring are as exposed as `.env` files on disk — any process with D-Bus access can read them. Reserve `--plaintext` for ephemeral or isolated environments (e.g., CI containers where D-Bus access is restricted). **⚠️ Important**: While secret-tool-run improves security, temporary files are still written to disk briefly in file mode. For maximum security: - **Use `@SECRETS@`** for tools that accept a file path (FD mode — zero disk I/O) - **Use `--source`** for tools that need env vars (source mode — zero disk I/O) - Use encrypted home directories - Ensure your keyring is properly locked when not in use - Be cautious running secret-tool-run on shared systems ## Troubleshooting ### "Command fails with @SECRETS@" The command may require a regular file instead of a file descriptor. Try without the `@SECRETS@` token: # If this fails: secret-tool-run mycommand --file @SECRETS@ # Try this instead: secret-tool-run mycommand ### "No secrets found" on every run Check if secrets are actually stored: secret-tool search app "$(basename $PWD)" If nothing appears, the keyring store failed. Try storing manually: secret-tool store --label "Test" app "my-test" # Paste your secret, press Ctrl-D secret-tool lookup app "my-test" ### Secrets file not deleted after run Check for a `.keep` file: ls -la .env.keep Remove it to restore auto-cleanup: rm .env.keep ### Want to delete stored secrets # List secrets for current folder secret-tool search app "$(basename $PWD)" # Delete specific entry secret-tool clear app "$(basename $PWD)" # Or for a specific app name secret-tool clear app "myproject-prod" ### Command fails but secrets file remains If your command crashes before secret-tool-run's cleanup trap runs, manually remove: rm .env # or your custom secrets file name ## Uninstallation Run the uninstall script: ./uninstall.sh This will: 1. Remove the `secret-tool-run` binary 2. Optionally help you clear keyring entries To manually clear all secret-tool-run secrets from keyring: # List all entries secret-tool search app "" # Remove specific ones secret-tool clear app "your-app-name"