feat: add bao-secrets composite action (public, no secrets) (#545)
Shared actions live in a PUBLIC repo so CI runners can clone them anonymously. They can't clone the private spikersoft-infrastructure repo (a job token is scoped to its own repo), which is why the notifications Bao cutover failed with "Repository not found". bao-secrets is pure curl/jq logic — role_id/secret_id come from the caller's Actions secrets — so nothing sensitive is exposed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
69060e49d7
commit
db71896903
@@ -1,3 +1,25 @@
|
|||||||
# gitea-actions
|
# gitea-actions
|
||||||
|
|
||||||
Shared Gitea Actions composite actions (public: logic only, no secrets). bao-secrets = OpenBao AppRole fetch. spikersoft-issues#545
|
Shared **public** Gitea Actions composite actions for the SpikerSoft org.
|
||||||
|
|
||||||
|
Public on purpose: these contain only workflow *logic* (shell/curl/jq), never
|
||||||
|
secrets. Runners can clone this repo anonymously, which is why shared actions
|
||||||
|
live here instead of the private `spikersoft-infrastructure` repo (a private
|
||||||
|
repo can't be cloned by a job token scoped to a different repo).
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
- **bao-secrets** — AppRole-login to OpenBao and export requested KV v2 secrets
|
||||||
|
into the job env (masked). Phase 1, spikersoft-issues#545.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: https://git.spikersoft.com/spikerj/gitea-actions/bao-secrets@master
|
||||||
|
with:
|
||||||
|
role-id: ${{ secrets.BAO_ROLE_ID }}
|
||||||
|
secret-id: ${{ secrets.BAO_SECRET_ID }}
|
||||||
|
secrets: |
|
||||||
|
DOCKER_PASSWORD secret/ci/shared/registry password
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference by full URL (`https://git.spikersoft.com/...`) so Gitea resolves it
|
||||||
|
from this instance instead of github.com.
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# bao-secrets — fetch OpenBao secrets in one step
|
||||||
|
|
||||||
|
Composite action for Gitea Actions (spikersoft-issues#545). AppRole-logs in to
|
||||||
|
OpenBao, reads the KV v2 paths you list, and exports them as **masked** env vars
|
||||||
|
for the rest of the job — so workflows stop carrying a pile of per-repo Actions
|
||||||
|
secrets.
|
||||||
|
|
||||||
|
## One-time setup per repo
|
||||||
|
|
||||||
|
1. Provision the repo's AppRole (once, by an admin):
|
||||||
|
```sh
|
||||||
|
export BAO_ADDR=https://bao.spikersoft.com BAO_TOKEN=<admin token>
|
||||||
|
cd /mnt/infrastructure/openbao && bash provision-ci-approles.sh
|
||||||
|
```
|
||||||
|
It prints a `role_id` and `secret_id` per repo.
|
||||||
|
2. In the repo's **Settings → Actions → Secrets**, set exactly two:
|
||||||
|
`BAO_ROLE_ID` and `BAO_SECRET_ID`. These replace `DOCKER_PASSWORD`,
|
||||||
|
`*_S3_SECRET_KEY`, `ACCESS_TOKEN_GITEA`, `DISCORD_*`, `HF_TOKEN`, … — delete
|
||||||
|
those once the workflow is migrated.
|
||||||
|
|
||||||
|
## Use it in a workflow
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- uses: spikerj/spikersoft-infrastructure/.gitea/actions/bao-secrets@master
|
||||||
|
with:
|
||||||
|
role-id: ${{ secrets.BAO_ROLE_ID }}
|
||||||
|
secret-id: ${{ secrets.BAO_SECRET_ID }}
|
||||||
|
secrets: |
|
||||||
|
DOCKER_PASSWORD secret/ci/shared/registry password
|
||||||
|
VIDEOS_S3_KEY secret/ci/backend/minio/videos secret_key
|
||||||
|
|
||||||
|
# DOCKER_PASSWORD and VIDEOS_S3_KEY are now normal (masked) env vars:
|
||||||
|
- run: echo "$DOCKER_PASSWORD" | docker login git.spikersoft.com -u ci --password-stdin
|
||||||
|
```
|
||||||
|
|
||||||
|
### `secrets` format
|
||||||
|
|
||||||
|
One mapping per line: **`ENV_NAME <kv-v2-path> <field>`**, whitespace-separated.
|
||||||
|
The path is exactly what you'd pass to `bao kv get`. Blank lines and `#`
|
||||||
|
comments are ignored. Multiline values (PEM keys, etc.) are handled.
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
|
||||||
|
| input | required | default | notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `role-id` | yes | — | `${{ secrets.BAO_ROLE_ID }}` |
|
||||||
|
| `secret-id` | yes | — | `${{ secrets.BAO_SECRET_ID }}` |
|
||||||
|
| `secrets` | yes | — | the mapping block above |
|
||||||
|
| `bao-addr` | no | `https://bao.spikersoft.com` | override for testing |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tokens issued to CI are **short-lived (15m)** and **read-only**, scoped to the
|
||||||
|
repo's own tree + `secret/ci/shared/*` by its policy. The action revokes its
|
||||||
|
token when done.
|
||||||
|
- If a path is outside the role's policy the step **fails loudly** (`permission
|
||||||
|
denied`) rather than exporting an empty value — verified in
|
||||||
|
`provision-ci-approles.sh`'s policy scoping.
|
||||||
|
- Runner needs `curl` and `jq` (present on the standard act_runner images).
|
||||||
|
- Rotation: `secret_id`s expire in 90 days — re-run `provision-ci-approles.sh`
|
||||||
|
and update the two Actions secrets; nothing else changes.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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 <kv-v2-path> <field>
|
||||||
|
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 <kv-v2-path> <field>): ${line}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env_name="$1"; path="$2"; field="$3"
|
||||||
|
mount="${path%%/*}"; rest="${path#*/}" # KV v2: <mount>/data/<rest>
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user