vault-tools — Encrypted Credential Storage
Overview
vault-tools.sh is a small, dependency-free (openssl only) Bash toolkit
for storing shared or personal credentials at rest, encrypted, on Linux
systems — no ansible-vault, no enterprise password vault, no external
service required.
It exists to bridge a specific gap: some systems don't have
ansible-vault available, and there's no enterprise secrets vault
(Bitwarden, HashiCorp Vault, etc.) in place yet. Until one is, this fills
that role well enough for day-to-day use, under a few hard constraints
that shaped its design:
- No unencrypted credential may ever be stored in a file — ciphertext at rest is fine; plaintext at rest, even briefly, is not.
- No hardware tokens available — no YubiKeys, and PKI certs can't be bridged from a workstation to these hosts, so nothing here depends on smart cards or hardware-backed keys.
- Must work without
ansible-vault— some hosts in scope don't have Ansible Automation Platform installed.
The result: credentials live in a single openssl-encrypted file
(secrets.sh.enc by default) and get decrypted straight into shell
variables — never into a plaintext file on disk — either at login or on
demand.
This is explicitly an interim solution. It's a reasonable stopgap while the case for a proper enterprise vault gets made to management, not a long-term replacement for one.
Requirements
openssl— a common OS tool, present on most every install with no extra packages needed.- Bash 4+ — Most Linux ship this by default.
- An
$EDITOR(defaults tovi) — only needed if you usevault_edit.
Installation
- Place
vault-tools.shsomewhere readable by whoever needs it — a personal home directory for a personal vault, or a shared path (with appropriate group permissions) for a team vault. - Source it wherever you want the
vault_*functions available — typically~/.bashrcfor every interactive shell, or~/.bash_profileif you only want it (and the automatic vault load, see below) once per login. - Optionally set
VAULT_FILEbefore sourcing, if you don't want the defaultsecrets.sh.encin the current directory:
bash
export VAULT_FILE=/opt/secrets/team.sh.enc
source /opt/secrets/vault-tools.sh
Security model, plainly stated
- Confidentiality, not authenticity. Encryption is AES-256-CBC with
PBKDF2 key derivation. This protects the contents of the vault but
doesn't detect tampering the way an HMAC-based scheme (like
ansible-vault's format) would. If that matters more to you than simplicity, that's a real trade-off to weigh, not an oversight. - Plaintext never touches persistent disk in the
vault_set,vault_unset,vault_list, andvault_loadpaths — decrypted content only ever lives in shell variables, held in memory for the life of the function call.vault_editis the one exception: it decrypts to a file under/dev/shm(tmpfs, RAM-backed) so an interactive editor has something real to save to, then re-encrypts and removes it. /dev/shmisn't disk — as long as the system isn't swapping. If swap is enabled and unencrypted, and the system is under enough memory pressure, that RAM-backed file could theoretically be paged out. Worth knowing ifvault_editis in regular use on memory-constrained hosts.- Bash can't scrub memory.
unsetandrmfree the variable or the inode; neither guarantees the underlying bytes get zeroed. This is a ceiling on what any bash-based approach can guarantee, not a bug here. - Sourcing this file won't change your shell's settings. Every
function scopes
set -euo pipefailto itself vialocal -, so your interactive shell's own options (or lack of them) are untouched before and after anyvault_*call.
Lifecycle
The functions map to a natural sequence: create a vault once, then read and write it as needed.
1. vault_init — create the vault
Run once, per vault. Refuses to run if VAULT_FILE already exists, so it
can't accidentally overwrite a populated vault.
$ vault_init
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:
Created secrets.sh.enc. Use vault_set to add your first secret.
openssl prompts twice on purpose here — a typo on first-time encryption
would otherwise lock you out of a vault with nothing in it yet worth
losing, so it's caught immediately instead.
2. vault_set — add or update a secret
$ vault_set DB_PASSWORD 'correct-horse-battery-staple'
Updated DB_PASSWORD in secrets.sh.enc.
Adds a new export VAR=value line, or updates it in place if VAR
already exists. Values are shell-escaped automatically, so quotes,
spaces, and $ in a password round-trip correctly. Decrypts, edits, and
re-encrypts to a temp file that's atomically moved into place — the
original vault is never touched unless the whole operation succeeds
(wrong password included: nothing gets written).
3. vault_unset — remove a secret
$ vault_unset DB_PASSWORD
Removed DB_PASSWORD from secrets.sh.enc.
4. vault_edit — free-form editing
For anything vault_set/vault_unset don't cover cleanly — reordering,
touching several variables at once, adding comments:
$ vault_edit
# opens $EDITOR on the decrypted contents (via /dev/shm); re-encrypts on save/exit
5. vault_list — see what's in the vault
$ vault_list
DB_PASSWORD
API_KEY
Prints variable names only — never values — so you can check the vault's contents without a secret value ending up in your terminal scrollback.
6. vault_load — use the secrets
The step that actually gets the credentials into your shell:
$ source <(vault_load)
enter AES-256-CBC decryption password:
$ echo "$DB_PASSWORD"
correct-horse-battery-staple
Typically wired into ~/.bash_profile so it runs automatically at login:
# ~/.bash_profile
source ~/vault-tools.sh
source <(vault_load)
One caveat worth knowing: source <(vault_load) doesn't always fail
loudly if decryption fails — a missing vault produces no output at all,
so source trivially succeeds on empty input and nothing gets loaded; a
wrong password usually (not always) surfaces as a visible syntax error,
since openssl can emit partial garbled output before it detects the
bad padding. vault_load's own error message always prints to stderr
either way, so you'll see something at an interactive login — but for
a guaranteed clean failure instead of relying on that, use:
if vault_secrets="$(vault_load)"; then
source <(printf '%s' "$vault_secrets")
else
echo "vault load failed, nothing was set" >&2
fi
unset vault_secrets
Function reference
| Function | Purpose |
|---|---|
vault_init |
Create a new, empty vault (refuses if one already exists) |
vault_set VAR VALUE |
Add or update one secret |
vault_unset VAR |
Remove one secret |
vault_edit |
Open the decrypted vault in $EDITOR for free-form changes |
vault_list |
List secret names (not values) |
vault_load |
Decrypt to stdout — pair with source <(vault_load) |
Use case 1: personal vault
Scenario: an individual engineer wants credentials for their own lab or test systems available at login, without retyping them each session or leaving them sitting in a plaintext dotfile.
# one-time setup
export VAULT_FILE=~/.secrets/personal.sh.enc
mkdir -p ~/.secrets
source ~/vault-tools.sh
vault_init
vault_set LAB_ROOT_PW 'whatever-it-is'
vault_set TEST_DB_URI 'postgres://user:pw@testhost/db'
# wire into ~/.bash_profile
cat >> ~/.bash_profile <<'EOF'
export VAULT_FILE=~/.secrets/personal.sh.enc
source ~/vault-tools.sh
source <(vault_load)
EOF
From here, every login prompts once for the vault password and the credentials are just environment variables for the rest of the session. Since it's single-user, there's no sharing problem to solve — the password is whatever the individual chooses, and nobody else needs it.
Use case 2: shared team vault
Scenario: a team shares a small set of service-account or system credentials across several engineers on a jump host or shared automation account.
# one person sets it up, on a path the whole team can read
sudo mkdir -p /opt/team-secrets
sudo chown :devsecops-team /opt/team-secrets
sudo chmod 750 /opt/team-secrets
export VAULT_FILE=/opt/team-secrets/team.sh.enc
source /opt/team-secrets/vault-tools.sh
vault_init
vault_set SVC_ACCOUNT_PW 'whatever-it-is'
vault_set GITLAB_RUNNER_TOKEN 'glrt-...'
# each team member adds to their own ~/.bash_profile
cat >> ~/.bash_profile <<'EOF'
export VAULT_FILE=/opt/team-secrets/team.sh.enc
source /opt/team-secrets/vault-tools.sh
source <(vault_load)
EOF
Whoever needs to add or change a secret uses vault_set/vault_unset/
vault_edit against the same shared path; everyone else picks up the
change the next time they log in and vault_load runs.
A team vault has one property worth calling out explicitly: this is symmetric encryption — one shared password protects everything in the vault, for everyone. There's no per-person key to revoke when someone leaves the team, the way there would be with GPG or a PKI-backed approach. When someone leaves:
- Rotate the vault's own password (re-encrypt with
vault_editor by recreating it), and - Rotate every credential actually stored inside it.
Both matter — changing just the vault password doesn't erase what a departing member already saw in plaintext while they had access.
Known limitations (summary)
- Confidentiality only — no built-in tamper/authentication check.
/dev/shm-based editing depends on swap being off or encrypted to truly never touch physical disk.- Bash can't guarantee decrypted values are scrubbed from memory.
source <(vault_load)can fail silently in one specific case (missing vault); use the capture-then-source form above if that's a concern.- This is an interim tool. It exists to close the gap responsibly until an enterprise vault is in place — not to replace the case for one.