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:
- 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.
- 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.
- Neither language can guarantee decrypted secrets are scrubbed from
memory.
unset/rmin Bash and$var = $nullin 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 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 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, andvault_loadpaths — decrypted content only ever lives in shell variables.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 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 pipefailto itself vialocal -, so your interactive shell's own options are untouched before and after anyvault_*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
vault_init— create the vault. Run once; refuses to run ifVAULT_FILEalready 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.
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.
-
vault_unset VAR— remove a secret. -
vault_edit— free-form editing (reordering, several variables at once, comments) via$EDITOR, decrypting through/dev/shmas noted above. -
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. -
vault_load— decrypt to stdout, meant to be paired withsource <(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
- Place
vault-tools.ps1somewhere readable by whoever needs it — same personal-vs-shared-path logic as the Linux side. - 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
- Optionally set
$env:VAULT_FILEbefore dot-sourcing, if you don't want the defaultsecrets.vaultin the current directory.
Security specifics
- Password-based AES, not DPAPI. PowerShell's built-in
ConvertTo/From-SecureStringis the more "native-feeling" option, but without-Keyit 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 usesSystem.Security. Cryptographydirectly (PBKDF2 + AES-256-CBC) instead, matching the Linux side's approach and keeping both use cases available. - PBKDF2 uses HMACSHA1, deliberately. The
Rfc2898DeriveBytesconstructor 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/shmequivalent ships in the box.Edit-Vaultcan'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 thanvim/notepad. - No
source <(...)equivalent needed. PowerShell has no process substitution, and dot-sourcing needs a real file.Import-VaultSecretsets$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
-
New-Vault— create the vault. Run once; refuses to run if the vault file already exists. Prompts twice (enter + verify), same rationale asvault_init. -
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. -
Remove-VaultSecret -Name VAR— remove a secret. -
Edit-Vault— free-form editing, in-console as described above. -
Get-VaultSecretName— list secret names only, never values. -
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-Vaultis 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