Skip to content

Vault shows "not configured" — sidecar/Job race crashes sidecar before KV v2 engine is created #54

Description

@rowan-stein

User Report

Vault integration is not working. Getting error in the UI: Vault not configured/unavailable. Showing graph-required secrets only. The issue occurs after the cluster has been running for some time. Vault is running and unsealed, but does not have the secret/ KV v2 engine mounted.

Specification

Root Cause: Sidecar/Job Race Conditions + Non-Idempotent Commands

The vault-auto-init sidecar and the vault-init-unseal Job both run the same script (vault_auto_init_script) concurrently. Both share the same Vault PVC (/vault/data). There are three TOCTOU race conditions where the loser crashes due to set -euo pipefail:

Race 1: vault operator init

Both the sidecar and Job check initialized == false and call vault operator init. The second caller gets exit code 2 ("Vault is already initialized") → set -e kills the script.

Race 2: vault token create -id="dev-root"

Both check if the dev-root token exists, both find it missing, both call vault token create. The second caller gets exit code 2 ("token already exists") → script crashes. Note: this happens INSIDE the if body where set -e still applies, even though ! VAULT_TOKEN=... vault token lookup uses negation.

Race 3: vault secrets enable -path=secret

Both check if secret/ mount exists, both find it missing, both call vault secrets enable. The second caller gets "path is already in use" → the explicit exit 1 in the error handler kills the script.

Failure Timeline ("Unsealed but no secret/ mount")

T+0s    Job starts, sidecar starts (both running vault_auto_init_script)
T+15s   Both install tooling (apk + vault binary download)
T+20s   Both see Vault uninitialized → Race 1: one inits, other crashes OR 
        Job wins init, sidecar picks up initialized state
T+22s   Both unseal Vault
T+23s   Both call ensure_dev_root_token → Race 2: one creates token, other crashes
T+23s   Sidecar crashes (set -e, exit code 2) → CrashLoopBackOff
T+23s   Job continues → ensure_kv_v2_secret_mount → creates secret/ engine → exits (EXIT_AFTER_UNSEAL=true)
        --- OR ---
T+23s   Job crashes, sidecar continues → creates secret/ engine → loops
        --- OR (worst case) ---
T+23s   Both crash at Race 2 → Job retries (backoff_limit=6), eventually succeeds and exits
T+30s   Sidecar in CrashLoopBackOff, must re-download tooling from internet
T+30s   If internet available: sidecar recovers after backoff, creates secret/ engine
T+30s   If internet unavailable: sidecar crash-loops indefinitely, secret/ engine NEVER created

The "unsealed but no mount" state occurs when:

  1. The Job creates the dev-root token and secret/ engine, then exits (EXIT_AFTER_UNSEAL=true)
  2. Later, Vault pod restarts → Vault re-seals
  3. Sidecar restarts → tries to download tooling → fails (no internet) → CrashLoopBackOff
  4. OR sidecar gets tooling → re-unseals → but crashes at Race 2/3 with the Job (if Job is still retrying)
  5. Eventually Vault gets unsealed but secret/ engine is not re-created because the sidecar is in CrashLoopBackOff

Required Fixes

Fix 1: Use Vault image for sidecar and Job (eliminate internet dependency)

Replace alpine:3.19.1 with public.ecr.aws/hashicorp/vault:1.17.2 for both containers. Remove the Vault CLI download from ensure_tooling(). This eliminates the internet dependency on restarts.

In vault_values local, extraContainers section:

  • Change image from "public.ecr.aws/docker/library/alpine:3.19.1" to "public.ecr.aws/hashicorp/vault:1.17.2"
  • Remove the VAULT_VERSION env var

In kubernetes_job_v1.vault_init_unseal resource:

  • Change image from "public.ecr.aws/docker/library/alpine:3.19.1" to "public.ecr.aws/hashicorp/vault:1.17.2"

In vault_auto_init_script:

  • Remove the VAULT_VERSION variable
  • In ensure_tooling(): remove the entire Vault CLI download block, remove unzip from package list

Fix 2: Make race-prone commands idempotent (eliminate crashes)

All three race-prone operations must handle "already exists" gracefully instead of crashing:

ensure_initialized() — after vault operator init fails, re-check if Vault is now initialized:

log "initializing vault"
if ! init_json="$(vault operator init -key-shares=1 -key-threshold=1 -format=json 2>&1)"; then
  # Another process may have initialized first — check again
  if vault status -format=json 2>/dev/null | jq -e '.initialized == true' >/dev/null 2>&1; then
    log "vault was initialized by another process; loading artifacts"
    load_artifacts
    return
  fi
  log "ERROR: vault operator init failed: $init_json"
  exit 1
fi

ensure_dev_root_token() — after vault token create fails, verify the token now exists:

if ! VAULT_TOKEN="$root_token" vault token lookup "$DEV_ROOT_TOKEN" >/dev/null 2>&1; then
  log "creating dev-root token"
  if ! VAULT_TOKEN="$root_token" vault token create -id="$DEV_ROOT_TOKEN" -policy=root >/dev/null 2>&1; then
    # Race: another process created it first
    if VAULT_TOKEN="$root_token" vault token lookup "$DEV_ROOT_TOKEN" >/dev/null 2>&1; then
      log "dev-root token created by another process"
    else
      log "ERROR: failed to create dev-root token"
      exit 1
    fi
  fi
fi

ensure_kv_v2_secret_mount() — after vault secrets enable fails, re-check if the mount exists:

log "enabling kv v2 secrets engine at path secret/"
if ! VAULT_TOKEN="$root_token" vault secrets enable -path=secret -version=2 kv >/dev/null 2>&1; then
  # Race: another process may have created it
  recheck="$(VAULT_TOKEN="$root_token" vault secrets list -format=json 2>/dev/null || true)"
  if printf '%s' "$recheck" | jq -e '."secret/" | select(.type == "kv" and ((.options.version // "") | tostring) == "2")' >/dev/null 2>&1; then
    log "secret/ engine created by another process"
  else
    log "ERROR: failed to enable kv v2 secrets engine at secret/"
    exit 1
  fi
fi

Fix 3: Remove redundant vault-init-unseal Job

The sidecar already handles init, unseal, dev-root token, and KV v2 engine creation. The Job duplicates this work and is the source of all race conditions. Removing it eliminates the races entirely.

Remove the entire kubernetes_job_v1.vault_init_unseal resource and its kubernetes_config_map_v1.vault_init_unseal config map.

Files to Change

  • stacks/platform/main.tf

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions