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.
This commit is contained in:
parent
1f5e5b9a37
commit
67e81d39f3
5 changed files with 366 additions and 12 deletions
246
hosts/web-arm/modules/supabase/FUELTIDE_AUTH_SETUP.md
Normal file
246
hosts/web-arm/modules/supabase/FUELTIDE_AUTH_SETUP.md
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
# 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 1–6 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue