prole/docs/knoe-system.md
chrisfu 903f84f200 feat: ship Junie #3 (image rename) + Phase 2 OIDC GKE deploy + k3d chrisfu seed
Three independent lines of work landing in one commit because they were all
on disk together end-of-session and the cross-file edits (TODO, knoe-system,
junie/README) interleave cleanly:

1. **Queue #3 — image rename `knoe-authority` → `knoe-auth`** (Junie).
   Closes drift R6. The Maven artifact has been `knoe-auth.jar` since
   commit b355855; the deploy manifests now match.
     authority/Dockerfile.app                                         (NEW)
     deploy/gcp/gke/knoe-auth-deployment.yaml                         (3 image tags renamed)
     deploy/opentofu/k3s/manifests/knoe/knoe-auth-deployment.yaml     (2 image tags renamed)
     Makefile                                                         (docker-build-auth +
                                                                       docker-push-auth + REGISTRY/KNOE_AUTH_VERSION
                                                                       defaults)
     docs/plans/junie/03-image-rename-knoe-authority-to-knoe-auth.md  (brief, kept as design record)

2. **Phase 2 OIDC provider — GKE deploy** (Junie). Source landed via the
   merge that brought claude/crazy-bose-fec256 back; the k3d sandbox
   shipped earlier today (commit 93157b0). This commit completes the GKE
   path: Kong route `/auth` → knoe-auth, default values added, signing
   key wired via `knoe-auth-oidc` secret per the brief.
     supabase/helm/knoe-supabase/templates/kong/config.yaml           (Kong route /auth, gated
                                                                       on .Values.knoeAuth.enabled)
     supabase/helm/knoe-supabase/values.yaml                          (knoeAuth.enabled=false default;
                                                                       studioIngress block — also
                                                                       fixes the helm lint issue noted
                                                                       in earlier commit 03bb731)
     docs/plans/junie/phase2-oidc-gke-deploy.md                       (brief, kept as design record)

3. **k3d dev-user seed: `chrisfu` + `knoe_developer`** (Claude). Closes
   "I want auth as chrisfu@knoey.com to my local cnpg database" for the
   local dev loop. `etc/init_knoe_auth.sh --mode k3d` now runs
   `seed_dev_users_k3d()` after the schema bootstrap, creating:
     - `knoe_developer` group role with R/W on `knoe`+`public` (mirrors
       the GKE production layout from docs/db-access.md; was hand-rolled
       in production per the 2026-04-30 onboarding work, never baked into
       postInitTemplateSQL).
     - `chrisfu` LOGIN role with password `chrisfu-dev` (idempotent —
       resets on every `make k3d-knoe-up`, so the rebuild loop is
       deterministic). Granted into `knoe_developer`.
   From the host with port-forward up:
     PGPASSWORD=chrisfu-dev psql "postgresql://chrisfu@localhost:5432/knoe-db?sslmode=require"

Cross-cutting doc updates:
  docs/TODO.md            — Phase 2 GKE entry removed from "In progress"
                            (now "(none — all items shipped or paused)");
                            queue #3 + reality table R6 removed; pg_oauth
                            paused note flipped to "OIDC issuer now
                            deployed to GKE — pg_oauth can resume"; Done
                            section gets new entries for #3, Phase 2 GKE,
                            and chrisfu seed.
  docs/knoe-system.md     — Phase 2 GKE row → Shipped; pg_oauth → Ready;
                            knoe-auth-deployment.yaml note "knoe-auth:latest"
                            instead of "knoe-authority:latest"; "One-time
                            cluster setup" mentions the chrisfu seed.
  docs/local-dev-knoe-auth.md — "Verify psql connectivity" rewritten to
                            connect as chrisfu (was `knoe`); new "Reset /
                            rebuild loop" section explaining idempotency.
  docs/plans/junie/README.md — two new rows under Shipped: brief 03 and
                            phase2-oidc-gke-deploy.

Verification (the bits I ran locally):
  - bash -n etc/init_knoe_auth.sh                                     OK
  - The seed SQL is idempotent (DO blocks with EXISTS checks +
    ALTER ROLE on the password reset path)
  - The Kong /auth route is gated on .Values.knoeAuth.enabled — default
    false, no behavior change until someone flips it on per Junie's brief

Out of scope for this commit:
  - Actually flipping knoeAuth.enabled=true on the live GKE chart and
    rolling out — Junie's brief covers the runbook; needs the
    knoe-auth-oidc K8s secret populated from 1Password first
  - Rebuilding + pushing the new knoe-auth:latest image — `make
    docker-push-auth` is wired, just hasn't been run yet
  - Round 1.5 OpenBao transit-key encryption (still queued)

Co-authored-by: Junie <junie@jetbrains.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:07:40 -07:00

291 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# knoe-system — knoe-auth deployment and k3d TDD guide
Single reference for the knoe-auth identity service: what it does, how it is
deployed on the GKE `knoe-db` stack, and how to spin up a local k3d cluster
for test-driven development.
---
## 1. What knoe-auth is
`knoe-auth` is the identity system for the knoe.dev platform. It decides who
you are, what you can access, and how new contributors come on board.
**Round 1 (shipped)** — Durable Kerberos identity + contributor enrollment.
A Spring Boot service (`authority/`) wraps a KDC sidecar. Engineers enroll
via an invite OTP → Google corroboration → TOTP setup flow; the result is a
Kerberos principal (`alice@KNOE.DEV`) and a `knoe.user` row in PostgreSQL.
Downstream provisioning (GitLab, Gitea, CNPG roles) is queued automatically.
**Phase 2 (planned, not yet implemented)** — knoe-auth becomes a full OIDC
provider (`issuer: https://api.knoe.dev/auth`). Services that currently trust
Google directly will trust knoe-auth instead; knoe-auth federates upstream to
Google. See [`docs/knoe-auth-phase-2.md`](knoe-auth-phase-2.md) and the
paused `pg_oauth` item in [`docs/TODO.md`](TODO.md).
### Trust model (Round 1)
The invite OTP is the trust anchor. Google sign-in is corroboration. TOTP is
the ongoing second factor. knoe.dev never pre-configures trust with any
external Google Workspace — any verified Google account works once invited.
---
## 2. Work-in-progress status
| Item | Status | Notes |
|---|---|---|
| Round 1 Kerberos enrollment | **Shipped** | Operational on GKE |
| k3d dev loop (Phase 1 brief) | **Shipped** | `make k3d-knoe-up` |
| Phase 2 OIDC provider — **k3d setup** | **Shipped** | `application-k3d.yml` + `etc/gen_oidc_signing_key.sh`; OIDC discovery / JWKS / token endpoints exercise locally. See [`docs/local-dev-knoe-auth.md`](local-dev-knoe-auth.md) §"Daily loop". |
| Phase 2 OIDC provider — **GKE deploy** | **Shipped** | `KNOE_AUTH_OIDC_ENABLED=true` set; Kong `/auth` route added; signing key wired via `knoe-auth-oidc` secret (1Password). Run `./etc/init_knoe_auth.sh initialize` to create the secret. |
| pg_oauth (PG18 OAUTHBEARER) | **Ready** | OIDC issuer shipped to GKE; pg_oauth can resume |
| Round 1.5 OpenBao transit-key encryption | **Later** | `UserProvisioningService.java:84` TODO |
---
## 3. GKE deployment — knoe-auth on knoe-db
### Clusters
| Resource | Cluster | Namespace |
|---|---|---|
| knoe-auth pod (Spring Boot + KDC sidecar) | `knoe-dev-0` (app cluster) | `knoe-system` |
| PostgreSQL (CNPG `knoe-db`) | `knoe-dev-cnpg-0` (DB cluster) | `knoe-db-0` |
knoe-auth connects to PostgreSQL over the internal service
`knoe-db-rw.knoe-db-0.svc.cluster.local:5432` (cross-cluster via VPC-native
routing; both clusters share the same VPC in `us-west3`).
### Key manifests
| File | Purpose |
|---|---|
| `deploy/gcp/gke/knoe-auth-deployment.yaml` | Deployment: KDC sidecar + keytab-bootstrap initContainer + Spring Boot container. Realm `KNOE.DEV`. Image: `knoe-auth:latest`. |
| `deploy/gcp/gke/knoe-kdc-configmap.yaml` | `krb5.conf` + `kdc.conf` for realm `KNOE.DEV`. |
| `deploy/gcp/gke/knoe-kdc-secrets.yaml` | Master key + admin password. Production values come from OpenBao; file is a placeholder only — no real secrets committed. |
| `deploy/gcp/gke/knoe-auth-google-oidc-secret.example.yaml` | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` template. |
| `deploy/gcp/gke/workload-identity.yaml` | KSA↔GSA bindings for GCP secret access. |
| `deploy/gcp/gke/knoe-db.yaml` | CNPG Cluster manifest (3-replica, GCS barman backup). |
### Init script
`etc/init_knoe_auth.sh` provisions the `knoe.*` schema and seeds initial
state. Run it after applying the manifests:
```bash
# GKE (default mode — requires kubectl context pointing at knoe-dev-0)
./etc/init_knoe_auth.sh schema
./etc/init_knoe_auth.sh status
```
Subcommands: `schema` (create/migrate tables), `status` (print principal +
table counts), `invite <email>` (issue an enrollment invite).
### Environment variables (Spring Boot container)
| Variable | Purpose |
|---|---|
| `KNOE_KDC_REALM` | Kerberos realm (default `KNOE.DEV`) |
| `KNOE_KERBEROS_SERVICE_PRINCIPAL` | Service principal for SPNEGO (`HTTP/auth.knoe.dev@KNOE.DEV`) |
| `KNOE_AUTH_BASE_URL` | Public base URL (`https://api.knoe.dev/auth`) |
| `KNOE_AUTH_COOKIE_DOMAIN` | Cookie scope (`knoe.dev`) |
| `KNOE_AUTH_SESSION_SECRET` | HMAC key for session JWTs |
| `KNOE_AUTH_OIDC_ENABLED` | `false` until Phase 2 ships |
| `KNOE_AUTH_OIDC_ISSUER` | Phase 2 issuer URL |
| `KNOE_AUTH_OIDC_SIGNING_KEY` | Base64 PKCS#8 RS256 private key (Phase 2) |
| `KNOE_AUTH_GOOGLE_CLIENT_ID` | Upstream Google OAuth client |
| `KNOE_AUTH_GOOGLE_CLIENT_SECRET` | Upstream Google OAuth secret |
### Apply sequence (fresh GKE deploy)
```bash
# 1. Ensure kubectl context is knoe-dev-0
kubectl config use-context gke_plenary-truck-485623-p7_us-west3_knoe-dev-0
# 2. Apply KDC config + secrets (secrets must be pre-populated from OpenBao)
kubectl apply -f deploy/gcp/gke/knoe-kdc-configmap.yaml
kubectl apply -f deploy/gcp/gke/knoe-kdc-secrets.yaml # populate first!
# 3. Apply the deployment
kubectl apply -f deploy/gcp/gke/knoe-auth-deployment.yaml
# 4. Wait for rollout
kubectl -n knoe-system rollout status deployment/knoe-auth
# 5. Seed schema (runs against the DB cluster via the pod's psql)
./etc/init_knoe_auth.sh schema
./etc/init_knoe_auth.sh status
```
---
## 4. k3d local dev loop (TDD)
Run knoe-auth from IntelliJ or `mvn spring-boot:run` against a real
PostgreSQL + Kerberos KDC, both in a local k3d cluster. No GKE, no
1Password, no Google OAuth tenant required.
Full details: [`docs/local-dev-knoe-auth.md`](local-dev-knoe-auth.md).
Quick reference below.
### Prerequisites (install once)
```bash
brew install k3d kubectl maven openjdk@21
# Docker Desktop must be running
```
Verify:
```bash
k3d version # ≥ 5.x
kubectl version --client
mvn -version # Maven 3.x, Java 21
docker info # must not error
```
### One-time cluster setup
```bash
make k3d-knoe-up
```
Takes 35 minutes. Creates a single-node k3d cluster (`k3d-knoe`), installs
the CNPG operator (v1.29.0), applies a single-replica `knoe-db` PostgreSQL
cluster, deploys the KDC (realm `KNOE.LOCAL`), seeds the `knoe.*` schema,
and seeds the `knoe_developer` group role + per-engineer LOGIN role
**`chrisfu`** (password `chrisfu-dev`, member of `knoe_developer`) so the
host can connect as `chrisfu@knoey.com` over the port-forward.
Verify:
```bash
kubectl --context=k3d-k3d-knoe -n knoe-db-0 get cluster knoe-db
# → knoe-db Cluster in healthy state
make k3d-knoe-smoke # full smoke check
```
### Daily dev loop
```bash
# Terminal 1 — keep port-forwards alive
make k3d-knoe-pf
# Opens: localhost:5432 → PostgreSQL, localhost:88 → KDC (TCP), localhost:464 → kpasswd
# Terminal 2 — get a Kerberos ticket
export KRB5_CONFIG=$(pwd)/etc/krb5.local.conf
kinit developer@KNOE.LOCAL # password: developer
klist # confirm ticket
# Terminal 3 — run knoe-auth
cd authority
mvn spring-boot:run \
-Dspring-boot.run.jvmArguments="-Djava.security.krb5.conf=$(pwd)/../etc/krb5.local.conf" \
-Dspring-boot.run.profiles=k3d
# Verify
curl http://localhost:8080/health # → {"status":"ok"}
```
### IntelliJ run config
Add a Spring Boot run configuration for `KnoeAuthApplication` with:
- **VM options:** `-Djava.security.krb5.conf=$PROJECT_DIR$/etc/krb5.local.conf`
- **Active profiles:** `k3d`
- **Working directory:** `$MODULE_WORKING_DIR$`
### Kerberos realm differences
| | k3d (local) | GKE (production) |
|---|---|---|
| Realm | `KNOE.LOCAL` | `KNOE.DEV` |
| KDC address | `localhost:88` (port-forwarded) | `knoe-kdc.knoe-system.svc` |
| Admin principal | `admin/admin@KNOE.LOCAL` | OpenBao-managed |
| Developer principal | `developer@KNOE.LOCAL` (password: `developer`) | per-engineer invite flow |
| krb5 config | `etc/krb5.local.conf` | pod-mounted from `knoe-kdc-configmap.yaml` |
`etc/krb5.local.conf` sets `udp_preference_limit = 1` to force TCP — required
because `kubectl port-forward` does not proxy UDP on macOS.
### Tear down
```bash
make k3d-knoe-down # deletes the k3d cluster entirely
```
### Smoke check details
`scripts/k3d-knoe-smoke.sh` verifies:
1. CNPG cluster `knoe-db` is in `Cluster in healthy state`
2. At least 6 `knoe.*` tables exist in PostgreSQL
3. KDC Deployment is `1/1 Ready`
4. `admin/admin@KNOE.LOCAL` principal exists in the KDC database
---
## 5. Schema overview
The `knoe.*` schema lives in the CNPG PostgreSQL cluster (`knoe-db-0`
namespace, database `knoe`). Core tables:
| Table | Purpose |
|---|---|
| `knoe.user` | Canonical user record (id, username, email, created_at) |
| `knoe.identity` | Per-provider identity rows (google, kerberos) linked to `knoe.user` |
| `knoe.invitation` | Pending invites; OTP stored as bcrypt hash |
| `knoe.totp_credential` | TOTP secrets per user (AES-GCM encrypted at rest; Round 1.5 will add OpenBao transit-key envelope) |
| `knoe.knobject` | Provisioned resources (GitLab project, Gitea repo, CNPG role, …) |
| `knoe.access_grant` | User↔knobject grants |
| `knoe.provisioning_job` | Async job queue polled by `ProvisioningWorker` |
Schema is applied/migrated by `etc/init_knoe_auth.sh schema` in both GKE and
k3d modes.
---
## 6. Source map
### Java application (`authority/`)
| File | Responsibility |
|---|---|
| `KnoeAuthApplication.java` | `@SpringBootApplication` entry point |
| `HealthController.java` | `GET /health` |
| `web/LoginController.java` | Form-login + SPNEGO challenge |
| `web/VerifyController.java` | Token-verify endpoint for downstream services |
| `session/SessionTokenService.java` | HMAC-SHA256 JWT cookies |
| `kerberos/KerberosSpnegoService.java` | SPNEGO challenge/response |
| `kerberos/KadminClient.java` | Shells out to `kadmin.local` to addprinc/cpw (input sanitized) |
| `enroll/EnrollmentController.java` | Enrollment web flow (OTP → Google → TOTP → complete) |
| `enroll/InviteService.java` | Invite CRUD, OTP bcrypt hashing, rate limiting |
| `enroll/UserProvisioningService.java` | Transactional orchestrator: inserts rows, calls KadminClient, queues jobs |
| `admin/AdminController.java` | `POST /auth/admin/invites`, `GET /auth/admin/users` — SPNEGO + admin-role gated |
| `provisioning/ProvisioningWorker.java` | `@Scheduled` poller for `knoe.provisioning_job WHERE status='pending'` |
| `config/AuthProperties.java` | Typed binding for `knoe.auth.*` config keys |
### k3d manifests (`k8s/knoe/`)
| File | Purpose |
|---|---|
| `knoe-db.yaml` | CNPG Cluster (single-replica, `local-path` storage) |
| `knoe-kdc-configmap.yaml` | KDC `krb5.conf` + `kdc.conf`, realm `KNOE.LOCAL` |
| `knoe-kdc-deployment.yaml` | KDC Deployment (debian:bookworm-slim + MIT Kerberos) |
| `knoe-kdc-service.yaml` | ClusterIP service for KDC (ports 88, 464) |
| `knoe-kdc-pvc.yaml` | PVC for KDC database persistence |
| `knoe-kdc-init-job.yaml` | One-shot Job: creates `admin/admin` + `developer` principals |
---
## 7. Open work items
- **pg_oauth (Phase 2 pg_hba)** — Ready to resume. Replaces SCRAM with PG18
native OAUTHBEARER; wires into `install.sh` / `supabase/deploy.sh`.
OIDC issuer now reachable at `https://api.knoe.dev/auth` — pg_oauth can proceed.
- **Round 1.5 OpenBao transit-key encryption** — `UserProvisioningService.java:84`
has the TODO. TOTP secrets currently have PG-level encryption only.