diff --git a/.gitea/workflows/cd-dev.yaml b/.gitea/workflows/cd-dev.yaml index c5429ec2..8430467e 100644 --- a/.gitea/workflows/cd-dev.yaml +++ b/.gitea/workflows/cd-dev.yaml @@ -112,7 +112,6 @@ jobs: # 注入 Dev K8s Secrets - name: Inject Dev K8s Secrets env: - DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} SRE_GROUP_CHAT_ID: ${{ secrets.SRE_GROUP_CHAT_ID }} @@ -129,10 +128,17 @@ jobs: print(base64.b64encode(data).decode(), end="") PY } - write_deploy_key() { + prepare_deploy_key() { mkdir -p ~/.ssh umask 077 - printf '%s\n' "${DEPLOY_SSH_KEY}" > ~/.ssh/deploy_key + local source_key="${AWOOOI_DEPLOY_SSH_KEY_PATH:-${HOME}/.ssh/deploy_key}" + if [ ! -r "${source_key}" ]; then + echo "❌ deploy ssh key file missing: ${source_key}" >&2 + exit 1 + fi + if [ "${source_key}" != "${HOME}/.ssh/deploy_key" ]; then + cp "${source_key}" "${HOME}/.ssh/deploy_key" + fi chmod 600 ~/.ssh/deploy_key } TG_BOT_TOKEN_B64="$(secret_b64_env TELEGRAM_BOT_TOKEN)" @@ -141,7 +147,7 @@ jobs: GEMINI_API_KEY_B64="$(secret_b64_env GEMINI_API_KEY)" mkdir -p ~/.ssh - write_deploy_key + prepare_deploy_key # Keep deploy-time host keys separate from the runner user's global # known_hosts, which is also used by reboot/cold-start checks. DEPLOY_KNOWN_HOSTS="${HOME}/.ssh/deploy_known_hosts" diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 909ec50b..3f4166aa 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -1331,7 +1331,6 @@ jobs: AWOOOP_OPERATOR_API_KEY: ${{ secrets.AWOOOP_OPERATOR_API_KEY }} CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} DATABASE_URL: ${{ secrets.DATABASE_URL }} - DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} JWT_ALGORITHM: ${{ secrets.JWT_ALGORITHM }} JWT_SECRET: ${{ secrets.JWT_SECRET }} @@ -1378,10 +1377,17 @@ jobs: printf '%s' "${secret_value}" | base64 | tr -d '\n' fi } - write_deploy_key() { + prepare_deploy_key() { mkdir -p "${HOME}/.ssh" umask 077 - printf '%s\n' "${DEPLOY_SSH_KEY}" > "${HOME}/.ssh/deploy_key" + local source_key="${AWOOOI_DEPLOY_SSH_KEY_PATH:-${HOME}/.ssh/deploy_key}" + if [ ! -r "${source_key}" ]; then + echo "❌ deploy ssh key file missing: ${source_key}" >&2 + exit 1 + fi + if [ "${source_key}" != "${HOME}/.ssh/deploy_key" ]; then + cp "${source_key}" "${HOME}/.ssh/deploy_key" + fi chmod 600 "${HOME}/.ssh/deploy_key" } @@ -1411,7 +1417,7 @@ jobs: SRE_GROUP_CHAT_ID_B64="$(secret_b64_env SRE_GROUP_CHAT_ID)" # S1/S2: 統一命名 deploy_key,改用 ssh-keyscan 與強制 host key 驗證。 - write_deploy_key + prepare_deploy_key # 2026-05-13 Codex: keyscan must include ED25519 explicitly. Some # OpenSSH builds otherwise record only RSA/ECDSA, then strict deploy # SSH fails with "No ED25519 host key is known" after image push. @@ -1656,17 +1662,23 @@ jobs: - name: Deploy to K8s (ArgoCD GitOps) env: CD_PUSH_TOKEN: ${{ secrets.CD_PUSH_TOKEN }} - DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | - write_deploy_key() { + prepare_deploy_key() { mkdir -p "${HOME}/.ssh" umask 077 - printf '%s\n' "${DEPLOY_SSH_KEY}" > "${HOME}/.ssh/deploy_key" + local source_key="${AWOOOI_DEPLOY_SSH_KEY_PATH:-${HOME}/.ssh/deploy_key}" + if [ ! -r "${source_key}" ]; then + echo "❌ deploy ssh key file missing: ${source_key}" >&2 + exit 1 + fi + if [ "${source_key}" != "${HOME}/.ssh/deploy_key" ]; then + cp "${source_key}" "${HOME}/.ssh/deploy_key" + fi chmod 600 "${HOME}/.ssh/deploy_key" } mkdir -p ~/.ssh - write_deploy_key + prepare_deploy_key # 2026-05-13 Codex: mirror Inject K8s Secrets host-key handling so the # deploy job never reaches SSH with a known_hosts file missing ED25519. # 2026-06-13 Codex: use the deploy-only known_hosts file so this @@ -2195,13 +2207,18 @@ jobs: # evidence and notification signal, but no longer blocks CD completion. - name: Alert Chain Smoke Test id: alert_chain_smoke - env: - DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | - write_deploy_key() { + prepare_deploy_key() { mkdir -p "${HOME}/.ssh" umask 077 - printf '%s\n' "${DEPLOY_SSH_KEY}" > "${HOME}/.ssh/deploy_key" + local source_key="${AWOOOI_DEPLOY_SSH_KEY_PATH:-${HOME}/.ssh/deploy_key}" + if [ ! -r "${source_key}" ]; then + echo "❌ deploy ssh key file missing: ${source_key}" >&2 + exit 1 + fi + if [ "${source_key}" != "${HOME}/.ssh/deploy_key" ]; then + cp "${source_key}" "${HOME}/.ssh/deploy_key" + fi chmod 600 "${HOME}/.ssh/deploy_key" } collect_observability_statuses() { @@ -2230,7 +2247,7 @@ jobs: OTEL_COLLECTOR_STATUSES="" EVENT_EXPORTER_STATUSES="" - write_deploy_key + prepare_deploy_key DEPLOY_KNOWN_HOSTS="${HOME}/.ssh/deploy_known_hosts" if ssh-keyscan -T 5 -t ed25519,rsa,ecdsa "${K8S_SSH_HOST}" > "${DEPLOY_KNOWN_HOSTS}" 2>/dev/null && test -s "${DEPLOY_KNOWN_HOSTS}"; then SSH_OPTS="-i ${HOME}/.ssh/deploy_key -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${DEPLOY_KNOWN_HOSTS} -o ConnectTimeout=10" diff --git a/.gitea/workflows/deploy-alerts.yaml b/.gitea/workflows/deploy-alerts.yaml index 29ed0022..4ac6eb3b 100644 --- a/.gitea/workflows/deploy-alerts.yaml +++ b/.gitea/workflows/deploy-alerts.yaml @@ -31,12 +31,15 @@ jobs: python3 -c "import yaml; yaml.safe_load(open('ops/monitoring/slo-rules.yml')); print('SLO YAML OK')" - name: Setup SSH key - env: - DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | mkdir -p ~/.ssh umask 077 - printf '%s\n' "${DEPLOY_SSH_KEY}" > ~/.ssh/id_ed25519 + source_key="${AWOOOI_DEPLOY_SSH_KEY_PATH:-${HOME}/.ssh/deploy_key}" + if [ ! -r "${source_key}" ]; then + echo "deploy ssh key file missing: ${source_key}" >&2 + exit 1 + fi + cp "${source_key}" ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan 192.168.0.110 >> ~/.ssh/known_hosts diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 48ae424f..a8b7c56a 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -7,10 +7,11 @@ - Gitea API / internal API 均讀回 `{"version":"1.25.5"}`;9 個 expected private repos 均可透過 Gitea SSH 讀回 heads:`awoooi`、`ewoooc`、`2026FIFAWorldCup`、`agent-bounty-protocol`、`AwoooGo`、`stockplatform-v2`、`vibework`、`momo-pro-system`、`tsenyang-website`。 - 188 backup exporter 讀回 `awoooi_gitea_bundle_expected_repo_missing_count=0`、`failed_repo_count=0`、`checksum_missing_count=0`、`all_expected_ok=1`;Gitea private bundle backup 沒有再只靠 public repo search 判斷。 - 推上 Gitea main 後 CD `#4256` 在 API redaction 單測失敗;已補 `agent-autonomous-runtime-control` public value redaction,避免 `secret_value` 類 public-forbidden term 出現在對外 runtime-control payload。 +- Gitea job log readback 發現 multi-line deploy SSH key 不應透過 step `env` 傳遞;已移除 `cd.yaml`、`cd-dev.yaml`、`deploy-alerts.yaml` 的 `DEPLOY_SSH_KEY` raw env,改用 runner 上既有 deploy key 檔案路徑,並升級 `check-gitea-step-env-secrets.js` 對 `DEPLOY_SSH_KEY` step env fail-closed。 **仍維持 / 未完成**: -- `registry.wooo.work/v2/` 與 `harbor.wooo.work/api/v2.0/health` 仍回 502,110 `5000/5001` 仍 closed;這是 Harbor/registry cold-start / auto-recovery 缺口,不能宣稱全 110 服務完成。 -- 110 SSH 在 post-boot 高負載窗口仍會 timeout;不得因此重開 legacy / generic runner,runner 仍放最後。 +- `registry.wooo.work/v2/` 與 `harbor.wooo.work/api/v2.0/health` 曾在 post-boot 期間持續 502;15:30 後已讀回 Harbor health `200`,但仍需把 Harbor cold-start / 110 SSH timeout 納入後續 SLO scorecard,不得只用 Gitea 200 宣稱全 110 服務完成。 +- 110 SSH 在 post-boot 高負載窗口曾 timeout;不得因此重開 legacy / generic runner,runner 仍放最後。 - 未讀 secret / token / `.env` / raw sessions / SQLite / auth;未使用 GitHub / `gh` / GitHub API;未刪 repo、未 restore、未 prune、未 DB write。 **下一步**: diff --git a/scripts/ci/check-gitea-step-env-secrets.js b/scripts/ci/check-gitea-step-env-secrets.js index 0ee9e78c..e6e4e580 100755 --- a/scripts/ci/check-gitea-step-env-secrets.js +++ b/scripts/ci/check-gitea-step-env-secrets.js @@ -15,6 +15,7 @@ const workflowDir = path.join(root, ".gitea", "workflows"); const violations = []; const routeViolations = []; const secretExprPattern = /\$\{\{\s*secrets\./; +const forbiddenStepEnvSecrets = new Set(["DEPLOY_SSH_KEY"]); for (const fileName of fs.readdirSync(workflowDir).sort()) { if (!fileName.endsWith(".yml") && !fileName.endsWith(".yaml")) { @@ -70,11 +71,18 @@ for (const fileName of fs.readdirSync(workflowDir).sort()) { if (block && block.section !== "env" && secretExprPattern.test(line)) { violations.push(`${filePath}:${index + 1}:${block.section}`); } + + if (block && block.section === "env") { + const envKey = trimmed.split(":", 1)[0]; + if (forbiddenStepEnvSecrets.has(envKey)) { + violations.push(`${filePath}:${index + 1}:env:${envKey}`); + } + } }); } if (violations.length > 0) { - console.error("Gitea workflow exposes secrets through run/with text:"); + console.error("Gitea workflow exposes secrets through unsafe run/with/env transport:"); for (const violation of violations) { console.error(` - ${violation}`); }