pediatric-ai-scribe-v3/docker-entrypoint.sh
Daniel 42e59fa958 feat(security): OpenBao-backed secret injection via entrypoint
Adds an optional secret-fetch step at container boot. When OPENBAO_ADDR,
OPENBAO_ROLE_ID, and OPENBAO_SECRET_ID are set, the entrypoint
authenticates to OpenBao via AppRole, pulls kv/ped-ai/prod, and exports
each key as a process env var before exec'ing node. When OPENBAO_ADDR
is unset the entrypoint is a no-op — the legacy .env flow continues to
work unchanged (e2e container, local dev, rollback).

Changes:
- docker-entrypoint.sh: new — AppRole login + KV fetch + env inject +
  exec. Fails fast on missing/invalid creds; unsets bootstrap vars
  before launching node so they don't linger in the process env.
- Dockerfile: multi-stage copy of /bin/bao from openbao/openbao:2.5.3
  (multi-arch handled automatically by buildx manifest-list resolution).
  Adds jq for JSON parsing. Wires ENTRYPOINT to the script; CMD
  remains ["node", "server.js"].
- .env.example: documents the three vault-bootstrap variables at the
  top and notes that everything below is vault-sourced when OPENBAO_ADDR
  is set.

Rollout is two-phase for safety: rebuild image with unchanged .env
(proves no regression in legacy mode), then add the three OpenBao vars
and restart to cut over to vault-sourced secrets. Rollback at any point
is blanking OPENBAO_ADDR in .env + restart.
2026-04-22 03:59:10 +02:00

54 lines
2.2 KiB
Bash
Executable file

#!/bin/sh
# Container entrypoint. Optionally fetches secrets from OpenBao before
# starting the app. Backwards compatible: if OPENBAO_ADDR is unset (e.g. e2e
# container, local dev with a populated .env), the vault step is skipped
# and the process starts with whatever's already in the environment.
#
# When OPENBAO_ADDR is set, OPENBAO_ROLE_ID + OPENBAO_SECRET_ID are required.
# The entrypoint logs in via AppRole, fetches kv/ped-ai/prod, exports each
# key as an env var, and then unsets the auth material before execing the
# real command so the Node process doesn't carry them.
set -eu
if [ -n "${OPENBAO_ADDR:-}" ]; then
if [ -z "${OPENBAO_ROLE_ID:-}" ] || [ -z "${OPENBAO_SECRET_ID:-}" ]; then
echo "[entrypoint] FATAL: OPENBAO_ADDR is set but OPENBAO_ROLE_ID or OPENBAO_SECRET_ID is missing." >&2
exit 1
fi
export BAO_ADDR="${OPENBAO_ADDR}"
echo "[entrypoint] authenticating to OpenBao at ${OPENBAO_ADDR} via AppRole..."
BAO_TOKEN="$(bao write -field=token auth/approle/login \
role_id="${OPENBAO_ROLE_ID}" \
secret_id="${OPENBAO_SECRET_ID}" 2>&1)"
if [ -z "${BAO_TOKEN}" ] || printf '%s' "${BAO_TOKEN}" | grep -qi error; then
echo "[entrypoint] FATAL: AppRole authentication failed:" >&2
echo "${BAO_TOKEN}" >&2
exit 1
fi
export BAO_TOKEN
SECRET_PATH="${OPENBAO_KV_PATH:-kv/ped-ai/prod}"
echo "[entrypoint] fetching secrets from ${SECRET_PATH}..."
SECRET_JSON="$(bao kv get -format=json "${SECRET_PATH}" 2>/dev/null | jq -c '.data.data' 2>/dev/null || true)"
if [ -z "${SECRET_JSON}" ] || [ "${SECRET_JSON}" = "null" ]; then
echo "[entrypoint] FATAL: no secrets returned from ${SECRET_PATH}." >&2
exit 1
fi
# Export each key/value as a shell-safe env var.
# jq's @sh filter single-quote-escapes values so eval is safe.
eval "$(printf '%s' "${SECRET_JSON}" | jq -r 'to_entries[] | "export \(.key)=\(.value | @sh)"')"
# Bootstrap credentials are no longer needed in the Node process env.
unset OPENBAO_ROLE_ID OPENBAO_SECRET_ID BAO_TOKEN
SECRET_COUNT="$(printf '%s' "${SECRET_JSON}" | jq -r 'keys | length')"
echo "[entrypoint] ✅ loaded ${SECRET_COUNT} secrets from OpenBao"
else
echo "[entrypoint] OPENBAO_ADDR not set — using existing environment (legacy .env path)"
fi
exec "$@"