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 to vi) — only needed if you use vault_edit.

Installation

  1. Place vault-tools.sh somewhere 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.
  2. Source it wherever you want the vault_* functions available — typically ~/.bashrc for every interactive shell, or ~/.bash_profile if you only want it (and the automatic vault load, see below) once per login.
  3. Optionally set VAULT_FILE before sourcing, if you don't want the default secrets.sh.enc in 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, and vault_load paths — decrypted content only ever lives in shell variables, held in memory for the life of the function call. vault_edit is 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/shm isn'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 if vault_edit is in regular use on memory-constrained hosts.
  • Bash can't scrub memory. unset and rm free 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 pipefail to itself via local -, so your interactive shell's own options (or lack of them) are untouched before and after any vault_* 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_edit or 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.