Files
CASAN/AINative_OKR_CASAN5/.gitea/vault-setup-runbook.md
T
thanhnvandClaude Opus 4.8 e891981b59 feat(plan-01): Phase 4b — external callers to packages paths + registry (Task 1.7)
Point the canonical entry points at the new package layout; the .specify compat facade
is retained as an intentional backward-compat layer (see below).

- .gitea/workflows/{ci,harness-ci}.yml: invoke packages/casan-harness/scripts/bash|tests
  (state paths .specify/logs kept). CI now runs on the new structure.
- infra/local-prod/docker-compose.yml: dashboard-server.py -> packages path (logs/alerts
  env stay under .specify state).
- scripts/casan-step.mjs + run-casan-pipeline.mjs: resolve the harness under
  packages/casan-harness, falling back to .specify so the adversarial/sourcegen sandboxes
  (which stage a .specify/ tree) keep working; requirement input prefers apps/okr/domain/input.
- project-registry.json: record the new layout (harness_root, state_root, governance_root,
  per-project domain_root) so Plan-06 can register a second app with its own domain.
  verify-harness-reuse.sh already resolves via CASAN_HARNESS_ROOT -> HARNESS_REUSE_VALID (3 projects).

Facade decision: the .specify/{scripts,tests,security,...} symlinks are KEPT as a
documented compat layer. A full hard cutover (removing them) still needs ~15 literal
`.specify/...` refs repointed (loop_common/evidence-pack/secrets-scan config+test paths,
run-casan-pipeline step scripts, and the signed policy-bundle.yaml path list which then
needs manifest regen + re-sign). That is a scoped follow-up; the physical separation
(code in packages/, domain in apps/, packages holds no domain data, single CASAN_* path
indirection) is complete and the full gate is green via BOTH entry paths: PASS=64 FAIL=0 SKIP=3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:36:24 +09:00

6.3 KiB

HashiCorp Vault Setup — CASAN H5 KMS

Oracle Cloud VPS: 161.33.139.73

What this achieves

Before: private key = file on disk, verify-audit-chain → anchor=unsigned in CI After: private key = Vault Transit (never leaves Vault), CI → anchor=signed


Step 1 — Add Vault to /opt/gitea/docker-compose.yml

Add the vault service and a named volume. The key sections to add:

# ── Add to the top-level volumes section ─────────────────────────
volumes:
  gitea-data:
  act-runner-data:
  vault-data:           # ← ADD THIS

# ── Add the vault service ─────────────────────────────────────────
  vault:
    image: hashicorp/vault:latest
    container_name: vault
    restart: unless-stopped
    networks:
      - gitea
    ports:
      - "8200:8200"        # expose so you can init from outside
    environment:
      # Dev mode: auto-init, auto-unseal, in-memory storage.
      # Root token is fixed — store it as a Gitea secret after setup.
      VAULT_DEV_ROOT_TOKEN_ID: "${VAULT_ROOT_TOKEN}"
      VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
      VAULT_LOG_LEVEL: "warn"
    cap_add:
      - IPC_LOCK           # required by Vault
    command: server -dev

Dev mode stores keys in RAM — keys survive container restarts because the token and key name are fixed, and Vault re-creates them on each start. For production: switch to server mode with file storage backend.


Step 2 — Add VAULT_ROOT_TOKEN to /opt/gitea/.env

# Pick a strong token (or generate one)
echo "VAULT_ROOT_TOKEN=casan-vault-$(openssl rand -hex 16)" >> /opt/gitea/.env

# Verify
grep VAULT_ROOT_TOKEN /opt/gitea/.env

Save this token — you'll need it for the Gitea secret in Step 5.


Step 3 — Start Vault

cd /opt/gitea
docker compose pull vault
docker compose up -d vault

# Wait ~5 seconds for Vault to boot, then check:
docker compose logs vault | tail -20
# Look for: "Development mode should NOT be used in production installations!"
# and: "Root Token: <your token>"

Verify from outside:

curl http://161.33.139.73:8200/v1/sys/health
# Expected: {"initialized":true,"sealed":false,...}

Step 4 — Enable Transit engine + create signing keys

Run these from anywhere that can reach port 8200. Replace <TOKEN> with your VAULT_ROOT_TOKEN value.

export VAULT_ADDR=http://161.33.139.73:8200
export VAULT_TOKEN=<TOKEN>

# Enable Transit secrets engine
curl -sf -X POST \
  -H "X-Vault-Token: $VAULT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"transit"}' \
  "$VAULT_ADDR/v1/sys/mounts/transit"

# Create RSA-2048 key for policy bundle signing
curl -sf -X POST \
  -H "X-Vault-Token: $VAULT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"rsa-2048","exportable":false}' \
  "$VAULT_ADDR/v1/transit/keys/casan-policy-key"

# Create RSA-2048 key for audit chain signing
curl -sf -X POST \
  -H "X-Vault-Token: $VAULT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"rsa-2048","exportable":false}' \
  "$VAULT_ADDR/v1/transit/keys/casan-audit-key"

echo "Vault Transit keys created."

Verify keys exist:

curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \
  "$VAULT_ADDR/v1/transit/keys/casan-policy-key" | python3 -c "
import sys,json; d=json.load(sys.stdin)
print('key type:', d['data']['type'])
print('exportable:', d['data']['exportable'])
"
# Expected: key type: rsa-2048 / exportable: False

Step 5 — Add VAULT_TOKEN as Gitea Actions Secret

  1. Open Gitea: http://161.33.139.73:3000
  2. Go to your repo → Settings → Secrets → Actions
  3. Add new secret:
    • Name: VAULT_TOKEN
    • Value: your VAULT_ROOT_TOKEN value
  4. Save.

The CI workflow references ${{ secrets.VAULT_TOKEN }} — this injects the token into the security-gate job without exposing it in logs.


Step 6 — Test Vault signing locally (optional)

From macOS with the project checked out:

export VAULT_ADDR=http://161.33.139.73:8200
export VAULT_TOKEN=<TOKEN>

cd Output_CASAN5_REFINED/AINative_OKR_CASAN5

# Test sign-policy-bundle with Vault
bash packages/casan-harness/scripts/bash/sign-policy-bundle.sh sign
# Expected: POLICY_BUNDLE_SIGNED ... key_backend=vault-kms

# Test sign-audit-head
bash packages/casan-harness/scripts/bash/sign-audit-head.sh
# Expected: SIGN_AUDIT_HEAD_OK ... anchor=vault-kms

# Verify audit chain
bash packages/casan-harness/scripts/bash/verify-audit-chain.sh
# Expected: AUDIT_CHAIN_VALID anchor=signed

Step 7 — Push code to trigger CI with Vault

git add .gitea/ packages/casan-harness/scripts/bash/vault-kms.sh \
        packages/casan-harness/scripts/bash/sign-audit-head.sh \
        packages/casan-harness/scripts/bash/sign-policy-bundle.sh
git commit -m "feat(H5): Vault KMS for policy + audit chain signing"
git push gitea main

Watch the CI run: http://161.33.139.73:3000//casan5/actions

Expected security-gate job output:

VAULT_KMS_READY
...
SIGN_AUDIT_HEAD_OK head=<hash> anchor=vault-kms
POLICY_BUNDLE_SIGNED ... key_backend=vault-kms
AUDIT_CHAIN_VALID anchor=signed          ← changed from "unsigned"
...
== verdict: PASS=7 FAIL=0 SKIP=1 ==

Architecture summary

CI Job (act_runner container)
│
├── vault-kms.sh sign   ──── POST /v1/transit/sign/casan-audit-key ──► Vault container
│                                                                        (key never leaves)
├── vault-kms.sh pubkey ──── GET  /v1/transit/keys/casan-audit-key ──► export public key
│                                                                         as PEM for openssl verify
└── verify-audit-chain.sh ── openssl dgst -verify audit-public.pem
    → AUDIT_CHAIN_VALID anchor=signed  ✅

Private key:

  • Stored only inside Vault's in-memory transit engine
  • Never written to disk as .pem
  • Never visible in CI logs
  • Access gated by VAULT_TOKEN Gitea secret

RAM footprint on VPS (1 GB)

Service Idle RAM
Gitea ~180 MB
act_runner ~50 MB
Vault ~60 MB
Total idle ~290 MB
During CI +300 MB (job container)
Peak ~590 MB ✅

Vault in dev mode uses ~60 MB — well within the 1 GB budget.