Fetching Vault Secrets in Bash Scripts
AI generated
$_
#!/
Bash · HashiCorp Vault · Secrets Management · DevOps
Fetching Vault Secrets in Bash Scripts
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.

18 min read AppRole · vault kv · lease rotation · Vault Agent Vault 1.15+ · Bash 5.x

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.

11. FAQ: Fetching Vault Secrets in Bash Scripts

1Why avoid .env files?
Permanently persists secrets, risk through backups, logs and commits.
2Which auth for scripts?
AppRole for CI and servers, Kubernetes auth for pods.
3Avoiding Bash history?
Secrets only as function return value, additionally use set +o history.
4Static vs dynamic?
Dynamic secrets are generated fresh per request, with an individual lease.
5Preventing connection drops?
Background process periodically calls vault lease renew.
6When Vault Agent?
For long running services, handles auth and renewal automatically.
7Safe injection into a child process?
env VAR=$(...) command sets the variable only for that call.
8Script runs longer than the lease?
Renewal logic detects expiry and requests a fresh secret.
9Docker image without secrets?
Secrets only at container runtime, never in the build process.
10Debugging without leaking?
Disable set -x selectively around the sensitive code block.