# knoe-auth Round 1 — Durable Kerberos Identity + Contributor Onboarding **Status:** Implemented and operational. Architectural reference. **Owner:** chrisfu **Audience:** Jr/mid engineer onboarding to knoe.dev. No prior Kerberos or OIDC experience assumed. --- ## 1. Context `knoe-auth` is the identity system for the knoe.dev platform — the thing that decides who you are, what you can access, and how new contributors come on board. It is a long-term, multi-round initiative. This document is the architectural reference for **Round 1**, which is shipped. ### The chicken-and-egg problem To build a sophisticated identity provider safely, the team needs to be able to authenticate themselves against *something* trustworthy in the meantime. We needed a working auth store *before* we could safely build the better one. Round 1 leans on a tool that has been doing this job for thirty-five years: **MIT Kerberos**. It is unfashionable but well-understood, cryptographically sound, and we already had it running in our k3s homelab cluster. We extended it to GKE, wrapped a small enrollment portal around it, and use that to onboard contributing engineers while Round 2 (a full OIDC provider) is being built. ### Why Kerberos Kerberos *principals* are durable, DNS-like identifiers. `chrisfu@KNOE.DEV` is a stable cryptographic identity that survives any change to our web stack, our database, or our cloud provider. Once a principal exists in the KDC, it can issue tickets that any kerberized service trusts — Spring Boot via SPNEGO, Postgres via `gss` auth, SSH, NFS — without each of those services needing its own user table. That's the asset Round 1 builds on. ### Two independent Google Workspaces — do not conflate | Workspace | Role | | --- | --- | | `knoe.dev` | Internal Google Workspace for the platform. Has zero pre-knowledge of any contributor's home org. | | `prole.org` | Workspace of the first contributing engineer's organization. Independently operated. | `knoe.dev` does **not** trust `prole.org` as a domain. `prole.org` is just one engineer's email provider, no different from `gmail.com` or any other workspace a future contributor might use. Trust between knoe.dev and a new contributor is bootstrapped by the **invite**, not by the contributor's home Google domain. ### Trust model — the most important paragraph in this document When a new engineer enrolls, the trust sequence is: 1. **An admin sends an invite to a specific email address or phone number.** That contact channel — and only that channel — is the trust anchor. The admin's choice of who to invite **is** the policy. 2. **The engineer proves they control that contact** by entering a one-time password (OTP) delivered to it. Until the OTP verifies, no further steps are possible. 3. **Only after the OTP gate** is the engineer offered a Google sign-in to *corroborate* their identity. The Google sign-in is welcomed in *after* trust is already established by the invite — it is not the source of trust. 4. **TOTP** (the rotating six-digit code from Google Authenticator / Authy) is set up as the ongoing 2FA credential. 5. **Only then** is a Kerberos principal minted, a `knoe.user` row inserted, and downstream provisioning jobs (GitLab account, Gitea account) queued. This means knoe.dev never needs to pre-configure trust with any external workspace. The OAuth2 app does **not** restrict by Google `hd` (hosted domain) — any verified Google account works. The contributor's home domain is *recorded* for audit (`knoe.identity.provider_hd`) but never used to gate access. If you remember nothing else: **the invite OTP is the trust anchor. Google is corroboration. TOTP is the ongoing factor.** --- ## 2. How it's wired — file map The actual files that implement Round 1. Verify with `git ls-files` before assuming any of the below has rotted. ### Java application — `authority/` The Spring Boot service that implements the enrollment flow, the admin API, and SPNEGO-protected endpoints. Two-pom Maven build (root `pom.xml` for `knoe-db` controller, `authority/pom.xml` for `knoe-auth`); the application package is `dev.knoe.auth` and the Maven coordinate is `dev.knoe:auth`. | File | Responsibility | | --- | --- | | `authority/src/main/java/dev/knoe/auth/KnoeAuthApplication.java` | `@SpringBootApplication` entry point. | | `authority/src/main/java/dev/knoe/auth/HealthController.java` | `/health` endpoint. | | `authority/src/main/java/dev/knoe/auth/web/LoginController.java` | Form-login + SPNEGO challenge for browsers without a ticket. | | `authority/src/main/java/dev/knoe/auth/web/VerifyController.java` | Token-verify endpoint for downstream services. | | `authority/src/main/java/dev/knoe/auth/session/SessionTokenService.java` | Issues HMAC-SHA256 JWT cookies after successful auth. | | `authority/src/main/java/dev/knoe/auth/session/SessionUser.java` | Authenticated principal carried in the security context. | | `authority/src/main/java/dev/knoe/auth/user/PrincipalNormalizer.java` | Strips realm/instance from a Kerberos principal (`alice/admin@KNOE.DEV` → `alice`). | | `authority/src/main/java/dev/knoe/auth/kerberos/KerberosSpnegoService.java` | SPNEGO challenge/response handling. | | `authority/src/main/java/dev/knoe/auth/kerberos/KerberosPasswordService.java` | Password-style auth fallback for browsers that can't do SPNEGO. | | `authority/src/main/java/dev/knoe/auth/kerberos/KadminClient.java` | Shells out to `kadmin.local` (in the KDC sidecar) to `addprinc` and `cpw`. **Sanitizes input.** | | `authority/src/main/java/dev/knoe/auth/enroll/EnrollmentController.java` | Web endpoints: `GET /auth/enroll`, `POST /auth/enroll/verify-otp`, `POST /auth/enroll/identity/start`, `GET /auth/enroll/google-callback`, `GET /auth/enroll/totp`, `POST /auth/enroll/totp/verify`, `POST /auth/enroll/complete`. | | `authority/src/main/java/dev/knoe/auth/enroll/InviteService.java` | CRUD + validation against `knoe.invitation`. OTP hashing (bcrypt) and rate limiting (3 attempts). | | `authority/src/main/java/dev/knoe/auth/enroll/GoogleOAuthService.java` | Exchange OAuth2 code → ID token, validate `email_verified`, return a `GoogleIdentity` record. **No `hd` allowlist.** | | `authority/src/main/java/dev/knoe/auth/enroll/TotpService.java` | Generate TOTP secret, produce `otpauth://` URI, verify codes. | | `authority/src/main/java/dev/knoe/auth/enroll/UserProvisioningService.java` | Transactional orchestrator: inserts user/identity/totp rows, calls `KadminClient`, queues provisioning jobs. | | `authority/src/main/java/dev/knoe/auth/admin/AdminController.java` | `POST /auth/admin/invites`, `GET /auth/admin/users`, `POST /auth/admin/grants`. SPNEGO + admin-role gated. | | `authority/src/main/java/dev/knoe/auth/admin/KnobjectService.java` | CRUD on `knoe.knobject` and `knoe.access_grant`; enqueues `provisioning_job` rows. | | `authority/src/main/java/dev/knoe/auth/provisioning/ProvisioningWorker.java` | `@Scheduled` poller for `knoe.provisioning_job WHERE status = 'pending'`. Dispatches to GitLab/Gitea/CNPG. | | `authority/src/main/java/dev/knoe/auth/config/AuthProperties.java` | Typed binding for `knoe.auth.*` keys. | | `authority/src/main/java/dev/knoe/auth/config/KerberosProperties.java` | Typed binding for `knoe.auth.kerberos.*` keys. | Tests for the above live under `authority/src/test/java/dev/knoe/auth/` — notably `web/VerifyControllerTest.java` and `session/SessionTokenServiceTest.java`. ### Kubernetes manifests — GKE | File | Purpose | | --- | --- | | `deploy/gcp/gke/knoe-kdc-configmap.yaml` | `krb5.conf` + `kdc.conf` for the `KNOE.DEV` realm. | | `deploy/gcp/gke/knoe-kdc-secrets.yaml` | Master key + admin password references. Production values come from OpenBao. | | `deploy/gcp/gke/knoe-auth-deployment.yaml` | KDC sidecar + Spring Boot pod. Service principal `HTTP/auth.knoe.dev@KNOE.DEV`. | | `deploy/gcp/gke/knoe-auth-google-oidc-secret.example.yaml` | Templated Secret with `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` placeholders. Real values are not committed. | | `deploy/gcp/gke/workload-identity.yaml` | KSA↔GSA bindings for any GCP-backed secret access. | ### Kubernetes manifests — k3s (homelab and customer deploys) | File | Purpose | | --- | --- | | `deploy/opentofu/k3s/manifests/knoe/prole-kdc-configmap.yaml` | The `PROLE.LOCAL` realm KDC for the homelab cluster. The pattern the GKE configmap was modeled on. | | `deploy/opentofu/k3s/manifests/knoe/prole-kdc-secrets.example.yaml` | Templated secrets for the same. | | `deploy/opentofu/k3s/manifests/knoe/knoe-auth-deployment.yaml` | KDC + Spring Boot for the homelab. | | `deploy/opentofu/k3s/manifests/knoe/knoe-auth-kerberos-configmap.yaml` | `krb5.conf` for the auth pod's Kerberos client. | ### Bootstrap scripts — `etc/` | File | Purpose | | --- | --- | | `etc/init_kdc.sh` | Provisions the in-cluster KDC. Idempotent. Read this end-to-end before writing anything that interacts with the KDC. | | `etc/init_knoe_users.sh` | Creates `knoe.user`, `knoe.user_role`, and (per Round 1) the additional auth tables; seeds initial principals via `kadmin.local`. | | `etc/init_kerberos.sh` | Cluster-wide krb5.conf wiring for kerberized services (Postgres, etc.). | --- ## 3. Architecture ### The invite-to-enrolled flow at a glance ``` Invite URL https://auth.knoe.dev/enroll?token= │ ├─ Step 1: Enter OTP (delivered to invite email/phone) │ ─ Trust anchor. Without this, no further steps. │ ├─ Step 2: Pick a username, link with Google (any account, any hd) │ ─ Corroboration. Records provider_sub + provider_hd for audit. │ ├─ Step 3: Scan QR with authenticator app, verify TOTP code │ ─ Sets up the ongoing 2FA factor. │ └─ Step 4: System provisions: ├─ Kerberos principal: @KNOE.DEV ├─ knoe.user row + knoe.identity link to Google subject ├─ knoe.totp_credential row (encrypted secret) └─ Async queue: GitLab account, Gitea account, … ``` ### Cluster topology Round 1 lives in the GKE app cluster (`knoe-dev-0`) in the `knoe-system` namespace. The database stays where it already is — the dedicated CNPG cluster `knoe-dev-cnpg-0`. See `CLAUDE.md` for the cluster layout and storage-quota rules. ``` knoe-dev-0 / knoe-system namespace: ┌─ knoe-auth (Spring Boot) ─────────────────────────────────────────┐ │ /health │ │ /auth/login, /auth/spnego, /auth/verify │ │ /auth/enroll/* (invite → OTP → Google → TOTP → provision) │ │ /auth/admin/* (create invites, manage knobjects) │ └────────────────────────────────────────────────────────────────────┘ │ kadmin.local calls (KDC is a sidecar in the same pod) ▼ ┌─ knoe-kdc (MIT Kerberos, KNOE.DEV realm) ─────────────────────────┐ │ Container pattern from prole-kdc-configmap.yaml │ │ Realm: KNOE.DEV │ │ Cross-realm trust with PROLE.LOCAL: deferred to Round 2 │ └────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─ CNPG / knoe-db (in knoe-dev-cnpg-0) ─────────────────────────────────┐ │ knoe.user (base table) │ │ knoe.user_role (base table) │ │ knoe.invitation (Round 1) │ │ knoe.identity (Round 1 — Google sub → knoe user) │ │ knoe.totp_credential (Round 1 — encrypted TOTP secret + backups) │ │ knoe.knobject (Round 1 — platform resources) │ │ knoe.access_grant (Round 1 — user → knobject grants) │ │ knoe.provisioning_job (Round 1 — async outbox) │ └────────────────────────────────────────────────────────────────────┘ ``` The KDC runs as a sidecar in the same pod as the Spring Boot app. They share the pod network namespace, so `kadmin.local` calls reach the KDC over loopback — no Kubernetes Service required between them. This is the same pattern the k3s deployment uses. --- ## 4. Schema The `knoe.*` schema lives in CNPG (`knoe-dev-cnpg-0` namespace `knoe-db-0`). Base tables (`knoe.user`, `knoe.user_role`) are created by `etc/init_knoe_users.sh` lines 482–510. Round 1 added the six tables below; they are created by the same script later in its run. ```sql -- ──────────────────────────────────────────────────────────────────── -- knoe.invitation — admin creates one of these per invited engineer. -- The (contact, otp_hash) pair IS the trust anchor for that engineer. -- ──────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS knoe.invitation ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), token TEXT NOT NULL UNIQUE, -- URL token (long, random) contact TEXT NOT NULL, -- email or phone the invite was sent to contact_type TEXT NOT NULL DEFAULT 'email',-- 'email' | 'sms' name_hint TEXT, -- optional display-name hint from admin otp_hash TEXT NOT NULL, -- bcrypt of the 6-digit OTP otp_expires_at TIMESTAMPTZ NOT NULL, -- short TTL (10 min) otp_attempts INT NOT NULL DEFAULT 0, -- max 3 before invalidation otp_verified_at TIMESTAMPTZ, -- set when OTP passes — gate for steps 2-4 created_by TEXT NOT NULL, -- admin username created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL, -- invite URL TTL (72h) used_at TIMESTAMPTZ, -- set at step-4 completion used_by TEXT -- knoe username after use ); -- ──────────────────────────────────────────────────────────────────── -- knoe.identity — external identity corroborations. -- Round 1 only writes Google rows here; future providers reuse the table. -- ──────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS knoe.identity ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES knoe.user(id) ON DELETE CASCADE, provider TEXT NOT NULL, -- 'google' provider_sub TEXT NOT NULL, -- Google subject ID (stable per user) provider_email TEXT, provider_hd TEXT, -- 'prole.org' | 'gmail.com' | NULL — audit only verified_at TIMESTAMPTZ NOT NULL, UNIQUE(provider, provider_sub) ); -- ──────────────────────────────────────────────────────────────────── -- knoe.totp_credential — the rotating 2FA factor for ongoing logins. -- ──────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS knoe.totp_credential ( user_id INT PRIMARY KEY REFERENCES knoe.user(id) ON DELETE CASCADE, secret TEXT NOT NULL, -- AES-GCM encrypted, key in OpenBao verified_at TIMESTAMPTZ, -- NULL until first successful verification backup_codes TEXT[], -- bcrypt-hashed one-time recovery codes created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ──────────────────────────────────────────────────────────────────── -- knoe.knobject — platform-managed resources (a "knobbed object", -- something an admin can hand to a user). -- ──────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS knoe.knobject ( id SERIAL PRIMARY KEY, type TEXT NOT NULL, -- 'gitea_repo' | 'gitlab_project' | 'cnpg_role' | 'openbao_policy' name TEXT NOT NULL, platform_id TEXT, -- external identifier on target platform metadata JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(type, name) ); -- ──────────────────────────────────────────────────────────────────── -- knoe.access_grant — user ← knobject with role. -- ──────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS knoe.access_grant ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES knoe.user(id), knobject_id INT NOT NULL REFERENCES knoe.knobject(id), role TEXT NOT NULL, -- 'owner' | 'developer' | 'viewer' granted_by TEXT NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT now(), revoked_at TIMESTAMPTZ, UNIQUE(user_id, knobject_id) ); -- ──────────────────────────────────────────────────────────────────── -- knoe.provisioning_job — async outbox. -- A worker bean inside knoe-auth polls this table. -- ──────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS knoe.provisioning_job ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES knoe.user(id), job_type TEXT NOT NULL, -- 'create_gitlab_user' | 'create_gitea_user' | 'grant_cnpg_role' status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'running' | 'done' | 'failed' payload JSONB, result JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` --- ## 5. Configuration ### `authority/.../application.properties` — relevant keys ```properties knoe.auth.google.client-id=${GOOGLE_CLIENT_ID} knoe.auth.google.client-secret=${GOOGLE_CLIENT_SECRET} knoe.auth.google.redirect-uri=https://auth.knoe.dev/auth/enroll/google-callback # No allowed-domains list. Trust is established by invite OTP, not by the # developer's home Google domain. provider_hd is recorded in knoe.identity # for audit, never used for access control. knoe.auth.enroll.invite-ttl-hours=72 knoe.auth.enroll.otp-ttl-minutes=10 knoe.auth.enroll.otp-max-attempts=3 knoe.auth.enroll.totp-issuer=knoe.dev knoe.auth.provisioning.poll-interval-ms=10000 ``` ### `pom.xml` — relevant dependencies ```xml dev.samstevens.totp totp-spring-boot-starter 1.7.1 com.google.api-client google-api-client 2.4.0 ``` ### Google OAuth2 app Configured by hand in the GCP console under project `plenary-truck-485623-p7`. - **Authorized redirect URIs:** - `https://auth.knoe.dev/auth/enroll/google-callback` (Round 1 enrollment) - `https://git.knoe.dev/...` (Gitea OIDC, future round) - `https://git.prole.org/...` (GitLab OIDC, future round) - **No `hd=` restriction** at the OAuth app level. Deliberate. - Client ID and secret stored in a Kubernetes Secret modeled on `deploy/gcp/gke/knoe-auth-google-oidc-secret.example.yaml`. --- ## 6. Step-by-step enrollment flow This is what the engineer being onboarded actually experiences, what the server actually does, and where the trust transitions live. ### Step 0 — Admin creates the invite ``` POST /auth/admin/invites Body: { contact: "chrisfu@prole.org", contact_type: "email", name_hint: "Chris Fu" } Server: generate invite_token (UUID v4) generate otp (6-digit numeric, 10-min TTL) hash otp with bcrypt → otp_hash insert into knoe.invitation send email to chrisfu@prole.org: Subject: "You've been invited to knoe.dev" Body: invite URL + "Your verification code: 847291" ``` The OTP is delivered through the **same channel** as the invite URL. Both arrive in the engineer's inbox. The OTP is *not* sent to a side channel — its purpose is to prove possession of that inbox. ### Step 1 — Engineer proves contact ownership (the trust gate) ``` GET /auth/enroll?token= → InviteService validates token is not expired/used → renders landing.html with the OTP entry form POST /auth/enroll/verify-otp { otp: "847291" } → InviteService compares bcrypt(otp) with otp_hash, checks expiry → on success: set otp_verified_at=now(), advance session to step 2 → on failure: increment otp_attempts; if >= 3, invalidate the invite and return 403 ``` After this step, the session is gated. The remaining steps are unreachable without a verified OTP. **This is where the trust transition happens.** ### Step 2 — Engineer picks a username and links Google ``` GET /auth/enroll/identity → renders identity.html — username field + display-name field + "Sign in with Google" button POST /auth/enroll/identity/start { username: "chrisfu", display_name: "Chris Fu" } → store proposed username + display name in session → redirect to Google OAuth2 authorize URL (state=, nonce=, NO hd parameter) GET /auth/enroll/google-callback?code=&state= → GoogleOAuthService exchanges code for ID token → validates id_token.email_verified == true (REQUIRED) → records provider_sub, provider_email, provider_hd (provider_hd is whatever Google reports — prole.org, gmail.com, etc.) → stores GoogleIdentity in session, advances to step 3 ``` This step is **corroboration**, not authorization. The engineer's home Google workspace is not a trust source. We accept any verified Google account and record which domain it came from for audit purposes. ### Step 3 — Engineer sets up TOTP ``` GET /auth/enroll/totp → TotpService.generateSecret() — 160-bit base32 secret → store the encrypted secret in the session (NOT yet in the DB) → render totp-setup.html with: • a QR code encoding otpauth://totp/knoe.dev:?secret=...&issuer=knoe.dev • the 16-character manual key for users with no QR scanner • "Open Google Authenticator / Authy and scan this code" POST /auth/enroll/totp/verify { code: "123456" } → TotpService.verify(sessionSecret, code) — validates within ±1 30-second window → on success: advance to step 4 → on failure: re-render with the same secret (don't rotate yet) ``` ### Step 4 — System provisions ``` POST /auth/enroll/complete → UserProvisioningService.provision(session) runs in a single transaction: 1. INSERT INTO knoe.user (username, realm='KNOE.DEV', email, display_name) 2. INSERT INTO knoe.identity (provider='google', sub, email, hd) 3. INSERT INTO knoe.totp_credential (AES-encrypted secret, verified_at=now()) 4. KadminClient.addPrincipal("@KNOE.DEV") 5. UPDATE knoe.invitation SET used_at=now(), used_by= 6. INSERT INTO knoe.provisioning_job (job_type='create_gitea_user', payload={...}) 7. INSERT INTO knoe.provisioning_job (job_type='create_gitlab_user', payload={...}) → render complete.html with the engineer's new username and a "what happens next" summary (their dev environment is being set up async, they'll get a follow-up email). ``` ### Properties of this flow you can rely on - The OTP delivery channel is the identity proof. If the OTP arrives, the engineer owns that inbox. - knoe.dev never trusted `prole.org`. It trusted the admin's choice to send the invite to a `prole.org` address. Different thing. - The Google link captures the engineer's home workspace as audit data, but does not gate access. - TOTP becomes the ongoing 2FA factor. The Google sign-in is a one-time corroboration; logins after enrollment use Kerberos + TOTP. - `knoe.identity.provider_hd` records the home domain without pre-judging it. --- ## 7. Verification Run this end-to-end after any change touching the auth/Kerberos surface. 1. `kubectl -n knoe-system get pods` — `knoe-auth` and `knoe-kdc` both Running. 2. `curl https://auth.knoe.dev/health` returns 200. 3. Hit `POST /auth/admin/invites` from an admin SPNEGO session, receive an invite URL. 4. Open the enrollment URL in a fresh browser, complete all four steps using a real Google account in a non-`knoe.dev` workspace (e.g. `gmail.com`). 5. `psql … -c "SELECT username, realm FROM knoe.user WHERE username = '';"` returns the row. 6. `kinit @KNOE.DEV` from a machine that trusts the realm — succeeds. 7. `psql … -c "SELECT job_type, status FROM knoe.provisioning_job WHERE user_id = (SELECT id FROM knoe.user WHERE username = '');"` shows `create_gitea_user` and `create_gitlab_user` rows. 8. After the polling interval, those rows transition to `status = 'done'` and the corresponding accounts exist on the platforms. --- ## 8. Out of scope for Round 1 Real, named items the team has discussed. They are **not** in Round 1 — when the team gets to them, each becomes its own plan in this directory. - Cross-realm trust between `KNOE.DEV` and `PROLE.LOCAL` (so a `chrisfu@PROLE.LOCAL` ticket can talk to a `KNOE.DEV` service). Round 2. - A full OIDC provider hosted by knoe-auth, replacing the dependence on Google for downstream services. Round 2. - SSO into kerberized Postgres roles (`gss` auth) so `knoe.user` rows map directly to database principals. - A "knobject inspector" admin UI. Right now the admin API is JSON-only. - Phone/SMS-based OTP delivery. Round 1 covers email; the `contact_type` column is already present on `knoe.invitation` so adding SMS later is additive. - Self-service password rotation, recovery flows, and deactivation. Admin-only for Round 1. - **Round 1.5 — OpenBao transit encryption for secrets at rest.** `UserProvisioningService` currently writes the per-user TOTP secret and any other long-lived per-user secrets to `knoe.*` tables in the clear (relying only on PG-level encryption-at-rest). Round 1.5 wraps writes/reads with an OpenBao transit-key envelope so the database can't yield plaintext secrets even if it leaks. Tracked by the `// TODO Round 1.5` marker in `authority/src/main/java/dev/knoe/auth/enroll/UserProvisioningService.java`. --- ## 9. Glossary **KDC** — Key Distribution Center. The Kerberos server. Holds the master key for the realm; issues TGTs (ticket-granting tickets) and service tickets. **Kerberos principal** — A named identity in a realm. Format: `name@REALM` (or `service/host@REALM`). Example: `chrisfu@KNOE.DEV`. Long-lived, cryptographic, decoupled from any particular service's user table. **Realm** — A Kerberos administrative domain. Independent KDCs each own their own realm. `PROLE.LOCAL` and `KNOE.DEV` are two realms; cross-realm trust is configured separately. **Keytab** — A file containing one or more principals' long-term keys, used by services to authenticate to the KDC without an interactive password. Spring Boot reads its service principal's keytab at startup. **SPNEGO** — Simple and Protected GSSAPI Negotiation Mechanism. The HTTP-layer protocol that lets a browser holding a Kerberos ticket authenticate to a web app over the wire. RFC 4178. Spring Security has built-in support. **kadmin / kadmin.local** — The KDC's administration tool. `kadmin` runs over the network with admin credentials; `kadmin.local` runs on the KDC host itself, bypassing the network protocol. Round 1 calls `kadmin.local` from a sidecar container. **TGT (Ticket-Granting Ticket)** — The first ticket the KDC issues to a user after they prove their identity. Used to request further service tickets without re-entering credentials. **TOTP** — Time-based One-Time Password. RFC 6238. The rotating six-digit code Google Authenticator and Authy show. A shared secret + the current Unix time bucketed into 30-second windows produces the code. **OAuth2** — Authorization framework. Lets a user grant an app limited access to their account at another service. Concerns *delegation*, not *identity*. **OIDC (OpenID Connect)** — An identity layer on top of OAuth2. Adds the `id_token` (a signed JWT with claims about the user). When we say "Google sign-in" we mean OIDC over Google's OAuth2. **`hd` claim** — In a Google OIDC `id_token`, the user's hosted-domain (i.e. their Google Workspace). Optional, present only for Workspace accounts. We *record* it in `knoe.identity.provider_hd` for audit but do **not** gate access on it. **Workload Identity** — GKE feature that binds a Kubernetes ServiceAccount to a Google Cloud IAM service account. Lets pods talk to GCP APIs (e.g. KMS for our master key) without long-lived JSON keyfiles. **CNPG** — CloudNativePG. The Postgres operator we use for `knoe-db`. Runs in `knoe-dev-cnpg-0`. See `CLAUDE.md` for cluster topology. **knobject** — A platform-managed resource that an admin can hand out to a user — a Gitea repo, a Postgres role, an OpenBao policy. The name is a portmanteau of "knoe" + "object". Modeled by `knoe.knobject`; granted via `knoe.access_grant`. **Round 1, Round 2, …** — Versioning convention for the auth roadmap. Each round is a self-contained, shippable increment. Round 1 stops where the platform can safely onboard contributing engineers; Round 2 introduces the OIDC provider; later rounds tighten the screws.