diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 7b64395..05b56f6 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -36,13 +36,17 @@ jobs: uses: actions/checkout@v5 - name: Verify runner toolchain - run: docker --version + run: | + docker --version + command -v bao + bao version - name: Load deployment secrets from OpenBao env: BAO_ADDR: ${{ secrets.BAO_ADDR }} - BAO_TOKEN: ${{ secrets.BAO_TOKEN }} - BAO_SECRET_PATH: ${{ secrets.BAO_SECRET_PATH }} + BAO_ROLE_ID: ${{ secrets.BAO_ROLE_ID }} + BAO_SECRET_ID: ${{ secrets.BAO_SECRET_ID }} + OPENBAO_APP: crank run: scripts/load-openbao-env.sh - name: Login to registry @@ -68,11 +72,17 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Verify OpenBao CLI + run: | + command -v bao + bao version + - name: Load deployment secrets from OpenBao env: BAO_ADDR: ${{ secrets.BAO_ADDR }} - BAO_TOKEN: ${{ secrets.BAO_TOKEN }} - BAO_SECRET_PATH: ${{ secrets.BAO_SECRET_PATH }} + BAO_ROLE_ID: ${{ secrets.BAO_ROLE_ID }} + BAO_SECRET_ID: ${{ secrets.BAO_SECRET_ID }} + OPENBAO_APP: crank run: scripts/load-openbao-env.sh - name: Configure SSH key diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 5a08f4d..bf74ab3 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -29,6 +29,8 @@ jobs: node --version npm --version docker --version + command -v bao + bao version - name: Build release binaries run: cargo build --release -p admin-api -p mcp-server @@ -64,8 +66,9 @@ jobs: - name: Load deployment secrets from OpenBao env: BAO_ADDR: ${{ secrets.BAO_ADDR }} - BAO_TOKEN: ${{ secrets.BAO_TOKEN }} - BAO_SECRET_PATH: ${{ secrets.BAO_SECRET_PATH }} + BAO_ROLE_ID: ${{ secrets.BAO_ROLE_ID }} + BAO_SECRET_ID: ${{ secrets.BAO_SECRET_ID }} + OPENBAO_APP: crank run: scripts/load-openbao-env.sh - name: Login to registry diff --git a/README.md b/README.md index af9b915..7914856 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Crank - платформа для публикации внешних API в в - workflows лежат в `.gitea/workflows`; - Gitea Actions в этом репозитории рассчитаны только на `self-hosted` runner; -- Gitea secrets содержат только bootstrap-доступ к OpenBao: `BAO_ADDR`, `BAO_TOKEN`, `BAO_SECRET_PATH`; +- Gitea secrets содержат только AppRole-доступ к OpenBao: `BAO_ADDR`, `BAO_ROLE_ID`, `BAO_SECRET_ID`; - внешние GitHub-specific механики вроде `workflow_run`, `actions/upload-artifact`, `softprops/action-gh-release` и `ghcr.io` intentionally не используются. ## Поддерживаемые протоколы diff --git a/docs/deployment.md b/docs/deployment.md index 7997f6e..ef0dace 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -253,25 +253,32 @@ CD для MVP должен: В Gitea secrets хранится только bootstrap-доступ к OpenBao: - `BAO_ADDR` -- `BAO_TOKEN` -- `BAO_SECRET_PATH` +- `BAO_ROLE_ID` +- `BAO_SECRET_ID` -Workflow читает `BAO_SECRET_PATH` через `scripts/load-openbao-env.sh` и получает оттуда deploy, -registry и runtime значения. Это оставляет Gitea runner без прямого списка прикладных секретов. +Workflow получает short-lived token через OpenBao AppRole и читает deploy, registry и runtime +значения через `scripts/load-openbao-env.sh`. Это оставляет Gitea runner без прямого списка +прикладных секретов. -Минимальный рекомендуемый путь секрета: +OpenBao mount: ```text -kv/data/crank/community/deploy +ci/ ``` -Секрет может быть KV v1 или KV v2. Для KV v2 workflow ожидает стандартный ответ вида -`data.data`. +Workflow читает KV v2 секреты: + +```text +shared/registry +shared/deploy-ssh +projects/crank/deploy +projects/crank/runtime +``` В OpenBao должны лежать: -- `DEPLOY_REGISTRY_USER` -- `DEPLOY_REGISTRY_TOKEN` +- `REGISTRY_USERNAME` +- `REGISTRY_PASSWORD` - `DEPLOY_SSH_KEY` - `DEPLOY_HOST` - `DEPLOY_PORT` @@ -279,6 +286,13 @@ kv/data/crank/community/deploy - `DEPLOY_PATH` - `DEPLOY_KNOWN_HOSTS`, optional +`scripts/load-openbao-env.sh` маппит registry credentials для существующего workflow: + +```text +REGISTRY_USERNAME -> DEPLOY_REGISTRY_USER +REGISTRY_PASSWORD -> DEPLOY_REGISTRY_TOKEN +``` + Workflow доставляет именно `deploy/community/docker-compose.yml` как public Community deployment manifest и собирает `.env` на сервере из значений OpenBao. Для runtime-переменных нужно создавать ключи с теми же именами, что и у приложений: diff --git a/docs/runtime-config.md b/docs/runtime-config.md index f108356..8a467dc 100644 --- a/docs/runtime-config.md +++ b/docs/runtime-config.md @@ -235,9 +235,9 @@ Demo/deployment: В deployment workflow больше не используется монолитный `DEPLOY_ENV_FILE`. -`.env` на сервере собирается из OpenBao. В Gitea Actions хранятся только bootstrap credentials -`BAO_ADDR`, `BAO_TOKEN` и `BAO_SECRET_PATH`, а внутри OpenBao ключи совпадают с именами -runtime env-переменных. Это значит, что: +`.env` на сервере собирается из OpenBao. В Gitea Actions хранятся только AppRole credentials +`BAO_ADDR`, `BAO_ROLE_ID` и `BAO_SECRET_ID`, а внутри OpenBao ключи проекта +`projects/crank/runtime` совпадают с именами runtime env-переменных. Это значит, что: - `CRANK_MASTER_KEY` хранится в OpenBao как ключ `CRANK_MASTER_KEY`; - `CRANK_DEMO_SEED` хранится в OpenBao как ключ `CRANK_DEMO_SEED`; diff --git a/scripts/README.md b/scripts/README.md index b57a717..918cdda 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -40,16 +40,25 @@ Usage: ## load-openbao-env.sh -Loads one OpenBao KV secret into a local shell env file. +Logs in to OpenBao with AppRole and loads Community deploy/runtime secrets into a local shell env file. Required environment variables: - `BAO_ADDR` -- `BAO_TOKEN` -- `BAO_SECRET_PATH` +- `BAO_ROLE_ID` +- `BAO_SECRET_ID` +- `OPENBAO_APP` Optional environment variables: - `OPENBAO_ENV_FILE`, defaults to `.openbao-env` -The script supports both KV v1 and KV v2 response shapes. +The script reads KV v2 secrets from mount `ci`: + +- `shared/registry` +- `shared/deploy-ssh` +- `projects/${OPENBAO_APP}/deploy` +- `projects/${OPENBAO_APP}/runtime` + +It also exposes `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` as the workflow-compatible +aliases `DEPLOY_REGISTRY_USER` and `DEPLOY_REGISTRY_TOKEN`. diff --git a/scripts/load-openbao-env.sh b/scripts/load-openbao-env.sh index a57637e..6376942 100755 --- a/scripts/load-openbao-env.sh +++ b/scripts/load-openbao-env.sh @@ -2,36 +2,67 @@ set -euo pipefail : "${BAO_ADDR:?BAO_ADDR is required}" -: "${BAO_TOKEN:?BAO_TOKEN is required}" -: "${BAO_SECRET_PATH:?BAO_SECRET_PATH is required}" +: "${BAO_ROLE_ID:?BAO_ROLE_ID is required}" +: "${BAO_SECRET_ID:?BAO_SECRET_ID is required}" +: "${OPENBAO_APP:?OPENBAO_APP is required}" output="${OPENBAO_ENV_FILE:-.openbao-env}" payload="$(mktemp)" -trap 'rm -f "$payload"' EXIT +merged="$(mktemp)" +trap 'rm -f "$payload" "$merged"' EXIT -curl --fail --silent --show-error \ - --header "X-Vault-Token: ${BAO_TOKEN}" \ - "${BAO_ADDR%/}/v1/${BAO_SECRET_PATH#/}" \ - > "$payload" +export BAO_ADDR +BAO_TOKEN="$(bao write -field=token auth/approle/login role_id="$BAO_ROLE_ID" secret_id="$BAO_SECRET_ID")" +export BAO_TOKEN -python3 - "$payload" "$output" <<'PY' +printf '{}\n' > "$merged" + +merge_secret() { + local path="$1" + + bao kv get -mount=ci -format=json "$path" > "$payload" + + python3 - "$payload" "$merged" <<'PY' +import json +import sys +from pathlib import Path + +payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +merged_path = Path(sys.argv[2]) +merged = json.loads(merged_path.read_text(encoding="utf-8")) + +data = payload.get("data", {}) +if isinstance(data, dict) and isinstance(data.get("data"), dict): + data = data["data"] + +if not isinstance(data, dict): + raise SystemExit("OpenBao response does not contain an object secret payload") + +merged.update({key: value for key, value in data.items() if value is not None}) +merged_path.write_text(json.dumps(merged), encoding="utf-8") +PY +} + +merge_secret "shared/registry" +merge_secret "shared/deploy-ssh" +merge_secret "projects/${OPENBAO_APP}/deploy" +merge_secret "projects/${OPENBAO_APP}/runtime" + +python3 - "$merged" "$output" <<'PY' import json import re import shlex import sys from pathlib import Path -payload_path = Path(sys.argv[1]) +data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) output_path = Path(sys.argv[2]) -payload = json.loads(payload_path.read_text(encoding="utf-8")) -data = payload.get("data", {}) +if "REGISTRY_USERNAME" in data and "DEPLOY_REGISTRY_USER" not in data: + data["DEPLOY_REGISTRY_USER"] = data["REGISTRY_USERNAME"] -if isinstance(data, dict) and isinstance(data.get("data"), dict): - data = data["data"] - -if not isinstance(data, dict): - raise SystemExit("OpenBao response does not contain an object secret payload") +if "REGISTRY_PASSWORD" in data and "DEPLOY_REGISTRY_TOKEN" not in data: + data["DEPLOY_REGISTRY_TOKEN"] = data["REGISTRY_PASSWORD"] name_re = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") lines = [ @@ -43,8 +74,6 @@ for key in sorted(data): if not name_re.match(key): raise SystemExit(f"Unsupported environment variable name from OpenBao: {key}") value = data[key] - if value is None: - continue if isinstance(value, bool): value = "true" if value else "false" elif not isinstance(value, str):