diff --git a/docs-source/itpp-infrastructure/CHANGELOG.md b/docs-source/itpp-infrastructure/CHANGELOG.md index b51b5ee..a128c68 100644 --- a/docs-source/itpp-infrastructure/CHANGELOG.md +++ b/docs-source/itpp-infrastructure/CHANGELOG.md @@ -4,3 +4,14 @@ - Created CHANGELOG.md (missing per project documentation standard) - Project directory: `/root/projects/itpp-infrastructure` + +## 2026-08-10 — Docs Auth Gate + +- **Added:** `docs-auth-gate.md` — full documentation of the auth gate for docs.itpropartner.com +- **Components:** + - `/opt/docs-auth/auth-validator.py` on app3 — JWT validator + sign-in handler (port 8099) + - `/etc/systemd/system/docs-auth-validator.service` — systemd unit + - `/etc/nginx/sites-available/docs.itpropartner.com.conf` — nginx config with `auth_request /auth` directive +- **Auth model:** Email allowlist-based access control using auth2 (Stack Auth) JWT tokens +- **Flow:** Unauthenticated → redirect to sign-in page → authenticate against internal project → `stack-access` cookie set → validator checks JWT + email allowlist → access granted +- **Allowed users:** `g@germainebrown.com`, `info@itpropartner.com` diff --git a/docs-source/itpp-infrastructure/docs-auth-gate.md b/docs-source/itpp-infrastructure/docs-auth-gate.md new file mode 100644 index 0000000..7afcdc6 --- /dev/null +++ b/docs-source/itpp-infrastructure/docs-auth-gate.md @@ -0,0 +1,133 @@ +# Docs Auth Gate + +> **Last Updated:** August 10, 2026 +> **Maintainer:** Sho'Nuff +> **Related:** [centralized-auth](/itpp-infrastructure/centralized-auth/) + +--- + +## Overview + +`docs.itpropartner.com` is gated behind authentication using Stack Auth (auth2) JWT session tokens. Unauthenticated visitors are redirected to a sign-in page. Access is controlled by an email address allowlist — only users whose email is in the list can view documentation. + +The gate was implemented on August 10, 2026, replacing the previously open-access docs site. + +## Architecture + +```text +Browser → docs.itpropartner.com (nginx) + → auth_request → http://127.0.0.1:8099/validate + → reads stack-access cookie → validates JWT against auth2 JWKS + → checks email against allowlist + → 200 (granted) or 401 (denied) + → 401 → redirect to /auth/sign-in (sign-in form) + → 200 → serve static docs +``` + +| Component | Location | Description | +|-----------|----------|-------------| +| **Nginx config** | `/etc/nginx/sites-available/docs.itpropartner.com.conf` | CloudPanel vhost with `auth_request` directive | +| **Auth validator** | `/opt/docs-auth/auth-validator.py` | FastAPI-like HTTP server on port 8099 | +| **Systemd service** | `/etc/systemd/system/docs-auth-validator.service` | Runs as `www-data`, auto-starts | +| **Sign-in page** | Served by the validator at `/auth/sign-in` | HTML form that authenticates against auth2 | +| **Stack Auth instance** | `auth2.itpropartner.com` (app3 Docker) | Handles user authentication and JWT issuance | + +## How It Works + +### Authentication Flow + +1. User visits `https://docs.itpropartner.com/` +2. Nginx makes an internal `auth_request` to `http://127.0.0.1:8099/validate` +3. Validator checks for `stack-access` cookie: + - **No cookie** → returns 401 → nginx redirects to `/auth/sign-in` + - **Cookie present** → validates JWT: + - Decodes JWT without verification to read `email` claim + - Checks email against **allowlist** (hardcoded `DOCS_ALLOWED` set) + - Verifies JWT signature against auth2 JWKS (ES256) + - Verifies issuer = `https://auth2-api.itpropartner.com/api/v1/projects/internal` + - Verifies audience = `internal` + - If all checks pass → returns 200 +4. On 200, nginx serves the static docs content + +### Sign-In Flow + +1. User is redirected to `/auth/sign-in?redirect_to=https://docs.itpropartner.com/` +2. Validator serves an HTML sign-in form +3. User enters email + password and submits +4. Validator POSTs credentials to auth2 at `http://127.0.0.1:8102/api/v1/auth/password/sign-in` using the **internal** project publishable key +5. On success, auth2 returns an `access_token` (JWT) +6. Validator sets `stack-access` cookie (HttpOnly, Secure, SameSite=Lax, domain=.itpropartner.com, Max-Age=24h) +7. Validator redirects browser to the original destination + +### Allowlist + +Access is gated by email. The allowlist is in the validator's `DOCS_ALLOWED` set: + +```python +DOCS_ALLOWED: set[str] = { + "g@germainebrown.com", + "info@itpropartner.com", +} +``` + +**To add or remove users**, edit `/opt/docs-auth/auth-validator.py` on app3, update the set, and restart the service: + +```bash +ssh app3 systemctl restart docs-auth-validator +``` + +## Why Not a Separate Stack Auth Project? + +The original plan was to create a separate `docs-itpp` project in Stack Auth and authenticate users specifically against that project. A project was created in the database with its own API keys and branch configuration. + +However, the self-hosted Stack Auth branch system requires internal initialization that doesn't trigger for manually-created projects. The sign-in API returned `BRANCH_DOES_NOT_EXIST` for the `docs-itpp` project despite having all the correct database rows — this is a known limitation of the self-hosted version. + +The email allowlist approach achieves the same result (project = group of members) without requiring multiple Stack Auth projects. + +## Security Model + +- **Cookie:** `stack-access` — HttpOnly (inaccessible to JavaScript), Secure (HTTPS only), SameSite=Lax, domain-scoped to `.itpropartner.com`, 24-hour expiry +- **JWT validation:** Signature verified against auth2 JWKS, expiry checked, issuer and audience validated +- **Allowlist:** Email checked from unverified claims BEFORE signature verification (safe: an attacker who can forge a JWT could also choose their own email) +- **No bypass:** The `/auth/sign-in` location explicitly disables `auth_request` to avoid infinite redirect loops + +## Troubleshooting + +### Check validator status +```bash +ssh app3 systemctl status docs-auth-validator +``` + +### View validator logs +```bash +ssh app3 journalctl -u docs-auth-validator --since "10 min ago" --no-pager +``` + +### Test authentication manually +```bash +# Sign in and get a cookie +curl -s -D - -X POST \ + -d "email=g@germainebrown.com&password=YOURPASSWORD" \ + https://docs.itpropartner.com/auth/sign-in | grep stack-access + +# Test access with the cookie +curl -s -o /dev/null -w "HTTP %{http_code}\n" \ + -H "Cookie: stack-access=YOUR_TOKEN" \ + https://docs.itpropartner.com/ +``` + +### Common errors + +| Error log | Cause | Fix | +|-----------|-------|-----| +| `no_token` | User has no `stack-access` cookie | User hasn't signed in — redirect to sign-in page is expected | +| `Invalid audience` | JWT `aud` claim doesn't match expected value | Check `ALLOWED_PROJECT` matches the token's `aud` | +| `Access denied for X (not in allowlist)` | User signed in but email not in `DOCS_ALLOWED` | Add email to allowlist in validator | +| `Auth API error 400` | Sign-in credentials rejected | User/password wrong or auth2 DB issue | + +## Server + +- **Host:** app3 (152.53.241.111) +- **Validator port:** 8099 (localhost only) +- **Stack Auth API:** http://127.0.0.1:8102 (Docker, localhost only) +- **Validator user:** `www-data` diff --git a/mkdocs.yml b/mkdocs.yml index 0c6ee46..91dc361 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,7 +33,9 @@ markdown_extensions: nav: - Home: index.md - Projects: - - ITPP Infrastructure: itpp-infrastructure/index.md + - ITPP Infrastructure: + - Overview: itpp-infrastructure/index.md + - Docs Auth Gate: itpp-infrastructure/docs-auth-gate.md - ITPP Standards: itpp-standards/index.md - TransitPin: transitpin/index.md - HomeLab: homelab/index.md