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

8.9 KiB
Raw Blame History

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.

# 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.

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

nix-shell -p sops --run 'sops hosts/mail/secrets.yaml'

Add:

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

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:

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:

GOTRUE_EXTERNAL_APPLE_CLIENT_ID=${APPLE_SERVICES_ID:-}

to something like:

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:

./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:

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 — 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.