feat(wave5): CI/CD pipeline + Vault KMS + OKR deploy to port 80/3001

Infrastructure (H3 CI gate, H5 KMS):
- Gitea Actions enabled (GITEA__actions__ENABLED=true)
- act_runner: Docker-outside-of-Docker for deploy job
- Vault Transit RSA-2048 signing keys (casan-audit-key, casan-policy-key)

Vault KMS scripts (H5 governance):
- .specify/scripts/bash/vault-kms.sh — sign/verify/pubkey/ensure-key
- .specify/scripts/bash/sign-audit-head.sh — sign audit chain via Vault
- Updated sign-policy-bundle.sh — Vault path + local fallback
- Updated security-gate.sh — KMS gate added (PASS=11 FAIL=0)

OKR app deployment (port 80/3001):
- Dockerfile.backend — node:22-slim (node:sqlite requires Node 22)
- Dockerfile.frontend — node:20-alpine build + nginx:alpine runtime
- nginx/nginx.conf — React SPA + /api/v1/* proxy to okr-backend:3001
- backend/entrypoint.sh — auto init DB on first run + seed
- .dockerignore

CI pipeline (.gitea/workflows/ci.yml):
- Job 1: Vitest frontend tests (H3)
- Job 2: CASAN security gate + Vault KMS signing (H4/H5)
- Job 3: Deploy OKR → port 80 (runs on push to main after tests pass)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thanhnv
2026-07-01 12:55:17 +09:00
co-authored by Claude Sonnet 4.6
parent 6e95e929f0
commit 9892e82221
15 changed files with 2528 additions and 6 deletions
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -euo pipefail
# CASAN H5 — Sign the audit chain head hash via Vault KMS (or local key fallback).
#
# Called by CI after harness tests rebuild audit.jsonl, so that
# verify-audit-chain.sh produces "anchor=signed" (not "anchor=unsigned").
#
# Usage:
# sign-audit-head.sh [audit-jsonl]
#
# Writes:
# <audit-dir>/audit-head.txt — the head hash (plain text)
# <audit-dir>/audit-head.sig — RSA signature of audit-head.txt
#
# After this script, verify-audit-chain.sh reports:
# AUDIT_CHAIN_VALID anchor=signed
#
# Environment (KMS path):
# VAULT_ADDR — e.g. http://vault:8200
# VAULT_TOKEN — token with transit/sign/casan-audit-key capability
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
AUDIT_LOG="${1:-$PROJECT_ROOT/.specify/logs/audit/audit.jsonl}"
AUDIT_DIR="$(dirname "$AUDIT_LOG")"
HEAD_FILE="$AUDIT_DIR/audit-head.txt"
HEAD_SIG="$AUDIT_DIR/audit-head.sig"
AUDIT_PUB="$PROJECT_ROOT/.specify/level5/central-governance/audit-public.pem"
if [[ ! -f "$AUDIT_LOG" ]]; then
echo "SIGN_AUDIT_HEAD_SKIP audit.jsonl not found" >&2
exit 0
fi
# ── Compute the current chain head ────────────────────────────────────────
HEAD_HASH="$(python - "$AUDIT_LOG" <<'PY'
import hashlib, json, sys
path = sys.argv[1]
previous = ""
with open(path, encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
record = json.loads(line)
core = "|".join([
record.get("timestamp",""), record.get("trace_id",""),
record.get("action",""), record.get("actor",""),
record.get("risk_level",""), record.get("decision",""),
record.get("approval_status",""), record.get("approver",""),
record.get("input_hash",""), record.get("output_hash",""),
previous,
])
previous = hashlib.sha256(core.encode()).hexdigest()
print(previous)
PY
)"
if [[ -z "$HEAD_HASH" ]]; then
echo "SIGN_AUDIT_HEAD_SKIP empty chain" >&2
exit 0
fi
printf '%s' "$HEAD_HASH" > "$HEAD_FILE"
# ── Sign the head file ────────────────────────────────────────────────────
VAULT_KMS="$SCRIPT_DIR/vault-kms.sh"
if [[ -n "${VAULT_ADDR:-}" && -n "${VAULT_TOKEN:-}" ]] && \
curl -sf "$VAULT_ADDR/v1/sys/health" >/dev/null 2>&1; then
# KMS path — sign via Vault Transit, export public key
bash "$VAULT_KMS" enable-transit
bash "$VAULT_KMS" sign "$HEAD_FILE" "$HEAD_SIG" "casan-audit-key"
bash "$VAULT_KMS" pubkey "$AUDIT_PUB" "casan-audit-key"
echo "SIGN_AUDIT_HEAD_OK head=$HEAD_HASH anchor=vault-kms"
else
# Fallback — local key (dev environment without Vault)
# IMPORTANT: Do NOT generate a new key pair here. audit-public.pem is committed
# and shared by both audit.jsonl and tool-calls.jsonl verification. Generating a
# new key overwrites audit-public.pem and breaks tool-calls-head.sig verification.
AUDIT_PRIV="$PROJECT_ROOT/.specify/level5/central-governance/audit-private.pem"
if [[ ! -f "$AUDIT_PRIV" ]]; then
echo "SIGN_AUDIT_HEAD_SKIP no private key and VAULT_ADDR not set — verify will show anchor=unsigned" >&2
exit 0
fi
openssl dgst -sha256 -sign "$AUDIT_PRIV" -out "$HEAD_SIG" "$HEAD_FILE"
echo "SIGN_AUDIT_HEAD_OK head=$HEAD_HASH anchor=local-file"
fi