# 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: ```yaml # ── 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 ```bash # 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 ```bash 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: " ``` Verify from outside: ```bash 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 `` with your `VAULT_ROOT_TOKEN` value. ```bash export VAULT_ADDR=http://161.33.139.73:8200 export VAULT_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: ```bash 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: ```bash export VAULT_ADDR=http://161.33.139.73:8200 export VAULT_TOKEN= cd Output_CASAN5_REFINED # repo root (app promoted from ) # 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 ```bash 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= 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.