Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ mini-auth is two things:
of the family: the vision, the catalog, the architecture, the recursive integrations, the open
design questions, and the roadmap — the full architecture reference.

> **Which doc do I want?** Two doors, pick one: **[docs/TEACHING.md](docs/TEACHING.md)** to *learn*
> (concepts → labs → security, in teaching order), or **[docs/DIRECTION.md](docs/DIRECTION.md)** to
> *understand the architecture* (vision, catalog, runtime map). Everything else — `GLOSSARY.md`,
> `LEARNING.md`, per-service `README.md`s — is reference the two doors link you to when you need it.

> 🎓 **New here? Start with [docs/TEACHING.md](docs/TEACHING.md).** It's a course-style track
> (concepts → hands-on labs → security) that teaches authentication & authorization using the family
> as the worked example, and it routes you to the right next step in one screen.
Expand Down
10 changes: 5 additions & 5 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ student reading the keyring can't see the layer the diagram promises.

## P3

### 7. [ ] Reduce doc entry-point overload
### 7. [x] Reduce doc entry-point overload
**Why:** A newcomer landing in `docs/` faces ~8 index-like files; `TEACHING-OUTLINE.md` (a
design-history doc) sits next to the live syllabus with a near-identical name and will be opened by
mistake.
Expand All @@ -119,7 +119,7 @@ mistake.
> for the syllabus, and fix any links that referenced it. Tighten the framing so `LEARNING.md` reads as
> "once you've done the course, re-walk the source," not a competing start point.

### 8. [ ] Add a "Learning?" banner to each service README + render diagrams for editor readers
### 8. [x] Add a "Learning?" banner to each service README + render diagrams for editor readers
**Why:** Per-service READMEs are reference docs with no pointer back into the curriculum, and the one
visual layer (mermaid) degrades to raw source for the code-first reader working in an editor.

Expand All @@ -130,7 +130,7 @@ visual layer (mermaid) degrades to raw source for the code-first reader working
> pre-rendered SVG/PNG alongside each `.md`, or add one line per diagram telling editor readers how to
> view it (e.g. paste into mermaid.live).

### 9. [ ] Add an mTLS / workload-identity concept-stage doc
### 9. [x] Add an mTLS / workload-identity concept-stage doc
**Why:** mini-ca ships and the glossary defines CSR/PoP/SAN/EKU, but there's no concept doc on *why*
mTLS / how chain-to-root validation works, and mini-ca is absent from the stage ladder (only a
code-tour capstone).
Expand All @@ -141,7 +141,7 @@ code-tour capstone).
> with a "Now read it" box into mini-ca's `ca/` package. Add it to the stage ladder in
> `docs/TEACHING.md` so the mini-ca capstone has a concept stage behind it.

### 10. [ ] Reframe or upgrade the mini-console "exercise harness"
### 10. [x] Reframe or upgrade the mini-console "exercise harness"
**Why:** All reviewers flagged that the harness flows are integration smoke-tests a learner *watches*,
not exercises they *do* (make a choice, get feedback). The framing oversells "exercise."

Expand All @@ -152,7 +152,7 @@ not exercises they *do* (make a choice, get feedback). The framing oversells "ex
> supply a grant/scope and have the harness show the resulting allow vs deny decision. If you change
> behavior, keep results secret-free and run `./gradlew build`.

### 11. [ ] (Optional) Add a "Stage 0.5: the tools you'll use" primer
### 11. [x] (Optional) Add a "Stage 0.5: the tools you'll use" primer
**Why:** Every lab assumes curl, env vars, background jobs, and jq/python. Lowers the on-ramp for the
bottom edge of the stated audience.

Expand Down
9 changes: 5 additions & 4 deletions docs/LEARNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ each other, so reading them in directory order (or alphabetically) drops you int
deep end. This is the order that builds the concepts up before they're used — it follows
the family's actual dependency DAG, smallest and most-foundational first.

> **Concepts-first vs. code-first.** This is the *code-first* path — it walks the source in
> dependency order. If you'd rather build the *ideas* first (with hands-on labs and a security
> track), start at [`TEACHING.md`](TEACHING.md) and come here when you want to read the
> implementation. The two are designed to cross-link: each teaching stage points at the files below.
> **Not a competing start point.** If you haven't done the course yet, start at
> [`TEACHING.md`](TEACHING.md) — it builds the *ideas* first, with hands-on labs and a security
> track. Come here once you've done that, to re-walk the same territory as *source*, in dependency
> order instead of teaching order. The two are designed to cross-link: each teaching stage points at
> the files below.

**How to use this.** At each stop, read the named files (and run the quick-start where
there is one), then move on. Each stop lists the *concept you learn there* — that's the
Expand Down
6 changes: 6 additions & 0 deletions docs/TEACHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
> **First time on this machine?** Read [`SETUP.md`](SETUP.md) before your first lab — a five-minute
> preflight (JDK 21, the first build needs network) plus a rescue list for the usual step-1 dead ends
> (port-in-use on 8455/8466/8477…, wrong JDK, offline build, stray background servers).
>
> **New to the command line?** [`tutorials/00-tools-you-will-use.md`](tutorials/00-tools-you-will-use.md)
> is an optional primer on `curl`, exporting env vars, backgrounding/killing a server, and decoding a
> JWT with `jq` or `python3` — skip it if all of that is already muscle memory.

You are standing in front of four kinds of document. Keep them straight and the repo opens up:

Expand Down Expand Up @@ -57,13 +61,15 @@ proves it. Do them in order and each one earns the next. (This is a different or
| Stage | You'll be able to… | Concept | Lab |
| --- | --- | --- | --- |
| **0** | Name which problem each `mini-` service solves | [`authn-vs-authz`](concepts/authn-vs-authz.md) | — |
| **0.5** *(optional)* | Use `curl`, env vars, background jobs, and `jq`/`python3` comfortably | — | [`00-tools-you-will-use`](tutorials/00-tools-you-will-use.md) |
| **1** | See a decision as a pure function; explain deny-by-default | [`authorization-model`](concepts/authorization-model.md) | [`01-resolve-a-principal`](tutorials/01-resolve-a-principal.md) |
| **2** | Say what a signed token *is* and verify one offline, by hand | [`what-a-token-is`](concepts/what-a-token-is.md) | [`02-build-and-verify-a-token-by-hand`](tutorials/02-build-and-verify-a-token-by-hand.md) ← **keystone** |
| **3** | Trace a machine identity end to end | [`oauth-and-oidc-flows`](concepts/oauth-and-oidc-flows.md) | [`03-machine-identity-end-to-end`](tutorials/03-machine-identity-end-to-end.md) |
| **3.5** | Explain how a passkey proves identity without a shared secret | [`what-a-passkey-is`](concepts/what-a-passkey-is.md) | *(see it in [`04`](tutorials/04-human-sso-end-to-end.md))* |
| **4** | Run a human SSO login: PKCE, passkeys, sessions, refresh | [`sessions-vs-tokens`](concepts/sessions-vs-tokens.md) | [`04-human-sso-end-to-end`](tutorials/04-human-sso-end-to-end.md) |
| **5** | Gate a no-auth app via forward-auth | *(reuse stage 4)* | [`05-gate-a-no-auth-app`](tutorials/05-gate-a-no-auth-app.md) |
| **6** | Explain how the family protects its own keys | [`envelope-encryption-and-kms`](concepts/envelope-encryption-and-kms.md) | [`06-protect-the-signing-keys`](tutorials/06-protect-the-signing-keys.md) |
| **7** | Explain mTLS and chain-to-root validation for workload identity | [`certificates-and-mtls`](concepts/certificates-and-mtls.md) | *(no lab yet — a code-tour capstone over `mini-ca`)* |
| **∥** | Carry the family's security reflexes anywhere | [`secure-design-invariants`](concepts/secure-design-invariants.md) | the [`security/`](security/) threat labs |

**The keystone is stage 2.** Building and verifying a token signature by hand — then tampering one
Expand Down
132 changes: 132 additions & 0 deletions docs/concepts/certificates-and-mtls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Certificates and mTLS — proving identity between *workloads*

> **Concept doc (explanation).** The capstone concept behind **mini-ca**. New terms link to
> [`GLOSSARY.md`](../GLOSSARY.md#certificates-mini-ca); the rationale for shipping a small internal
> CA instead of a full PKI lives in [`DIRECTION.md`](../DIRECTION.md). There is no dedicated lab yet
> — this doc ends with a "Now read it" code tour of `services/mini-ca`'s `ca/` package instead.
> Read [`what-a-token-is.md`](what-a-token-is.md) first: a certificate is a *different* proof
> primitive for a *different* actor (a workload, not a person or a caller with a bearer token), but
> it reuses the same signature-verification idea.

If you take one idea from this doc, take this:

> **A certificate is a public key with a signed statement attached: "the CA that everyone already
> trusts vouches that this public key belongs to this name." mTLS is TLS where *both* sides present
> one of these and check the other's, so a *workload* — not a person typing a password, not a bearer
> token in a header — proves who it is using a private key it never has to send anywhere.**

---

## The problem certificates solve

Tokens (stage 2) and passkeys (stage 3.5) both answer "who is the **caller**" — a request arrives,
carrying a bearer token or having just finished a login ceremony. But **service-to-service**
traffic in a homelab has no caller in that sense: `mini-gateway` calling `mini-oidc`'s JWKS
endpoint, or one internal service calling another, is a *workload* talking to a *workload*. Neither
side is a human who can complete a passkey ceremony, and a long-lived shared secret baked into
config is exactly the kind of static credential the family's invariants try to avoid.

**mTLS (mutual TLS)** answers this: TLS's normal server-authentication handshake, run in *both*
directions. Each side holds a private key and presents a certificate; each side verifies the
other's certificate against a **trust anchor** both were configured to trust. Nothing secret ever
crosses the wire — only signatures and public keys, exactly like a token's JWS or a passkey's
assertion.

---

## What's actually inside a certificate

An [X.509](../GLOSSARY.md#certificates-mini-ca) certificate is a small signed document:

- **A public key** — the thing being vouched for.
- **A subject** — whose key this is (a distinguished name, plus modern TLS's real check, the
[SAN](../GLOSSARY.md#certificates-mini-ca) list of DNS names/IPs the cert is valid for).
- **A validity window** (`notBefore`/`notAfter`) and a **serial number** (how revocation refers to
it).
- **Extensions** that constrain what the key may be used for — `BasicConstraints` (is this a CA?),
`KeyUsage`, and [EKU](../GLOSSARY.md#certificates-mini-ca) (`clientAuth`+`serverAuth` for an mTLS
leaf).
- **A signature** over all of the above, made by the issuer's private key.

Verifying a certificate is the same offline-signature check you already know from stage 2 — "does
this signature, over these bytes, verify against this public key" — pointed at *a name* instead of
*a claim set*.

---

## The CSR → issue → verify flow

A workload never hands its private key to anyone, including the CA. Instead:

1. **The workload generates its own key pair** and keeps the private key. It builds a
[**CSR**](../GLOSSARY.md#certificates-mini-ca) (Certificate Signing Request, PKCS#10): "here is my
public key, here is the subject/SANs I want, and here is that whole request **self-signed** by my
own private key."
2. **The CA verifies [proof-of-possession](../GLOSSARY.md#certificates-mini-ca) (PoP)** — checking
the CSR's self-signature against the public key *inside* the CSR. This proves the requester
really holds the private key matching the public key it's asking to be certified — without the
CA ever seeing that private key.
3. **The CA issues a leaf**: same subject and public key, plus the mTLS extension set, a fresh
random serial, and a **short TTL** — then signs the whole thing with the CA's own private key.
4. **A verifier checks the chain to root**: does the leaf's signature verify against the CA's public
key, is the CA the verifier's configured trust anchor, is the leaf still inside its validity
window, and (for mTLS) does the presented SAN match who the verifier expected to be talking to?
Any failure means "don't trust this peer" — there is no partial credit.

> **Why proof-of-possession matters.** Without step 2, anyone could submit a CSR containing *your*
> public key and a subject naming *your* service — and get a valid-looking certificate for an
> identity they don't hold the private key for. PoP is what ties "I'm requesting this identity" to
> "I can prove I hold the matching secret," the same shape as a passkey's challenge-response.

---

## Why leaf TTLs are short

A CA can't "un-sign" a certificate — once issued, the signature is valid math forever. Two
mechanisms claw back a compromised or retired certificate, and mini-ca leans on the cheaper one:

- **Revocation** — the CA publishes a list of revoked serials; every verifier must fetch and check
it. This requires a live, reachable, freshness-checked revocation channel (a CRL or OCSP
responder) — real machinery a small homelab CA would rather not build.
- **Short TTLs** — if a leaf expires in hours or days, a leaked key is only useful for that narrow
window, and rotation (get a new leaf before the old one expires) becomes routine rather than an
incident response. **This is the primary control mini-ca relies on.**

> **Honest seam.** mini-ca still publishes a revocation list (`GET /revocations`) as the kill switch
> for the case a short TTL isn't fast enough — but it's a plain JSON list, not a signed DER CRL or an
> OCSP responder. See `services/mini-ca/README.md`'s non-goals: this is a deliberate, documented
> educational simplification, not an oversight.

---

## Where mini-ca fits the family

mini-ca doesn't reinvent crypto to do any of this — BouncyCastle builds and verifies the PKIX
structures, exactly the "vetted library" reflex the rest of the family follows. And mini-ca's own
CA private key is protected the same way the token plane's signing keys are: it's persisted as a
one-record `SigningKeys` document and, with `--kms-*` configured, **envelope-wrapped under
mini-kms** — the same recursive integration `envelope-encryption-and-kms.md` describes, reused
verbatim rather than rebuilt.

---

## Now read it

- **The issuing engine:** `services/mini-ca` → `ca/CertificateAuthority#issueFromCsr` — verifies
proof-of-possession (`csr.isSignatureValid(...)`), builds the extension set (`BasicConstraints`,
`KeyUsage`, `ExtendedKeyUsage` client+server auth, authority/subject key identifiers, SANs), and
signs with a fresh random serial and a short TTL. One `CaIssuanceException` for any malformed or
invalid CSR — no oracle for which check failed.
- **The root and key material:** `ca/CaKeys` — EC P-256 keygen, the self-signed root, and
`randomSerial()`.
- **Hand-rolled PEM:** `ca/Pem` — the encode/decode this family writes itself rather than pull in a
dependency just for base64-with-line-wraps.
- **Bootstrap and lifecycle:** `service/CaService` — mints a fresh CA on first run (else loads it),
issues/renews/revokes, and maintains the issuance log + revocation list; the CA private key flows
through the same `DocumentStore` SPI `KmsSigningKeyStore` implements.

There's no hands-on lab for this stage yet — read the code above with a CSR you generate yourself
(`openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout leaf.key -out
leaf.csr`), `POST /issue` it to mini-ca (admin-guarded; see `services/mini-ca/README.md`), and decode
what comes back (`openssl x509 -in leaf.pem -noout -text`) to see the extension set this doc
described land in a real certificate.
4 changes: 3 additions & 1 deletion docs/concepts/envelope-encryption-and-kms.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,6 @@ the family eating its own dog food — and the capstone that ties envelope encry
the `kms1:` envelope, `rewrap` for rotation).

Lab: [`06-protect-the-signing-keys.md`](../tutorials/06-protect-the-signing-keys.md) — start mini-kms,
run an issuer with `--kms-*`, and confirm only ciphertext touches disk.
run an issuer with `--kms-*`, and confirm only ciphertext touches disk. Then continue to stage 7,
[`certificates-and-mtls.md`](certificates-and-mtls.md) — the CA private key you just saw wrapped is
itself a `SigningKeys` document, reusing this exact integration.
6 changes: 6 additions & 0 deletions docs/design/mini-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ All POST routes carry a hidden `_csrf` field; the handler rejects a missing/mism

## 5. Exercise / test harness (test mode)

**Framing, honestly.** Most of these flows are integration smoke tests a learner *watches* run —
click Run, observe pass/fail/skip — not exercises where a learner makes a choice. The one exception
is `GatewayVerifyFlow`: the operator supplies the path, an access token, and (optionally) a
scope-excluded path, so it's a genuine predict-then-observe device (see the README's harness
section). Don't oversell the rest as "exercises" in the pedagogical sense.

The harness is an **I/O-free engine** (`harness/`) given the wired clients; each flow returns an `ExerciseResult(name, Status, steps, redactedDetail)`. **"Pass" is defined per flow as a cryptographic/state assertion verified offline**, not just an HTTP 200. Results render as green/red rows with per-step detail; **no step ever records a secret** — tokens/secrets are replaced with `«redacted»` and only non-secret facts (kid, sub, exp, serial, status code) are shown.

| Flow | Drives | "Pass" = (verified offline) |
Expand Down
6 changes: 4 additions & 2 deletions docs/diagrams/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
> depends on whom). These are the **sequence** diagrams — *what happens, in what order*, for the
> flows the course teaches. The concept and tutorial docs embed them.

Each diagram is mermaid (GitHub renders it inline). They show the **wired** runtime paths; where a
step is a designed-but-not-wired seam, the diagram says so and links
Each diagram is mermaid (GitHub renders it inline). **Reading source in an editor instead?** Every
diagram file has a one-line note above its code block pointing you to
<https://mermaid.live> — paste the block in to render it. They show the **wired** runtime paths;
where a step is a designed-but-not-wired seam, the diagram says so and links
[`concepts/honest-seams.md`](../concepts/honest-seams.md).

| Diagram | Flow | Used by |
Expand Down
2 changes: 2 additions & 0 deletions docs/diagrams/auth-code-pkce.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
> [`sessions-vs-tokens.md`](../concepts/sessions-vs-tokens.md). Lab:
> [`04`](../tutorials/04-human-sso-end-to-end.md).

> **Reading this in an editor, not GitHub?** Paste the block below into <https://mermaid.live> to render it.

```mermaid
sequenceDiagram
autonumber
Expand Down
2 changes: 2 additions & 0 deletions docs/diagrams/client-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
> [`what-a-token-is.md`](../concepts/what-a-token-is.md) + [`oauth-and-oidc-flows.md`](../concepts/oauth-and-oidc-flows.md).
> Lab: [`02`](../tutorials/02-build-and-verify-a-token-by-hand.md) / [`03`](../tutorials/03-machine-identity-end-to-end.md).

> **Reading this in an editor, not GitHub?** Paste the block below into <https://mermaid.live> to render it.

```mermaid
sequenceDiagram
autonumber
Expand Down
2 changes: 2 additions & 0 deletions docs/diagrams/forward-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
> [`authn-vs-authz.md`](../concepts/authn-vs-authz.md) (authZ at the edge). Lab:
> [`05`](../tutorials/05-gate-a-no-auth-app.md).

> **Reading this in an editor, not GitHub?** Paste the block below into <https://mermaid.live> to render it.

```mermaid
sequenceDiagram
autonumber
Expand Down
2 changes: 2 additions & 0 deletions docs/diagrams/kms-wrap-on-save.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
> [`envelope-encryption-and-kms.md`](../concepts/envelope-encryption-and-kms.md). Lab:
> [`06`](../tutorials/06-protect-the-signing-keys.md).

> **Reading this in an editor, not GitHub?** Paste the block below into <https://mermaid.live> to render it.

```mermaid
sequenceDiagram
autonumber
Expand Down
Loading