From 1bb16cde092fa753913b5a25987136c524423b17 Mon Sep 17 00:00:00 2001 From: github-ops Date: Tue, 16 Jun 2026 14:55:28 +0000 Subject: [PATCH] ci: load deploy secrets from openbao --- .gitea/workflows/deploy.yml | 85 +++++++++++++++++++----------------- .gitea/workflows/release.yml | 13 +++++- .gitignore | 1 + README.md | 1 + docs/deployment.md | 40 ++++++++++++----- docs/runtime-config.md | 9 ++-- scripts/README.md | 16 +++++++ scripts/load-openbao-env.sh | 57 ++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 57 deletions(-) create mode 100755 scripts/load-openbao-env.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 994716a..a7debc2 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -12,6 +12,7 @@ env: ADMIN_API_IMAGE: git.itexp.me/bsodfather/crank-community-admin-api MCP_SERVER_IMAGE: git.itexp.me/bsodfather/crank-community-mcp-server UI_IMAGE: git.itexp.me/bsodfather/crank-community-ui + OPENBAO_ENV_FILE: .openbao-env jobs: build-images: @@ -37,10 +38,18 @@ jobs: - name: Verify runner toolchain run: docker --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 }} + run: scripts/load-openbao-env.sh + - name: Login to registry run: | - printf '%s' '${{ secrets.DEPLOY_REGISTRY_TOKEN }}' | \ - docker login '${{ env.REGISTRY }}' -u '${{ secrets.DEPLOY_REGISTRY_USER }}' --password-stdin + . "$OPENBAO_ENV_FILE" + printf '%s' "$DEPLOY_REGISTRY_TOKEN" | \ + docker login '${{ env.REGISTRY }}' -u "$DEPLOY_REGISTRY_USER" --password-stdin - name: Build and push ${{ matrix.name }} run: | @@ -59,51 +68,42 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Load deployment secrets from OpenBao + env: + BAO_ADDR: ${{ secrets.BAO_ADDR }} + BAO_TOKEN: ${{ secrets.BAO_TOKEN }} + BAO_SECRET_PATH: ${{ secrets.BAO_SECRET_PATH }} + run: scripts/load-openbao-env.sh + - name: Configure SSH key run: | + . "$OPENBAO_ENV_FILE" mkdir -p ~/.ssh chmod 700 ~/.ssh - printf '%s\n' '${{ secrets.DEPLOY_SSH_KEY }}' > ~/.ssh/id_ed25519 + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 - name: Configure known hosts run: | - printf '%s\n' '${{ secrets.DEPLOY_KNOWN_HOSTS }}' > ~/.ssh/known_hosts + . "$OPENBAO_ENV_FILE" + if [ -n "${DEPLOY_KNOWN_HOSTS:-}" ]; then + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts + else + ssh-keyscan -p "${DEPLOY_PORT:-22}" "$DEPLOY_HOST" > ~/.ssh/known_hosts + fi chmod 644 ~/.ssh/known_hosts - name: Sync deployment files to server run: | - ssh -p '${{ secrets.DEPLOY_PORT }}' '${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}' \ - "mkdir -p '${{ secrets.DEPLOY_PATH }}'" - rsync -az -e "ssh -p ${{ secrets.DEPLOY_PORT }}" deploy/community/docker-compose.yml \ - '${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/docker-compose.yml' + . "$OPENBAO_ENV_FILE" + ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \ + "mkdir -p '$DEPLOY_PATH'" + rsync -az -e "ssh -p $DEPLOY_PORT" deploy/community/docker-compose.yml \ + "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/docker-compose.yml" - name: Write environment file - env: - POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - POSTGRES_USER: ${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }} - CRANK_STORAGE_ROOT: ${{ secrets.CRANK_STORAGE_ROOT }} - CRANK_PUBLISH_BIND: ${{ secrets.CRANK_PUBLISH_BIND }} - CRANK_ADMIN_BIND: ${{ secrets.CRANK_ADMIN_BIND }} - CRANK_MCP_BIND: ${{ secrets.CRANK_MCP_BIND }} - CRANK_MCP_REFRESH_MS: ${{ secrets.CRANK_MCP_REFRESH_MS }} - CRANK_LOG_LEVEL: ${{ secrets.CRANK_LOG_LEVEL }} - CRANK_MASTER_KEY: ${{ secrets.CRANK_MASTER_KEY }} - CRANK_BASE_URL: ${{ secrets.CRANK_BASE_URL }} - CRANK_CACHE_BACKEND: ${{ secrets.CRANK_CACHE_BACKEND }} - CRANK_CACHE_URL: ${{ secrets.CRANK_CACHE_URL }} - CRANK_CACHE_DEFAULT_TTL_MS: ${{ secrets.CRANK_CACHE_DEFAULT_TTL_MS }} - CRANK_SESSION_SECRET: ${{ secrets.CRANK_SESSION_SECRET }} - CRANK_PASSWORD_PEPPER: ${{ secrets.CRANK_PASSWORD_PEPPER }} - CRANK_SESSION_TTL_HOURS: ${{ secrets.CRANK_SESSION_TTL_HOURS }} - CRANK_BOOTSTRAP_ADMIN_EMAIL: ${{ secrets.CRANK_BOOTSTRAP_ADMIN_EMAIL }} - CRANK_BOOTSTRAP_ADMIN_PASSWORD: ${{ secrets.CRANK_BOOTSTRAP_ADMIN_PASSWORD }} - CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME: ${{ secrets.CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME }} - CRANK_DEMO_SEED: ${{ secrets.CRANK_DEMO_SEED }} run: | + . "$OPENBAO_ENV_FILE" tmp_env="$(mktemp)" append_if_set() { if [ -n "$2" ]; then @@ -139,15 +139,16 @@ jobs: printf 'CRANK_MCP_SERVER_IMAGE=%s:%s\n' '${{ env.MCP_SERVER_IMAGE }}' '${{ env.IMAGE_TAG }}' printf 'CRANK_UI_IMAGE=%s:%s\n' '${{ env.UI_IMAGE }}' '${{ env.IMAGE_TAG }}' } >> "$tmp_env" - cat "$tmp_env" | ssh -p '${{ secrets.DEPLOY_PORT }}' '${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}' \ - "mkdir -p '${{ secrets.DEPLOY_PATH }}' && cat > '${{ secrets.DEPLOY_PATH }}/.env'" + cat "$tmp_env" | ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \ + "mkdir -p '$DEPLOY_PATH' && cat > '$DEPLOY_PATH/.env'" rm -f "$tmp_env" - name: Validate required environment variables run: | - ssh -p '${{ secrets.DEPLOY_PORT }}' '${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}' " + . "$OPENBAO_ENV_FILE" + ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" " set -e - cd '${{ secrets.DEPLOY_PATH }}' + cd '$DEPLOY_PATH' required_vars=' POSTGRES_HOST POSTGRES_PORT @@ -172,10 +173,11 @@ jobs: - name: Deploy with Docker Compose run: | - ssh -p '${{ secrets.DEPLOY_PORT }}' '${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}' " + . "$OPENBAO_ENV_FILE" + ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" " set -e - cd '${{ secrets.DEPLOY_PATH }}' - echo '${{ secrets.DEPLOY_REGISTRY_TOKEN }}' | docker login '${{ env.REGISTRY }}' -u '${{ secrets.DEPLOY_REGISTRY_USER }}' --password-stdin + cd '$DEPLOY_PATH' + echo '$DEPLOY_REGISTRY_TOKEN' | docker login '${{ env.REGISTRY }}' -u '$DEPLOY_REGISTRY_USER' --password-stdin docker compose config -q docker compose pull admin-api mcp-server ui docker compose up -d --remove-orphans @@ -183,9 +185,10 @@ jobs: - name: Verify health endpoints run: | - ssh -p '${{ secrets.DEPLOY_PORT }}' '${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}' " + . "$OPENBAO_ENV_FILE" + ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" " set -e - cd '${{ secrets.DEPLOY_PATH }}' + cd '$DEPLOY_PATH' for attempt in \$(seq 1 30); do if curl --fail --silent http://127.0.0.1:3000/ >/dev/null \ && curl --fail --silent http://127.0.0.1:3001/health >/dev/null \ diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 8db6db7..5a08f4d 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -12,6 +12,7 @@ env: ADMIN_API_IMAGE: git.itexp.me/bsodfather/crank-community-admin-api MCP_SERVER_IMAGE: git.itexp.me/bsodfather/crank-community-mcp-server UI_IMAGE: git.itexp.me/bsodfather/crank-community-ui + OPENBAO_ENV_FILE: .openbao-env jobs: release: @@ -60,10 +61,18 @@ jobs: dir:/workspace -o spdx-json \ > dist/crank-community-${IMAGE_TAG}-sbom.spdx.json + - name: Load deployment secrets from OpenBao + env: + BAO_ADDR: ${{ secrets.BAO_ADDR }} + BAO_TOKEN: ${{ secrets.BAO_TOKEN }} + BAO_SECRET_PATH: ${{ secrets.BAO_SECRET_PATH }} + run: scripts/load-openbao-env.sh + - name: Login to registry run: | - printf '%s' '${{ secrets.DEPLOY_REGISTRY_TOKEN }}' | \ - docker login '${{ env.REGISTRY }}' -u '${{ secrets.DEPLOY_REGISTRY_USER }}' --password-stdin + . "$OPENBAO_ENV_FILE" + printf '%s' "$DEPLOY_REGISTRY_TOKEN" | \ + docker login '${{ env.REGISTRY }}' -u "$DEPLOY_REGISTRY_USER" --password-stdin - name: Build and push release images run: | diff --git a/.gitignore b/.gitignore index bbc2aba..089509e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /dist /coverage /.env +/.openbao-env /var /notes apps/ui/node_modules diff --git a/README.md b/README.md index 08edd1d..af9b915 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Crank - платформа для публикации внешних API в в - workflows лежат в `.gitea/workflows`; - Gitea Actions в этом репозитории рассчитаны только на `self-hosted` runner; +- Gitea secrets содержат только bootstrap-доступ к OpenBao: `BAO_ADDR`, `BAO_TOKEN`, `BAO_SECRET_PATH`; - внешние 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 d86be21..a7c75b8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -241,18 +241,38 @@ CD для MVP должен: - self-hosted Gitea Actions runner; - на сервере только `pull + restart`, без `docker compose up --build`. -Для private registry на сервере нужны отдельные deployment secrets: +В Gitea secrets хранится только bootstrap-доступ к OpenBao: + +- `BAO_ADDR` +- `BAO_TOKEN` +- `BAO_SECRET_PATH` + +Workflow читает `BAO_SECRET_PATH` через `scripts/load-openbao-env.sh` и получает оттуда deploy, +registry и runtime значения. Это оставляет Gitea runner без прямого списка прикладных секретов. + +Минимальный рекомендуемый путь секрета: + +```text +kv/data/crank/community/deploy +``` + +Секрет может быть KV v1 или KV v2. Для KV v2 workflow ожидает стандартный ответ вида +`data.data`. + +В OpenBao должны лежать: - `DEPLOY_REGISTRY_USER` - `DEPLOY_REGISTRY_TOKEN` - -Они используются только для `docker login` на целевом Linux host перед `docker compose pull`. - -Монолитный `DEPLOY_ENV_FILE` больше не используется. +- `DEPLOY_SSH_KEY` +- `DEPLOY_HOST` +- `DEPLOY_PORT` +- `DEPLOY_USER` +- `DEPLOY_PATH` +- `DEPLOY_KNOWN_HOSTS`, optional Workflow доставляет именно `deploy/community/docker-compose.yml` как public Community deployment manifest -и собирает `.env` на сервере из отдельных Gitea secrets. Для runtime-переменных нужно -создавать secrets с теми же именами, что и у приложений: +и собирает `.env` на сервере из значений OpenBao. Для runtime-переменных нужно +создавать ключи с теми же именами, что и у приложений: - `POSTGRES_HOST` - `POSTGRES_PORT` @@ -280,9 +300,9 @@ Workflow доставляет именно `deploy/community/docker-compose.yml` Практически это означает: -- одна runtime-переменная = один GitHub secret; -- ротация одного секрета не требует переписывать весь набор env; -- `.env` на сервере каждый deploy собирается заново из актуальных secrets. +- Gitea знает только OpenBao bootstrap credentials; +- ротация runtime/deploy значений делается в OpenBao; +- `.env` на сервере каждый deploy собирается заново из актуального OpenBao секрета. ## 11. Граница Community против future commercial delivery diff --git a/docs/runtime-config.md b/docs/runtime-config.md index 2f7e6aa..f108356 100644 --- a/docs/runtime-config.md +++ b/docs/runtime-config.md @@ -235,11 +235,12 @@ Demo/deployment: В deployment workflow больше не используется монолитный `DEPLOY_ENV_FILE`. -`.env` на сервере собирается из отдельных GitHub secrets, причем имя секрета совпадает с именем -runtime env-переменной. Это значит, что: +`.env` на сервере собирается из OpenBao. В Gitea Actions хранятся только bootstrap credentials +`BAO_ADDR`, `BAO_TOKEN` и `BAO_SECRET_PATH`, а внутри OpenBao ключи совпадают с именами +runtime env-переменных. Это значит, что: -- `CRANK_MASTER_KEY` хранится как GitHub secret `CRANK_MASTER_KEY`; -- `CRANK_DEMO_SEED` хранится как GitHub secret `CRANK_DEMO_SEED`; +- `CRANK_MASTER_KEY` хранится в OpenBao как ключ `CRANK_MASTER_KEY`; +- `CRANK_DEMO_SEED` хранится в OpenBao как ключ `CRANK_DEMO_SEED`; - и так же для остальных runtime-переменных. Для БД основной runtime-контракт теперь компонентный: diff --git a/scripts/README.md b/scripts/README.md index 54e596a..b57a717 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -37,3 +37,19 @@ Usage: ```bash ./scripts/staging-note-block.sh rmcp.itexp.me abc1234 "codex + operator" partial ``` + +## load-openbao-env.sh + +Loads one OpenBao KV secret into a local shell env file. + +Required environment variables: + +- `BAO_ADDR` +- `BAO_TOKEN` +- `BAO_SECRET_PATH` + +Optional environment variables: + +- `OPENBAO_ENV_FILE`, defaults to `.openbao-env` + +The script supports both KV v1 and KV v2 response shapes. diff --git a/scripts/load-openbao-env.sh b/scripts/load-openbao-env.sh new file mode 100755 index 0000000..a57637e --- /dev/null +++ b/scripts/load-openbao-env.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${BAO_ADDR:?BAO_ADDR is required}" +: "${BAO_TOKEN:?BAO_TOKEN is required}" +: "${BAO_SECRET_PATH:?BAO_SECRET_PATH is required}" + +output="${OPENBAO_ENV_FILE:-.openbao-env}" +payload="$(mktemp)" +trap 'rm -f "$payload"' EXIT + +curl --fail --silent --show-error \ + --header "X-Vault-Token: ${BAO_TOKEN}" \ + "${BAO_ADDR%/}/v1/${BAO_SECRET_PATH#/}" \ + > "$payload" + +python3 - "$payload" "$output" <<'PY' +import json +import re +import shlex +import sys +from pathlib import Path + +payload_path = Path(sys.argv[1]) +output_path = Path(sys.argv[2]) + +payload = json.loads(payload_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") + +name_re = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +lines = [ + "# Generated by scripts/load-openbao-env.sh", + "# Do not commit this file.", +] + +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): + value = str(value) + lines.append(f"export {key}={shlex.quote(value)}") + +output_path.write_text("\n".join(lines) + "\n", encoding="utf-8") +output_path.chmod(0o600) +print(f"Loaded {len(lines) - 2} OpenBao values into {output_path}", file=sys.stderr) +PY