vault-tools — Encrypted Credential Storage (RHEL 8 & Windows Server 2016)

Overview

vault-tools is a small, dependency-free way to store shared or personal credentials at rest, encrypted, without an enterprise password vault (Bitwarden, HashiCorp Vault, etc.) in place yet. There are two independent implementations — vault-tools.sh for RHEL 8 (Bash + openssl) and vault-tools.ps1 for Windows Server 2016 (PowerShell + .NET) — built to the same design philosophy but each using only what ships natively on its platform. Neither depends on the other.

They exist to bridge a specific gap, under a few hard constraints that shaped the design of both:

  • 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 CAC/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 RHEL hosts in scope don't have Ansible Automation Platform installed, and it isn't a Windows tool to begin with.

Both implementations: encrypt with a password (not a Windows-identity- or hardware-bound key, so the same vault works for a single person or a whole team), and decrypt straight into the current session's environment variables rather than into a plaintext file on disk.

This is explicitly an interim solution on both platforms. It's a reasonable stopgap while the case for a proper enterprise vault gets made to management, not a long-term replacement for one.

Shared security philosophy

Both implementations follow the same three rules, even though the underlying crypto libraries and languages are entirely different:

  1. Confidentiality is password-based, not tied to a single user's identity or a piece of hardware — this is what lets the same design serve both a personal vault and a shared team vault.
  2. Plaintext touches persistent disk as little as possible — ideally never; where a platform can't fully avoid it (see each platform's notes on free-form editing below), that's called out explicitly rather than glossed over.
  3. Neither language can guarantee decrypted secrets are scrubbed from memory. unset/rm in Bash and $var = $null in PowerShell both free the reference, not necessarily the underlying bytes. That's a ceiling on what either implementation can promise, not a bug in either one.

Linux / RHEL 8 (vault-tools.sh)

Requirements

  • openssl — a base RHEL 8 dependency, present on every install with no extra packages needed.
  • Bash 4+ — RHEL 8 ships 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 specifics

  • Confidentiality, not authenticity. Encryption is AES-256-CBC with PBKDF2 key derivation. This protects the vault's contents but doesn't detect tampering the way an HMAC-based scheme (like ansible-vault's format) would.
  • 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. 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 memory pressure, that RAM-backed file could theoretically be paged out.
  • 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 are untouched before and after any vault_* call.
  • FIPS: openssl's crypto runs through RHEL's FIPS-validated module when the system is in FIPS mode. Confirm that status with your ISSO if it matters for your environment.

Lifecycle

  1. vault_init — create the vault. Run once; refuses to run if VAULT_FILE already exists.

bash $ 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 — a typo on first-time encryption would otherwise lock you out of a vault with nothing in it yet worth losing.

  1. vault_set VAR VALUE — add or update a secret.

bash $ vault_set DB_PASSWORD 'correct-horse-battery-staple' Updated DB_PASSWORD in secrets.sh.enc.

Values are shell-escaped automatically, so quotes, spaces, and $ round-trip correctly. Writes to a temp file and atomically moves it into place — a wrong password or any failure leaves the original vault untouched.

  1. vault_unset VAR — remove a secret.

  2. vault_edit — free-form editing (reordering, several variables at once, comments) via $EDITOR, decrypting through /dev/shm as noted above.

  3. vault_list — print secret names only, never values, so you can check the vault's contents without a value ending up in your terminal scrollback.

  4. vault_load — decrypt to stdout, meant to be paired with source <(vault_load):

bash $ source <(vault_load) enter AES-256-CBC decryption password: $ echo "$DB_PASSWORD" correct-horse-battery-staple

Typically wired into ~/.bash_profile:

bash # ~/.bash_profile source ~/vault-tools.sh source <(vault_load)

Caveat: source <(vault_load) doesn't always fail loudly if decryption fails — a missing vault produces no output, so source trivially succeeds on empty input. For a guaranteed clean failure:

bash 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)

Windows Server 2016 (vault-tools.ps1)

Requirements

  • PowerShell 5.1 (ships with Server 2016) or later.
  • .NET Framework 4.6.2+ (Server 2016's default). No modules, no third-party tools.

Installation

  1. Place vault-tools.ps1 somewhere readable by whoever needs it — same personal-vs-shared-path logic as the Linux side.
  2. Dot-source it wherever you want the functions available — typically your PowerShell profile ($PROFILE), the Windows analog of .bash_profile:

powershell # $PROFILE . C:\Tools\vault-tools.ps1 Import-VaultSecret

  1. Optionally set $env:VAULT_FILE before dot-sourcing, if you don't want the default secrets.vault in the current directory.

Security specifics

  • Password-based AES, not DPAPI. PowerShell's built-in ConvertTo/From-SecureString is the more "native-feeling" option, but without -Key it encrypts via DPAPI — tied to the current Windows user and machine, which breaks the shared-team-vault use case entirely (nobody else can decrypt it). This toolkit uses System.Security. Cryptography directly (PBKDF2 + AES-256-CBC) instead, matching the Linux side's approach and keeping both use cases available.
  • PBKDF2 uses HMACSHA1, deliberately. The Rfc2898DeriveBytes constructor that lets you specify SHA256 explicitly requires .NET Framework 4.7.2+; Server 2016 ships 4.6.2 by default. PBKDF2-HMACSHA1 is still an NIST-accepted KDF choice — not the same risk as using SHA1 for a signature. If your systems are confirmed on .NET Framework 4.7.2+, this is worth revisiting.
  • No native RAM-backed filesystem on Windows Server 2016 — no /dev/shm equivalent ships in the box. Edit-Vault can't shell out to an external editor without writing plaintext to a real disk-backed temp file, so instead it decrypts, prints the current contents, and collects replacement lines directly in the console (blank line to finish) — no file touched at all, just a less familiar editing experience than vim/notepad.
  • No source <(...) equivalent needed. PowerShell has no process substitution, and dot-sourcing needs a real file. Import-VaultSecret sets $env: variables directly instead — same effect (process-wide, gone when the session ends), without needing a bash-style workaround.
  • Testing status: validated against PowerShell 7 (Core, cross-platform), using only APIs stable since .NET Framework 2.0 to maximize compatibility — not yet confirmed against actual PowerShell 5.1 on a real Server 2016 box. Treat as pending verification until that happens.

Lifecycle

  1. New-Vault — create the vault. Run once; refuses to run if the vault file already exists. Prompts twice (enter + verify), same rationale as vault_init.

  2. Set-VaultSecret -Name VAR -Value VALUE — add or update a secret. Writes to a temp file and moves it into place; a wrong password or any failure leaves the original vault untouched.

  3. Remove-VaultSecret -Name VAR — remove a secret.

  4. Edit-Vault — free-form editing, in-console as described above.

  5. Get-VaultSecretName — list secret names only, never values.

  6. Import-VaultSecret — decrypt and load every secret into $env: variables for the current session:

powershell PS> Import-VaultSecret Vault password: ******** Loaded 2 secret(s) from secrets.vault. PS> $env:DB_PASSWORD correct-horse-battery-staple

Every function also accepts an optional -Password (a SecureString), which skips the interactive prompt if supplied — useful for scripted or automated scenarios, same role -pass plays for testing on the Linux side.

Function reference

Function Purpose
New-Vault Create a new, empty vault (refuses if one already exists)
Set-VaultSecret -Name -Value Add or update one secret
Remove-VaultSecret -Name Remove one secret
Edit-Vault In-console free-form editing (no external editor)
Get-VaultSecretName List secret names (not values)
Import-VaultSecret Decrypt and set $env: variables for the session

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 in a plaintext dotfile.

Linux:

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'

cat >> ~/.bash_profile <<'EOF'
export VAULT_FILE=~/.secrets/personal.sh.enc
source ~/vault-tools.sh
source <(vault_load)
EOF

Windows:

$env:VAULT_FILE = "$HOME\.secrets\personal.vault"
New-Item -ItemType Directory -Force "$HOME\.secrets" | Out-Null
. C:\Tools\vault-tools.ps1
New-Vault
Set-VaultSecret -Name LAB_ROOT_PW -Value 'whatever-it-is'
Set-VaultSecret -Name TEST_DB_URI -Value 'postgres://user:pw@testhost/db'

Add-Content $PROFILE @'
$env:VAULT_FILE = "$HOME\.secrets\personal.vault"
. C:\Tools\vault-tools.ps1
Import-VaultSecret
'@

Either way, 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.

Use case 2: shared team vault

Scenario: a team shares a small set of service-account or system credentials across several engineers.

Linux:

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

Windows:

New-Item -ItemType Directory -Force D:\TeamSecrets | Out-Null
# restrict the folder to the team group via icacls/Set-Acl, same intent
# as the chmod/chown above — verify the exact ACL syntax on your box.

$env:VAULT_FILE = "D:\TeamSecrets\team.vault"
. D:\TeamSecrets\vault-tools.ps1
New-Vault
Set-VaultSecret -Name SVC_ACCOUNT_PW -Value 'whatever-it-is'
Set-VaultSecret -Name GITLAB_RUNNER_TOKEN -Value 'glrt-...'

# each team member adds to their own $PROFILE
Add-Content $PROFILE @'
$env:VAULT_FILE = "D:\TeamSecrets\team.vault"
. D:\TeamSecrets\vault-tools.ps1
Import-VaultSecret
'@

Whoever needs to add or change a secret uses Set-VaultSecret/ Remove-VaultSecret/Edit-Vault against the same shared path; everyone else picks it up at their next login.

A team vault has one property worth calling out explicitly, on either platform: this is symmetric encryption — one shared password protects everything, 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 (vault_edit / Edit-Vault, or recreate 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, on both platforms — no built-in tamper/ authentication check.
  • Neither Bash nor PowerShell can guarantee decrypted values are scrubbed from memory.
  • Linux: /dev/shm-based editing depends on swap being off or encrypted; source <(vault_load) can fail silently on a missing vault (use the capture-then-source form).
  • Windows: PBKDF2 uses HMACSHA1 pending confirmation of .NET Framework 4.7.2+; Edit-Vault is console-based, not an external editor, for lack of a native RAM disk; not yet verified against real PowerShell 5.1 / Server 2016 — pending testing.
  • Both are interim tools. They exist to close the gap responsibly until an enterprise vault is in place — not to replace the case for one.

Code

Linux / BASH:

This is the code - I need to convince MKDocs to include it dynamically.

Linux tool: vault-tools.sh

Windows tool: vault-tools.ps1