nixos/hosts/web-arm/modules/supabase/FUELTIDE_AUTH_SETUP.md
Dominik Polakovics 67e81d39f3 feat(supabase): add Google/Apple OAuth and fueltide.io-branded email flows
Enables the auth providers and transactional email flows the self-hosted
Supabase was missing compared to the cloud instance:

- GoTrue now accepts Google and Apple OAuth (web flow); Apple client-secret
  JWT is signed fresh on every activation from the SOPS-stored .p8 so
  there's no 6-month rotation ritual.
- SMTP points at mail.cloonar.com:587 with SASL auth via a new `supabase`
  LDAP account; a `noreply@fueltide.io` mailAlias lets that account send
  as the fueltide.io address.
- rspamd on mail.cloonar.com gets a per-domain DKIM key for fueltide.io
  (selector `default`) so outbound mail is signed.
- MAILER_AUTOCONFIRM is off so signup confirmation + password reset
  actually go through email.
- SITE_URL + URI_ALLOW_LIST point at app.fueltide.io / stage so links in
  emails and OAuth redirects land in the right app.

FUELTIDE_AUTH_SETUP.md documents the manual steps (LDAP entries, SOPS
additions, DNS records, Google/Apple console setup) that must be completed
before merging.
2026-04-22 22:08:29 +02:00

246 lines
8.9 KiB
Markdown
Raw 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.

# Supabase auth setup: Google + Apple OAuth, fueltide.io email
This doc lists the **user-side steps** required to make the code changes in
this branch functional. Nothing here is performed by Nix — these are manual
actions on external services, LDAP, SOPS, and DNS.
The Nix changes in this branch cover:
- `hosts/web-arm/modules/supabase/default.nix` — GoTrue env for Google + Apple
OAuth, SMTP pointed at `mail.cloonar.com:587`, `MAILER_AUTOCONFIRM=false`,
`SITE_URL` + `URI_ALLOW_LIST` for fueltide.io, python+cryptography in the
env-generate path (for Apple JWT signing).
- `hosts/web-arm/modules/supabase/env-generate.sh` — new `auth.env` block that
pulls SMTP + OAuth creds from SOPS and signs the Apple client-secret JWT
fresh on every activation.
- `hosts/mail/modules/dkim-fueltide.nix` — installs a per-domain DKIM key for
fueltide.io into rspamd so outbound mail from `noreply@fueltide.io` is
signed.
Complete the seven steps below **before** merging to master. Merging without
them will deploy a broken GoTrue (missing OAuth/SMTP creds → auth emails fail,
OAuth flows 500).
---
## 1. LDAP service account + fueltide alias on `mail.cloonar.com`
Mirrors the `gitea@cloonar.com` / `authelia@cloonar.com` pattern. The alias
on `noreply@fueltide.io` is what `smtpd_sender_login_maps` uses to let the
`supabase` SASL user send as that address without tripping
`reject_authenticated_sender_login_mismatch`.
```bash
# on mail.cloonar.com
SMTP_PASS=$(openssl rand -base64 30 | tr -d '/+=' | head -c 32)
echo "SMTP_PASS (store this in SOPS, step 3): $SMTP_PASS"
CRYPT=$(mkpasswd -m sha-512 "$SMTP_PASS")
cat > /tmp/supabase.ldif <<EOF
dn: uid=supabase,ou=users,dc=cloonar,dc=com
objectClass: mailAccount
objectClass: inetOrgPerson
uid: supabase
cn: Supabase Auth
sn: Auth
mail: supabase@cloonar.com
mailSendOnly: TRUE
userPassword: {CRYPT}$CRYPT
description: SASL account for Supabase GoTrue outbound mail
dn: mail=noreply@fueltide.io,ou=aliases,dc=cloonar,dc=com
objectClass: mailAlias
mail: noreply@fueltide.io
maildrop: supabase@cloonar.com
EOF
ldapadd -x -D "cn=admin,dc=cloonar,dc=com" -W -f /tmp/supabase.ldif
rm /tmp/supabase.ldif
```
## 2. Generate the fueltide.io DKIM key
Selector is `default` to match the glob that rspamd's `dkim_signing` block
(`hosts/mail/modules/rspamd.nix:15-19`) watches for.
```bash
mkdir -p /tmp/dkim-gen && cd /tmp/dkim-gen
nix-shell -p rspamd --run \
"rspamadm dkim_keygen -s default -d fueltide.io -k fueltide.io.default.key"
# private key: fueltide.io.default.key -> goes into SOPS (step 3)
# public key: printed to stdout -> goes into DNS (step 4)
```
Wipe the temp dir once both are copied out.
## 3. SOPS edits (two files)
### `hosts/mail/secrets.yaml`
```bash
nix-shell -p sops --run 'sops hosts/mail/secrets.yaml'
```
Add:
```yaml
rspamd-dkim-fueltide-io-key: |
-----BEGIN PRIVATE KEY-----
<paste contents of /tmp/dkim-gen/fueltide.io.default.key>
-----END PRIVATE KEY-----
```
### `hosts/web-arm/secrets.yaml`
```bash
nix-shell -p sops --run 'sops hosts/web-arm/secrets.yaml'
```
Inside the existing `supabase-env` multiline value, append eight new lines
(these are sourced as shell variables by `env-generate.sh`):
```
SMTP_USER=supabase@cloonar.com
SMTP_PASS=<plaintext from step 1>
GOOGLE_CLIENT_ID=<from step 5>
GOOGLE_SECRET=<from step 5>
APPLE_TEAM_ID=XWJ4DC7TBH
APPLE_KEY_ID=<from step 6>
APPLE_SERVICES_ID=com.cloonar.supabase.fueltide
APPLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\n<.p8 body>\n-----END PRIVATE KEY-----
```
Note on `APPLE_PRIVATE_KEY`: it must be **one line** with literal backslash-n
separating the PEM lines (no real newlines inside the value). The python
signer in `env-generate.sh` un-escapes those via `decode("unicode_escape")`
before loading the PEM. To format an existing `AuthKey_XXX.p8` as that single
line:
```bash
awk '{printf "%s\\n", $0}' AuthKey_XXXXXXXXXX.p8
```
## 4. DNS records for `fueltide.io`
Add on whichever DNS provider hosts fueltide.io:
```
TXT @ v=spf1 mx a:mail.cloonar.com ~all
TXT default._domainkey v=DKIM1; k=rsa; p=<public key from step 2>
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@cloonar.com; fo=1
```
PTR for mail.cloonar.com is already set (it's been sending for cloonar.com).
If fueltide.io has no MX record, outbound is fine but bounces from remote MTAs
won't route — acceptable for one-way transactional mail. Add an MX pointing at
`mail.cloonar.com.` if you want bounces to be received.
## 5. Google Cloud OAuth client (≈ 5 min)
1. console.cloud.google.com → **APIs & Services → OAuth consent screen**.
External user type. App name `Fueltide`, user support email, developer
contact. Scopes: `openid`, `email`, `profile`. Submit (or keep in testing
if only internal users).
2. **Credentials → Create Credentials → OAuth client ID → Web application**.
Name `Supabase`. Authorised redirect URI:
`https://supabase.cloonar.com/auth/v1/callback`.
3. Copy Client ID + Client Secret → into SOPS as `GOOGLE_CLIENT_ID` and
`GOOGLE_SECRET`.
## 6. Apple Developer Sign in with Apple (≈ 15 min, paid account required)
1. developer.apple.com → **Certificates, IDs & Profiles → Identifiers → +
→ Services IDs**. Description `Fueltide Supabase Auth`. Identifier
`com.cloonar.supabase.fueltide`. Check **Sign in with Apple → Configure**.
2. Primary App ID: existing `io.fueltide.workout` (Team `XWJ4DC7TBH`, see
`hosts/web-arm/sites/fueltide.io.nix`). Domains and Subdomains:
`supabase.cloonar.com`. Return URLs:
`https://supabase.cloonar.com/auth/v1/callback`. Save.
3. **Keys → +** → name `Fueltide Supabase Auth` → check **Sign in with Apple
→ Configure** → primary App ID `io.fueltide.workout`. Register.
4. **Download the `.p8` file now** — Apple only offers it once.
5. Note the Key ID (10 chars) displayed on the key page.
6. Team ID is `XWJ4DC7TBH` (already known).
7. Into SOPS on web-arm:
- `APPLE_TEAM_ID=XWJ4DC7TBH`
- `APPLE_KEY_ID=<from step 5>`
- `APPLE_SERVICES_ID=com.cloonar.supabase.fueltide`
- `APPLE_PRIVATE_KEY=<single-line .p8 as described in step 3>`
### iOS native flow (optional)
If the fueltide iOS app will use `supabase.auth.signInWithIdToken({ provider:
'apple', token: identityToken })` (native `AuthenticationServices` SDK, no web
browser), the iOS bundle ID must also appear in `GOTRUE_EXTERNAL_APPLE_CLIENT_ID`.
Change the line in `env-generate.sh` that currently reads:
```sh
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=${APPLE_SERVICES_ID:-}
```
to something like:
```sh
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=${APPLE_SERVICES_ID:-},io.fueltide.workout
```
(GoTrue accepts a comma-separated audiences list here and validates incoming
id_tokens against any of them.)
## 7. Merge and deploy
Once steps 16 are done:
```bash
./scripts/test-configuration web-arm
./scripts/test-configuration mail
git checkout master
git merge --no-ff <this-branch>
git push
```
Bento rolls out both hosts. On `web-arm.cloonar.com`:
```bash
sudo systemctl restart supabase-env-generate
sudo cat /run/supabase/auth.env # expect 8 new vars populated
sudo podman exec supabase-auth nc -vz mail.cloonar.com 587
sudo podman restart supabase-auth
```
### Verification checklist
- [ ] `/run/supabase/auth.env` contains `GOTRUE_EXTERNAL_APPLE_SECRET=<long-JWT>`.
- [ ] Second `systemctl restart supabase-env-generate` produces a different
Apple JWT (freshness — signed with new `iat`).
- [ ] `curl -X POST -H 'apikey: <anon>' -H 'Content-Type: application/json' \
https://supabase.cloonar.com/auth/v1/signup \
-d '{"email":"<real inbox>","password":"correct horse battery staple"}'`
delivers a mail with `From: noreply@fueltide.io` within ~30 s.
- [ ] Mail headers show `dkim=pass`, `spf=pass`, `dmarc=pass`
(`Authentication-Results` header).
- [ ] `POST /auth/v1/recover` triggers a reset mail.
- [ ] Browser visit to
`https://supabase.cloonar.com/auth/v1/authorize?provider=google`
completes and lands on `/auth/v1/callback`. Row in `auth.identities`
with `provider='google'`.
- [ ] Same with `?provider=apple` from a page Apple's Return URL accepts.
- [ ] Send a signup to [mail-tester.com](https://www.mail-tester.com/) — target
≥ 9/10 spam score.
## Rotation notes
- **Apple client-secret JWT**: auto-regenerated on every activation
(`supabase-env-generate.service`). No manual rotation.
- **Apple `.p8` key**: no expiry, but revoking it in the Apple console
immediately breaks auth. If ever rotated, update `APPLE_KEY_ID` and
`APPLE_PRIVATE_KEY` in SOPS together.
- **Google client secret**: no expiry; rotate via Google Cloud console if
leaked and update `GOOGLE_SECRET` in SOPS.
- **DKIM key**: no expiry, but best practice is to rotate yearly. Rotation
= regenerate keypair (step 2), replace the SOPS value (step 3), update DNS
(step 4), deploy. Keep both old+new DNS records live for 24h during
cutover.
- **SMTP LDAP password**: no expiry. To rotate, run `mkpasswd` again and
update both the LDAP userPassword attribute and SOPS `SMTP_PASS`.