infra/docs/runbooks/forgejo-open-signups.md
viktor 78095aa273
All checks were successful
ci/woodpecker/push/default Pipeline was successful
docs(forgejo): runbook reflects Authentik disabled + zero-click GitHub
Authentik OAuth2 source is now disabled (login_source.is_active=0) and GitHub
auto-registration (zero-click sign-up) is on. Document why (global auto-reg +
Authentik's email-as-username 500; Forgejo/Authentik email mismatch blocks
account-linking) and how to re-enable Authentik later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 17:37:46 +00:00

9.4 KiB

Runbook: Forgejo open self-service signups

Last updated: 2026-06-19

forgejo.viktorbarzin.me allows open native self-registration (anyone can create a local Forgejo account from the web form), gated against bots by two layers:

  1. Cloudflare Turnstile captcha on the registration form.
  2. Mandatory email confirmation — a new account stays inactive until the user clicks an activation link emailed to the address they registered with.

Two external login sources also work alongside local accounts: the pre-existing Sign in with GitHub OAuth2 login (the Authentik OAuth2 source is now DISABLED — see the GitHub section below) (see the GitHub section below). Opening local signups was additive — it did not touch SSO.

Most of this is Terraform-managed in stacks/forgejo/. The one exception is the OAuth2 login sources (Authentik, GitHub), which live in Forgejo's own DB and are added via forgejo admin auth — there is no clean Terraform resource for them (their secrets are mirrored to Vault for recovery).

What is configured (and where)

All on the kubernetes_deployment.forgejo container env in stacks/forgejo/main.tf (Forgejo reads app.ini keys from FORGEJO__<section>__<KEY> env vars):

Setting Value Effect
service.DISABLE_REGISTRATION false Registration is enabled
service.ALLOW_ONLY_EXTERNAL_REGISTRATION false Native local sign-up allowed (was true = OAuth-only)
service.ENABLE_CAPTCHA true Captcha required on the signup form
service.CAPTCHA_TYPE cfturnstile Cloudflare Turnstile
service.CF_TURNSTILE_SITEKEY widget id Public; rendered in the page
service.CF_TURNSTILE_SECRET from forgejo-turnstile Secret Server-side verification
service.REGISTER_EMAIL_CONFIRM true Account inactive until email is confirmed
mailer.* see below Sends the activation email
oauth2_client.ENABLE_AUTO_REGISTRATION true First GitHub (OAuth2) sign-in auto-creates the account

Captcha guards registration onlyREQUIRE_CAPTCHA_FOR_LOGIN is left at the default false, so existing users are not captcha'd on every login.

Cloudflare Turnstile widget — turnstile.tf

  • The widget is a Terraform resource: cloudflare_turnstile_widget.forgejo_signup (mode managed, domain forgejo.viktorbarzin.me), created with the CF Global API Key already wired in cloudflare_provider.tf. The account id is resolved via data.cloudflare_accounts.
  • .id is the public sitekey (passed as a plain env value). .secret is the secret key, stored in the forgejo-turnstile K8s Secret and injected via secret_key_ref. The secret also lives in TF state (Tier-1 PG, encrypted at rest) — same trust level as the CF API key already in state.
  • Forgejo is non-proxied (direct A record to Traefik), but Turnstile is a client-side JS widget served from challenges.cloudflare.com, so proxy status is irrelevant — the widget works regardless.

Rotate the widget secret (e.g. if it leaks):

cd stacks/forgejo && vault login -method=oidc
../../scripts/tg apply --non-interactive -replace=cloudflare_turnstile_widget.forgejo_signup

This mints a new sitekey+secret, updates the forgejo-turnstile Secret, and (via the Reloader annotation) rolls the Forgejo pod. Verify the new sitekey appears in the /user/sign_up HTML afterwards.

Mailer — email-secret.tf + [mailer] env

  • Forgejo sends as noreply@viktorbarzin.me via mail.viktorbarzin.me:587 with PROTOCOL=smtp+starttls. This reuses the same mailserver SASL account Authentik uses (stacks/authentik/email-secret.tf) — one credential, one rotation point.
  • The host MUST be mail.viktorbarzin.me, not mailserver.mailserver.svc: the mailserver serves the *.viktorbarzin.me wildcard cert, which does not cover the .svc DNS name, so STARTTLS cert verification would fail. mail.viktorbarzin.me resolves in-cluster (→ 10.0.20.1) and matches the cert.
  • The password is synced from Vault secret/authentiksmtp_password by the forgejo-email ExternalSecret (ESO ClusterSecretStore vault-kv) into the forgejo-email K8s Secret (key PASSWD), referenced by FORGEJO__mailer__PASSWD.
  • The deployment carries reloader.stakater.com/auto: "true", so a rotation of either secret rolls the pod automatically.

GitHub sign-in (OAuth2 source)

People can sign up / sign in with GitHub — the active Forgejo OAuth2 source. GitHub sign-up is zero-click (auto-registration creates the account on first login).

Authentik is DISABLED on purpose (2026-06-19). ENABLE_AUTO_REGISTRATION is GLOBAL across OAuth sources, and Authentik's preferred_username claim is the user's email — invalid as a Forgejo username, which 500'd auto-create. Viktor's Forgejo email (me@viktorbarzin.me) does not match his Authentik email (vbarzin@gmail.com), so account-linking can't bridge it. Per his directive GitHub was prioritised; the Authentik source was deactivated via UPDATE login_source SET is_active=0 WHERE name='Authentik' in the forgejo MySQL DB. Re-enable with is_active=1 after fixing Authentik's username claim.

  • Source (Forgejo DB, not Terraform — added via CLI, same as Authentik):
    forgejo admin auth add-oauth --name github --provider github --key <client-id> --secret <client-secret>
    
    The source name must stay github — it is part of the callback URL (/user/oauth2/github/callback) registered on the GitHub side, so renaming it breaks the callback. forgejo admin auth list shows it (ID 2).
  • GitHub OAuth App: a classic OAuth App under the ViktorBarzin GitHub account (Settings → Developer settings → OAuth Apps). Homepage https://forgejo.viktorbarzin.me, callback https://forgejo.viktorbarzin.me/user/oauth2/github/callback. GitHub has no API to create OAuth Apps — creating it is a browser-only step.
  • Credentials: Vault secret/viktorforgejo_github_oauth_client_id / forgejo_github_oauth_client_secret (kept for recovery; the live values are in Forgejo's DB).
  • Auto-registration: FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION=true (main.tf) makes a first GitHub login create the account directly. The GitHub identity is the trust gate for this path — the Turnstile captcha + email confirmation only apply to the native signup form, not OAuth.

Rotate the GitHub client secret — generate a new one in the GitHub OAuth App, then:

vault kv patch secret/viktor forgejo_github_oauth_client_secret=<new>
POD=$(kubectl -n forgejo get pod -l app=forgejo -o jsonpath='{.items[0].metadata.name}')
kubectl -n forgejo exec "$POD" -- su-exec git forgejo admin auth update-oauth --id 2 --secret <new>

(Source id from forgejo admin auth list.)

Recreate after a Forgejo DB loss: the source is not in Terraform, so after a from-scratch restore, re-run the add-oauth command above with the Vault creds.

Re-closing / tightening signups

Edit stacks/forgejo/main.tf and scripts/tg apply (or commit + push — CI applies):

  • OAuth-only again (revert this change): set FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION back to "true".
  • No new accounts at all (admins create them): set FORGEJO__service__DISABLE_REGISTRATION to "true".
  • Require admin approval per signup (strongest, instead of email confirm): set REGISTER_MANUAL_CONFIRM=true and REGISTER_EMAIL_CONFIRM=false (Forgejo makes the two mutually exclusive). New accounts then queue under Site Administration → Identity & Access → Accounts until an admin activates them.

Handling spam / abuse accounts

A signup that clears Turnstile + email confirmation is still a real, low-privilege Forgejo user. To deal with abuse:

  • Ban/delete via Site Administration → Identity & Access → Accounts, or forgejo admin user delete --username <name> inside the pod (kubectl -n forgejo exec deploy/forgejo -- forgejo admin user ...).
  • New users get Forgejo defaults (they can create repos/orgs). If abuse warrants, tighten with [service].DEFAULT_ALLOW_CREATE_ORGANIZATION=false and/or [repository].MAX_CREATION_LIMIT (add as env vars; out of scope for the initial open-signups change).

Operational notes

  • The Forgejo deployment is single-replica with Recreate strategy, so any config apply briefly restarts the pod (git remote + OCI registry unavailable for a few seconds). Expected, not an incident.
  • The signup page is not behind Cloudflare's bot-fight (Forgejo is non-proxied) — Turnstile + email confirmation are the bot gate. CrowdSec + Traefik rate limiting still front the host.

Verify it's working

POD=$(kubectl -n forgejo get pod -l app=forgejo -o jsonpath='{.items[0].metadata.name}')
# Env present:
kubectl -n forgejo exec "$POD" -- env | grep -E 'ALLOW_ONLY_EXTERNAL|ENABLE_CAPTCHA|CAPTCHA_TYPE|CF_TURNSTILE_SITEKEY|REGISTER_EMAIL_CONFIRM|mailer__ENABLED'
# Turnstile widget rendered on the form:
kubectl -n forgejo exec "$POD" -- wget -qO- http://localhost:3000/user/sign_up | grep -oE 'cf-turnstile|data-sitekey="[^"]*"'
# Secrets healthy:
kubectl -n forgejo get externalsecret forgejo-email
kubectl -n forgejo get secret forgejo-email forgejo-turnstile

A full real-world check is to register a throwaway account and confirm the activation email arrives. The mailer transport (server/port/cert/cred) is shared with Authentik, which is already in production for external user enrollment.