secure injection without plaintext environment variables
A database password in a .env file or a pipeline variable is a secret that eventually ends up in a log, a backup, or a history file. Vault secrets that Bash scripts fetch, verify, and never persist anywhere close exactly that gap, including dynamic database credentials and automatic rotation.
Table of Contents
- 1. Why secrets do not belong in environment variables or files
- 2. Vault authentication in scripts: token, AppRole, Kubernetes
- 3. A wrapper for vault kv get with error handling
- 4. Injecting secrets at runtime instead of persisting them
- 5. Dynamic secrets and lease management
- 6. Vault Agent and templates as an alternative to manual retrieval
- 7. Accounting for rotation and expiration in the script
- 8. Error handling, audit logging and common pitfalls
- 9. Environment variable, file and Vault retrieval compared
- 10. Summary
- 11. FAQ
1. Why secrets do not belong in environment variables or files
The classic approach of storing database passwords or API keys in a .env file or as a CI variable has a structural downside: once a secret is written to disk or into an environment variable, it exists in multiple places at once, in backups, in ps aux output, in shell history, or in log files that accidentally log environment variables. Vault secrets, fetched by a script specifically at runtime and never stored permanently anywhere, significantly reduce this attack surface.
The second structural advantage of Vault secrets over static credentials is central control: a single Vault server manages access rights, audit logs, and expiration for all secrets in a company, instead of every team maintaining its own .env files with varying security practices. If a secret is compromised, it can be revoked centrally, without anyone needing to search every single configuration file across the company.
The following sections show how Bash scripts securely fetch Vault secrets: from authentication through secure injection to dynamic database credentials with automatic rotation.
2. Vault authentication in scripts: token, AppRole, Kubernetes
For interactive use, a personal Vault token via vault login is fine. For automated scripts in CI pipelines or on servers, AppRole authentication is the established standard. AppRole separates a role ID, considered less sensitive and allowed to live in the script or repository, from a secret ID, provided separately and short lived, for example via a CI variable with restricted visibility.
On Kubernetes workloads, the Kubernetes auth method is a better fit instead: Vault verifies the pod's service account token against the Kubernetes API and issues a Vault token based on that, without any additional secret ID being distributed. For Vault secrets in production environments, this method is often the safest option, because no extra credentials need to be distributed.
#!/usr/bin/env bash
# vault-auth.sh — obtain a short-lived Vault token via AppRole
set -euo pipefail
readonly VAULT_ADDR="${VAULT_ADDR:?VAULT_ADDR not set}"
readonly ROLE_ID="${VAULT_ROLE_ID:?VAULT_ROLE_ID not set}"
readonly SECRET_ID="${VAULT_SECRET_ID:?VAULT_SECRET_ID not set}"
vault_authenticate() {
local response token
response="$(curl -sf --request POST \
--data "{\"role_id\": \"${ROLE_ID}\", \"secret_id\": \"${SECRET_ID}\"}" \
"${VAULT_ADDR}/v1/auth/approle/login")" \
|| { echo "[ERROR] Vault AppRole login failed" >&2; exit 1; }
token="$(jq -r '.auth.client_token' <<< "$response")"
[[ "$token" != "null" && -n "$token" ]] || { echo "[ERROR] No token in Vault response" >&2; exit 1; }
echo "$token"
}
VAULT_TOKEN="$(vault_authenticate)"
export VAULT_TOKEN
echo "[OK] Authenticated against Vault (token TTL managed by Vault policy)" >&2
What matters for this authentication step in the context of Vault secrets: the issued token is kept exclusively as an environment variable in the current process memory, never written to a file. After the TTL configured via Vault policy expires, the token automatically becomes invalid, regardless of whether the script actively discards it.
3. A wrapper for vault kv get with error handling
The direct call vault kv get -field=password secret/db works, but without error handling a script may still report success on a failed Vault access if the return value is empty but the exit code is not checked cleanly. A wrapper for Vault secrets explicitly checks whether the secret path exists, whether the value is not empty, and aborts with a clear error message in each of these cases instead of silently passing along an empty password.
It is also worth adding a short lived in memory cache for Vault secrets that are needed multiple times within the same script run, to avoid repeated network round trips to the Vault server without persisting the secret beyond the end of the script run.
#!/usr/bin/env bash
# vault-get.sh — safe secret retrieval with in-memory caching
set -euo pipefail
declare -A _secret_cache=()
vault_get_secret() {
local path="$1" field="$2"
local cache_key="${path}#${field}"
if [[ -n "${_secret_cache[$cache_key]:-}" ]]; then
echo "${_secret_cache[$cache_key]}"
return 0
fi
local value
value="$(vault kv get -field="$field" "$path" 2>/dev/null)" \
|| { echo "[ERROR] Failed to read ${field} from ${path}" >&2; exit 1; }
[[ -n "$value" ]] || { echo "[ERROR] Secret ${field} at ${path} is empty" >&2; exit 1; }
_secret_cache["$cache_key"]="$value"
echo "$value"
}
# Usage: db_password=$(vault_get_secret "secret/data/db" "password")
4. Injecting secrets at runtime instead of persisting them
The safest way to handle Vault secrets in Bash is to inject them exclusively into the process memory of a directly started child process instead of holding them in a shell variable that could accidentally end up in a log. The pattern env VAR="$(vault_get_secret ...)" command sets the variable only for the invocation of command, it appears neither in Bash history nor as a persistent environment variable.
Even safer is passing the value through a named pipe or file descriptor when an application reads secrets from a file instead of an environment variable, for example with <(vault_get_secret ...) as process substitution. The value then exists only as a virtual file that never touches disk, for as long as the reading process runs.
#!/usr/bin/env bash
# inject-secrets.sh — pass secrets to a child process without persisting them
set -euo pipefail
source ./vault-get.sh
# Pattern 1: scoped environment variable, only visible to the child process
run_migration() {
local db_password
db_password="$(vault_get_secret "secret/data/db" "password")"
env DB_PASSWORD="$db_password" php bin/console doctrine:migrations:migrate --no-interaction
}
# Pattern 2: process substitution — the application reads a "file" that never touches disk
run_with_secret_file() {
local api_key_path
api_key_path="$(vault_get_secret "secret/data/api" "key")"
some-tool --credentials-file <(echo "$api_key_path")
}
run_migration
Both patterns avoid Vault secrets ending up in a regular, unbounded visible shell variable. An additional protection: set +o history before handling secrets temporarily disables writing to the Bash history file, in case a secret accidentally shows up directly in an interactive command.
5. Dynamic secrets and lease management
A plain vault kv get reads out a secret that was stored once, which is fundamentally no different from a password in a configuration file, just centrally managed. The real security gain of Vault shows up with dynamic secrets: Vault generates a new, short lived database credential for every request with a defined lease duration, instead of using a single, long lived password for all access.
These dynamic Vault secrets require lease management in Bash scripts: if a script runs longer than the credential's lease duration, it must renew the lease before it expires, otherwise the database connection fails midway through the run. Vault provides an explicit renew endpoint that a script can call periodically.
#!/usr/bin/env bash
# dynamic-db-secret.sh — request and renew a dynamic database credential
set -euo pipefail
request_dynamic_db_credential() {
local response
response="$(vault read -format=json database/creds/readonly-role)"
DB_USERNAME="$(jq -r '.data.username' <<< "$response")"
DB_PASSWORD="$(jq -r '.data.password' <<< "$response")"
LEASE_ID="$(jq -r '.lease_id' <<< "$response")"
LEASE_DURATION="$(jq -r '.lease_duration' <<< "$response")"
export DB_USERNAME DB_PASSWORD
echo "[OK] Dynamic credential issued, lease duration: ${LEASE_DURATION}s"
}
renew_lease_in_background() {
local half_life=$(( LEASE_DURATION / 2 ))
while true; do
sleep "$half_life"
vault lease renew "$LEASE_ID" >/dev/null \
|| { echo "[WARN] Lease renewal failed, requesting new credential" >&2; request_dynamic_db_credential; }
done
}
request_dynamic_db_credential
renew_lease_in_background &
RENEW_PID=$!
trap 'kill "$RENEW_PID" 2>/dev/null || true' EXIT
# Application logic using DB_USERNAME / DB_PASSWORD goes here
6. Vault Agent and templates as an alternative to manual retrieval
For long running services, manually fetching Vault secrets through a Bash wrapper is costly, because authentication, caching, and lease renewal would have to be reimplemented repeatedly. Vault Agent takes over these tasks as a standalone background process: it authenticates automatically, renders secrets into a file via templates, and renews leases in the background, without the Bash script itself containing any Vault logic.
A Bash script that works with Vault Agent then only needs to wait for a file rendered by the agent and read its contents, instead of building the entire authentication and rotation logic itself. This approach is especially useful for containers where Vault Agent runs as a sidecar.
#!/usr/bin/env bash
# wait-for-agent-secret.sh — consume a secret rendered by Vault Agent
set -euo pipefail
readonly SECRET_FILE="/vault/secrets/db-credentials.env"
readonly MAX_WAIT=30
for ((i = 1; i <= MAX_WAIT; i++)); do
[[ -s "$SECRET_FILE" ]] && break
echo "[INFO] Waiting for Vault Agent to render secrets ($i/${MAX_WAIT})"
sleep 1
done
[[ -s "$SECRET_FILE" ]] || { echo "[ERROR] Vault Agent did not render secrets in time" >&2; exit 1; }
# shellcheck source=/dev/null
source "$SECRET_FILE"
echo "[OK] Secrets loaded from Vault Agent template"
7. Accounting for rotation and expiration in the script
A frequently overlooked aspect of Vault secrets is that a script fetching a secret once at the start can, over a long run, encounter a secret that was rotated or expired in the meantime. Classic static secrets rarely change during a script run, dynamic secrets with short leases, however, change regularly if a batch job runs for hours instead of minutes.
A robust script checks on every critical access whether a Vault error indicates an expired token or an invalid lease, and in that case automatically requests a fresh secret instead of aborting the entire run with an unclear authentication error. This self healing is especially important for automated cron jobs, where nobody intervenes immediately if a run fails in the middle of the night.
8. Error handling, audit logging and common pitfalls
The most common mistake when handling Vault secrets in Bash is accidentally logging secret values, for example through set -x, which prints every command including expanded variable values. A script that enables debugging while processing Vault secrets writes plaintext passwords directly to the terminal or a log file. Debugging should therefore be selectively disabled around secret handling, for example with set +x before and set -x after the sensitive code block.
A second pitfall concerns error output from vault itself: some error messages contain parts of the requested path or metadata that, combined with other log entries, allow conclusions about the secret structure. Error handling should pass Vault error messages through generically, without unnecessarily writing internal path structures into broadly accessible logs.
A third, security critical mistake: accidentally baking Vault secrets into a Docker image because a build script fetches them during the image build and writes them into a file inside the image. Secrets belong exclusively in the running container at runtime, never in the image build process itself.
9. Environment variable, file and Vault retrieval compared
The following table compares the most common approaches to handling secrets in Bash scripts.
| Approach | Persistence | Rotation | Audit |
|---|---|---|---|
| .env file | permanent on disk | manual, often forgotten | no central log |
| CI variable | stored in pipeline configuration | manual per value | depends on the CI platform |
| Vault secrets (static) | only in process memory | centrally versionable | complete access log |
| Vault secrets (dynamic) | never persisted, short lived | automatic via lease | complete access log per lease |
| Vault Agent template | temporary file with 0600 permissions | automatic in the background | complete access log |
The table clearly shows: static files and CI variables are simple to set up but lack central control, while Vault secrets in every variant offer better rotation and complete audit logging, at growing effort from static retrieval through dynamic credentials to Vault Agent.
Mironsoft
Secrets management, Vault integration and secure Bash automation
Want secrets to disappear from your scripts, not just get hidden?
We integrate Vault secrets securely into your Bash automation: AppRole authentication, runtime injection without plaintext persistence, and dynamic database credentials.
Vault audit
Analysis of existing scripts for plaintext secrets in files and environment variables
Secure injection
Wrapper functions for runtime injection without persisting anything to disk
Dynamic secrets
Database credentials with automatic lease rotation instead of static passwords
10. Summary
Bash scripts should fetch Vault secrets exclusively at runtime and never persist them permanently in files, environment variables, or Docker images. AppRole or Kubernetes authentication replaces static credentials with short lived tokens, a wrapper with clear error handling prevents silently empty secrets, and process substitution or scoped environment variables inject values only into the immediate child process.
Dynamic secrets with lease management provide the biggest security gain, because every database credential is short lived and individually revocable, but they require explicit renewal logic in long running scripts. For permanently running services, Vault Agent takes over this complexity automatically. Anyone who consistently applies these patterns to Vault secrets significantly reduces the attack surface for secret leaks without slowing down automation itself.
Fetching Vault Secrets in Bash Scripts — The essentials
AppRole authentication
Role ID and short lived secret ID instead of static credentials for automated scripts.
Runtime injection
Secrets only in scoped environment variables or process substitution, never in permanent files.
Dynamic secrets
Short lived database credentials with lease management instead of one long lived password.
No debugging with secrets
Disable set -x selectively around sensitive code blocks to prevent plaintext logging.