From 035a8a27d90e96ab85ac881ff04f57a1dffd3ab7 Mon Sep 17 00:00:00 2001 From: Bryan Brooks Date: Fri, 13 Feb 2026 10:54:27 -0600 Subject: [PATCH] docs: update deployment docs for DigitalOcean migration Update CLAUDE.md to reflect current DO managed Kubernetes deployment, single atauth namespace, and DO registry. Add DO kustomize overlay with block storage and registry pull secrets. --- CLAUDE.md | 226 ++++++++++++++------- gateway/k8s/overlays/do/kustomization.yaml | 37 ++++ 2 files changed, 188 insertions(+), 75 deletions(-) create mode 100644 gateway/k8s/overlays/do/kustomization.yaml diff --git a/CLAUDE.md b/CLAUDE.md index fef4cc7..998edca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,75 +3,77 @@ ## Overview ATAuth is an OpenID Connect (OIDC) Provider that uses AT Protocol OAuth (Bluesky) as the identity source. It allows applications to authenticate users via their Bluesky accounts. -## Architecture -- **Gateway**: Node.js/Express 5, TypeScript, SQLite -- **Identity Source**: AT Protocol OAuth (bsky.social) -- **Token Format**: ES256 signed JWTs -- **PKCE**: Required for all flows +## Critical: Domain Configuration -## Deployment +**IMPORTANT**: ATAuth staging MUST use `workingtitle.zip` domain, NOT `arcnode.xyz`. -### Staging -- **Public URL**: `https://auth-staging.arcnode.xyz` -- **OIDC Discovery**: `https://auth-staging.arcnode.xyz/.well-known/openid-configuration` -- **Namespace**: `atauth-staging` (k3s) -- **Image**: `10.43.37.39:5000/atauth:staging` +Why: The user's PDS is at `arcnode.xyz`. If ATAuth is also on `arcnode.xyz`, the browser sends `Sec-Fetch-Site: same-site` to the PDS OAuth endpoint, which the PDS rejects. Using a different domain (`workingtitle.zip`) makes it `cross-site`, which is allowed. -### Production -- **Namespace**: `atauth` (k3s) -- **Image**: `10.43.37.39:5000/atauth:latest` +## Architecture -## Secrets Management +``` +OIDC Client (e.g., Audiobookshelf) + ↓ redirects to +ATAuth (auth-staging.workingtitle.zip) - OIDC Provider + ↓ redirects to +User's PDS (arcnode.xyz) - AT Protocol OAuth + ↓ user authenticates, redirects back to +ATAuth (receives AT Proto tokens, issues OIDC tokens) + ↓ redirects back to +OIDC Client (receives OIDC tokens) +``` -### Kubernetes Secrets -Secrets are stored in Kubernetes Secrets and referenced via environment variables: -- `OIDC_KEY_SECRET`: 32-byte encryption key for OIDC signing keys -- `MFA_ENCRYPTION_KEY`: 64-char hex key for TOTP secrets -- `ADMIN_TOKEN`: Admin API authentication token +- **Gateway**: Node.js/Express 5, TypeScript, SQLite +- **Identity Source**: AT Protocol OAuth (user's PDS, e.g., arcnode.xyz or bsky.social) +- **Token Format**: ES256 signed JWTs +- **PKCE**: Required for all flows -### Best Practices -1. **Never commit secrets to git** - Use Kubernetes Secrets or SealedSecrets -2. **Rotate secrets regularly** - Especially OIDC_KEY_SECRET -3. **Use strong entropy** - Generate with `openssl rand -hex 32` -4. **Audit access** - Monitor who accesses secrets +## Deployment -### Secret Generation Commands +### Current Production (DigitalOcean) +- **Public URL**: `https://auth-staging.workingtitle.zip` +- **Apricot Gateway**: `https://apricot.workingtitle.zip` (routes `/auth/*` and `/admin/*` to ATAuth) +- **OIDC Discovery**: `https://auth-staging.workingtitle.zip/.well-known/openid-configuration` +- **Cluster**: DO Managed Kubernetes (`storm-dr-cluster`, nyc1) +- **Namespace**: `atauth` +- **Registry**: `registry.digitalocean.com/ghostmesh-registry` +- **Image**: `registry.digitalocean.com/ghostmesh-registry/atauth:latest` +- **Storage**: SQLite on `do-block-storage` PVC +- **Backups**: Every 6 hours to Backblaze B2 (`age` encrypted, cronjob in `backups` namespace) + +### Deployment Process (Manual) ```bash -# OIDC Key Secret (32 bytes) -openssl rand -hex 32 - -# MFA Encryption Key (32 bytes = 64 hex chars) -openssl rand -hex 32 +# Build and push +docker build --platform linux/amd64 -t registry.digitalocean.com/ghostmesh-registry/atauth:latest . +docker push registry.digitalocean.com/ghostmesh-registry/atauth:latest -# Admin Token -openssl rand -base64 32 +# Deploy +kubectl rollout restart deployment/atauth-gateway -n atauth ``` -## OIDC Client Configuration - -### Registered Clients +### Local k3s (Suspended) +Previously ran on k3s (Proxmox VMs 321/322) with two namespaces: `atauth-staging` (standalone) +and `apricot` (integrated with PDS). Local k3s VMs shut down as of Feb 12, 2026. +Config preserved in `gateway/k8s/overlays/staging/` and `gateway/k8s/overlays/production/`. -#### Audiobookshelf - -- **Client ID**: `audiobookshelf` -- **Redirect URIs**: - - `http://audiobookshelf.cloudforest-basilisk.ts.net:13378/auth/openid/callback` - - `https://audiobookshelf.cloudforest-basilisk.ts.net:13378/auth/openid/callback` -- **Grant Types**: `authorization_code`, `refresh_token` -- **Scopes**: `openid`, `profile`, `email` -- **PKCE**: Required -- **Client Secret**: Stored in Vaultwarden under "ATAuth OIDC Clients" +## Admin Access -### Secrets Storage Location +### Admin Token +The admin token is stored in **Vaultwarden**: +- **URL**: https://vaultwarden.cloudforest-basilisk.ts.net +- **Location**: `ATAuth` folder → `Admin Token - Staging` or `Admin Token - Production` -All OIDC client secrets are stored in **Vaultwarden** (LXC 120 @ vaultwarden.cloudforest-basilisk.ts.net): +### Using Admin API +```bash +# Set token from Vaultwarden +export ADMIN_TOKEN="" -- Folder: `ATAuth/OIDC Clients` -- Entry format: `{client_name} - {environment}` +# List OIDC clients +curl -s "https://auth-staging.workingtitle.zip/admin/oidc/clients" \ + -H "Authorization: Bearer $ADMIN_TOKEN" | jq . -### Registering New Clients -```bash -curl -X POST "https://auth-staging.arcnode.xyz/admin/oidc/clients" \ +# Create/update client +curl -X POST "https://auth-staging.workingtitle.zip/admin/oidc/clients" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -82,22 +84,79 @@ curl -X POST "https://auth-staging.arcnode.xyz/admin/oidc/clients" \ }' ``` +## OIDC Client Configuration + +### Registered Clients + +#### Audiobookshelf + +| Setting | Value | +|---------|-------| +| **Client ID** | `audiobookshelf` | +| **Client Secret** | Vaultwarden → `ATAuth/OIDC Clients/Audiobookshelf` | +| **Redirect URI** | `https://audiobookshelf.cloudforest-basilisk.ts.net/auth/openid/callback` | +| **Grant Types** | `authorization_code`, `refresh_token` | +| **Scopes** | `openid`, `profile`, `email` | +| **PKCE** | Required | + +**Note**: Redirect URI is HTTPS without port (Tailscale serve handles TLS on port 443). + +### Secrets Storage +All secrets in **Vaultwarden** (LXC 120 @ vaultwarden.cloudforest-basilisk.ts.net): +- `ATAuth/Admin Token - Staging` +- `ATAuth/Admin Token - Production` +- `ATAuth/OIDC Clients/Audiobookshelf` +- `ATAuth/OIDC Key Secret` + ## Infrastructure -### Cloudflare Tunnel +### Cloudflare Configuration +- **Account ID**: `abc90e3f264c71019a7f68204a5c4454` - **Tunnel ID**: `600f069c-4c53-4a59-b1fd-57c5ad09c044` -- **Route**: `auth-staging.arcnode.xyz` → `atauth-gateway-staging.atauth-staging.svc.cluster.local:80` +- **Routes**: + - `auth-staging.workingtitle.zip` → `http://atauth-gateway.atauth.svc.cluster.local:80` (DO cluster) + - `apricot.workingtitle.zip` → `http://atauth-gateway.atauth.svc.cluster.local:80` (DO cluster) + - `auth-staging.arcnode.xyz` → (DEPRECATED, causes same-site issues) ### Security (Cloudflare) - **WAF**: Cloudflare Managed Ruleset + OWASP Core (block mode) - **Rate Limiting**: 10 req/min on `/oauth/token`, `/oauth/authorize`, `/auth/*` - **TLS**: Minimum 1.3, Strict mode -- **Exposed Credentials Check**: Enabled -### Tekton Pipeline -- **Pipeline**: `atauth-build` (in `apricot` namespace) -- **Trigger**: Gitea webhook on push to `main` -- **Registry**: `gitea.cloudforest-basilisk.ts.net/arcnode.xyz/atauth-gateway` +### Cluster Resources +```bash +# Check deployment +kubectl -n atauth get pods +kubectl -n atauth logs -l app.kubernetes.io/part-of=atauth + +# View/edit config +kubectl -n atauth get configmap atauth-gateway-config -o yaml + +# Restart after config change +kubectl -n atauth rollout restart deployment atauth-gateway +``` + +## Related Services + +### User's PDS (arcnode.xyz) +- **Server**: Hetzner Cloud (pds-hetzner) +- **Public IP**: 46.224.33.19 +- **Tailscale**: 100.89.0.112 (pds-hetzner) +- **SSH**: `ssh -i ~/.ssh/vaultnode_ed25519 root@100.89.0.112` +- **OAuth Endpoint**: `https://arcnode.xyz/oauth/authorize` +- **DID**: `did:plc:k23ujfuppr3hr4pxvtaz7jro` +- **Handle**: `@bkb.arcnode.xyz` + +### Audiobookshelf +- **Location**: LXC 107 on `pv4` +- **Tailscale IP**: 100.115.188.60 +- **URL**: `https://audiobookshelf.cloudforest-basilisk.ts.net` (via Tailscale serve) +- **Internal**: nginx (8080) → ABS (13378) +- **SSH via pv4**: `ssh root@pv4.cloudforest-basilisk.ts.net "pct exec 107 -- "` + +### Vaultwarden +- **Location**: LXC 120 +- **URL**: https://vaultwarden.cloudforest-basilisk.ts.net ## Testing @@ -110,9 +169,16 @@ npm run test # All tests with watch npm run test:run # All tests once ``` -### Test Coverage -- **Unit Tests**: 65 tests (PKCE, Claims, Tokens services) -- **E2E Tests**: 22 tests (full OIDC flow) +### Manual OIDC Flow Test +```bash +# 1. Check discovery +curl -s "https://auth-staging.workingtitle.zip/.well-known/openid-configuration" | jq . + +# 2. Check JWKS +curl -s "https://auth-staging.workingtitle.zip/.well-known/jwks.json" | jq . + +# 3. Test in browser - go to Audiobookshelf and click "Login with OpenID" +``` ## API Endpoints @@ -126,17 +192,27 @@ npm run test:run # All tests once | `/oauth/userinfo` | User claims | | `/oauth/revoke` | Token revocation | -### Admin Endpoints +### Admin Endpoints (requires Bearer token) | Endpoint | Description | |----------|-------------| -| `/admin/oidc/clients` | CRUD for OIDC clients | -| `/admin/sessions` | Session management | -| `/admin/keys` | Key management | - -## Related Services - -### Audiobookshelf -- **Location**: LXC 107 on `pv4` -- **IP**: 10.50.1.117 / 100.115.188.60 (Tailscale) -- **Port**: 13378 -- **URL**: `http://audiobookshelf.cloudforest-basilisk.ts.net:13378` +| `GET /admin/oidc/clients` | List OIDC clients | +| `POST /admin/oidc/clients` | Create OIDC client | +| `GET /admin/oidc/clients/:id` | Get client details | +| `PUT /admin/oidc/clients/:id` | Update client | +| `DELETE /admin/oidc/clients/:id` | Delete client | +| `GET /admin/sessions` | List sessions | +| `GET /admin/keys` | List signing keys | + +## Troubleshooting + +### "Forbidden sec-fetch-site header same-site" +**Cause**: ATAuth and user's PDS are on the same registrable domain. +**Fix**: Use `workingtitle.zip` for ATAuth, not `arcnode.xyz`. + +### "invalid_redirect_uri" +**Cause**: Redirect URI in request doesn't match registered client. +**Fix**: Check client's registered redirect_uris match exactly (including https vs http, port, path). + +### Token refresh failures / Users logged out +**Cause**: iOS app sends chunked encoding with empty body on refreshSession. +**Fix**: nginx with `proxy_pass_request_body off` for that endpoint (handled on PDS side). diff --git a/gateway/k8s/overlays/do/kustomization.yaml b/gateway/k8s/overlays/do/kustomization.yaml new file mode 100644 index 0000000..b4e2d9c --- /dev/null +++ b/gateway/k8s/overlays/do/kustomization.yaml @@ -0,0 +1,37 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: atauth + +resources: + - ../../base + +images: + - name: atauth-gateway + newName: registry.digitalocean.com/ghostmesh-registry/atauth + newTag: latest + +patches: + # Set storageClass for DO block storage + - target: + kind: PersistentVolumeClaim + name: atauth-gateway-data + patch: | + - op: add + path: /spec/storageClassName + value: do-block-storage + + # Use DO registry pull secret + - target: + kind: Deployment + name: atauth-gateway + patch: | + - op: add + path: /spec/template/spec/imagePullSecrets + value: + - name: registry-ghostmesh-registry + +labels: + - pairs: + environment: digitalocean + includeSelectors: false -- knotmirror 0.0.1