name: "Fetch OpenBao secrets" description: "AppRole-login to OpenBao and export requested KV v2 secrets into the job env (masked). Phase 1, spikersoft-issues#545." inputs: role-id: description: "AppRole role_id — the repo's BAO_ROLE_ID Actions secret." required: true secret-id: description: "AppRole secret_id — the repo's BAO_SECRET_ID Actions secret." required: true bao-addr: description: "OpenBao address." required: false default: "https://bao.spikersoft.com" secrets: description: | One mapping per line: ENV_NAME The path is exactly what you'd pass to `bao kv get` (mount + logical path). Blank lines and `#` comments are ignored. Example: DOCKER_PASSWORD secret/ci/shared/registry password VIDEOS_S3_KEY secret/ci/backend/minio/videos secret_key required: true runs: using: "composite" steps: - shell: bash env: BAO_ADDR: ${{ inputs.bao-addr }} BAO_ROLE_ID: ${{ inputs.role-id }} BAO_SECRET_ID: ${{ inputs.secret-id }} BAO_SECRETS: ${{ inputs.secrets }} run: | set -euo pipefail command -v curl >/dev/null || { echo "::error::bao-secrets needs 'curl' on the runner"; exit 1; } command -v jq >/dev/null || { echo "::error::bao-secrets needs 'jq' on the runner"; exit 1; } # 1) AppRole login -> short-lived (15m) token. No -f so we can read the # error body; the token check below is the real gate. login=$(curl -s --max-time 15 -X POST \ --data "{\"role_id\":\"${BAO_ROLE_ID}\",\"secret_id\":\"${BAO_SECRET_ID}\"}" \ "${BAO_ADDR}/v1/auth/approle/login") || true TOKEN=$(printf '%s' "$login" | jq -r '.auth.client_token // empty') if [ -z "$TOKEN" ]; then echo "::error::OpenBao AppRole login failed: $(printf '%s' "$login" | jq -rc '.errors // "no response / network error"')" exit 1 fi echo "::add-mask::$TOKEN" # 2) fetch each requested secret into $GITHUB_ENV (masked, multiline-safe) while IFS= read -r line; do line="${line%%#*}" # shellcheck disable=SC2086 set -- $line [ "$#" -eq 0 ] && continue if [ "$#" -ne 3 ]; then echo "::error::bad 'secrets' line (need: ENV_NAME ): ${line}" exit 1 fi env_name="$1"; path="$2"; field="$3" mount="${path%%/*}"; rest="${path#*/}" # KV v2: /data/ resp=$(curl -s --max-time 15 -H "X-Vault-Token: $TOKEN" \ "${BAO_ADDR}/v1/${mount}/data/${rest}") || true val=$(printf '%s' "$resp" | jq -r --arg f "$field" '.data.data[$f] // empty') if [ -z "$val" ]; then echo "::error::no value at '${path}' field '${field}' (wrong path/field, or this role's policy denies it): $(printf '%s' "$resp" | jq -rc '.errors // "empty"')" exit 1 fi # mask every line of the value (handles multiline secrets like keys) while IFS= read -r vline; do [ -n "$vline" ] && echo "::add-mask::$vline"; done <<< "$val" { echo "${env_name}<<__BAO_EOF__" printf '%s\n' "$val" echo "__BAO_EOF__" } >> "$GITHUB_ENV" echo " ✓ ${env_name} <- ${path}#${field}" done <<< "$BAO_SECRETS" # 3) drop the token (short-lived anyway — just tidy) curl -s --max-time 10 -H "X-Vault-Token: $TOKEN" \ -X POST "${BAO_ADDR}/v1/auth/token/revoke-self" >/dev/null 2>&1 || true