diff --git a/.env.example b/.env.example index 9c79357..7703cbc 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,20 @@ +# ============================================================ +# OPENBAO (optional — recommended for production) +# ============================================================ +# When these three are set, the container fetches everything else below +# from OpenBao at kv/ped-ai/prod and ignores the equivalent .env values. +# Leave them unset (or blank) to fall back to .env-only (local dev, e2e). +# +# OPENBAO_ADDR=https://app.danvics.com +# OPENBAO_ROLE_ID= +# OPENBAO_SECRET_ID= +# OPENBAO_KV_PATH=kv/ped-ai/prod # override path if needed + +# ============================================================ +# Everything below is sourced from OpenBao when OPENBAO_ADDR is set. +# Only fill these in for local dev / e2e / when running without vault. +# ============================================================ + # ============================================================ # AI PROVIDER (choose one) # ============================================================ diff --git a/Dockerfile b/Dockerfile index b87c66e..22f4fff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,21 @@ +# ─── OpenBao CLI, copied from upstream image (multi-arch automatic) ─── +# Update the tag here to adopt a newer OpenBao. Binary is statically linked, +# safe to drop into the Node alpine image as-is. +FROM openbao/openbao:2.5.3 AS bao-src + FROM node:20-alpine WORKDIR /app # ffmpeg: audio conversion for AWS Transcribe (WebM → PCM) -# curl: download Whisper models for browser-based transcription -RUN apk add --no-cache ffmpeg curl +# curl: download Whisper models for browser-based transcription +# jq: JSON parsing for the entrypoint's OpenBao secret-fetch step +RUN apk add --no-cache ffmpeg curl jq + +# Pull the bao CLI out of the upstream image — matches host arch because +# buildx pulls the right manifest-list variant per build. +COPY --from=bao-src /bin/bao /usr/local/bin/bao +RUN /usr/local/bin/bao version COPY package.json ./ # argon2 compiles native code via node-gyp — needs python3/make/g++ at build time @@ -14,6 +25,9 @@ RUN apk add --no-cache --virtual .build-deps python3 make g++ \ COPY . . +# Ensure the entrypoint is executable regardless of host file permissions +RUN chmod +x /app/docker-entrypoint.sh + RUN mkdir -p /app/data/logs # Download Browser Whisper (COMPLETE self-hosting - zero CDN dependencies) @@ -37,5 +51,9 @@ EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 +# Entrypoint wrapper handles optional OpenBao secret fetch before exec'ing CMD. +# See docker-entrypoint.sh for the logic — it is a no-op if OPENBAO_ADDR is +# unset, so legacy .env-only deployments continue to work unchanged. +ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["node", "server.js"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..46b76bc --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,54 @@ +#!/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 "$@"