diff --git a/05-troubleshooting/forgejo-mailer-and-cli-recovery.md b/05-troubleshooting/forgejo-mailer-and-cli-recovery.md new file mode 100644 index 0000000..2a645d0 --- /dev/null +++ b/05-troubleshooting/forgejo-mailer-and-cli-recovery.md @@ -0,0 +1,105 @@ +--- +title: "Forgejo: Account Recovery & CLI Admin When Locked Out of the GUI" +domain: troubleshooting +category: general +tags: [forgejo, gitea, smtp, docker, account-recovery, self-hosting] +status: published +created: 2026-06-12 +updated: 2026-06-12 +--- +# Forgejo: Account Recovery & CLI Admin When Locked Out of the GUI + +Two related problems on a single-admin self-hosted **Forgejo** (or Gitea): the GUI *"Forgot password"* is disabled, and you can't log in to fix it. Here's how to (1) enable account recovery properly, and (2) recover from the command line when you're already locked out. + +## Symptoms + +- The *Forgot password* page shows: **"Account recovery is only available when email is set up. Please set up email to enable account recovery."** +- You can't log in (wrong/forgotten password), so you can't add an SSH key or change settings in the GUI either. + +## Part 1 — Enable account recovery (configure the mailer) + +Account recovery needs SMTP. If you already run a mail server on your tailnet, relay through it — **no app password needed** when the Forgejo host is `mynetworks`-trusted by that mail server. + +Edit `app.ini` (in the data volume, e.g. `/data/gitea/conf/app.ini`): + +```ini +[mailer] +ENABLED = true +PROTOCOL = smtp+starttls +SMTP_ADDR = 100.x.y.z ; mail server's tailnet IP +SMTP_PORT = 587 +FROM = forgejo@example.com +FORCE_TRUST_SERVER_CERT = true ; required when connecting by IP (cert CN won't match) +``` + +Notes: + +- `FORCE_TRUST_SERVER_CERT = true` is needed when you target the relay by **IP** — the TLS cert is issued for a hostname, not the IP, so verification would otherwise fail. Acceptable on a trusted internal hop. +- Omit `USER`/`PASSWD` if the relay accepts your host via `mynetworks` (no SASL). Otherwise add SMTP auth. +- `app.ini` lives in the persistent volume, so the change **survives container re-creation** (e.g. Watchtower's nightly pull). + +Apply and verify: + +```bash +docker restart forgejo +docker logs forgejo 2>&1 | grep -i "Mail Service Enabled" # confirms the mailer loaded +``` + +Test the SMTP path **before** trusting it (run from the host, mimicking Forgejo's connection): + +```bash +python3 - <<'EOF' +import smtplib, ssl +ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE +s = smtplib.SMTP("100.x.y.z", 587, timeout=15) +s.ehlo(); s.starttls(context=ctx); s.ehlo() +s.sendmail("forgejo@example.com", ["you@example.com"], + "Subject: test\r\n\r\nForgejo relay path test") +s.quit(); print("SENT_OK") +EOF +``` + +`SENT_OK` means the relay accepted the message. `/user/forgot_password` should now show the reset form instead of the email error. + +> **Container can't reach the tailnet IP?** Docker bridge networks usually route to Tailscale via the host (SNAT to the host's tailnet IP). Confirm with: +> `docker exec forgejo nc -w5 100.x.y.z 587 -p '' +``` + +> ⚠️ **Gotcha:** `change-password` sets `must_change_password=true` by default. That **forces a change on next GUI login _and_ returns HTTP 403 on the API** (`"You must change your password"`). Clear it: +> ```bash +> docker exec -u 1000 forgejo forgejo admin user must-change-password --unset +> ``` + +**Add an SSH key without the GUI** (basic-auth API — works only if 2FA is off): + +```bash +curl -u :'' -X POST -H 'Content-Type: application/json' \ + -d '{"title":"laptop","key":"ssh-ed25519 AAAA... you@host"}' \ + http://localhost:3004/api/v1/user/keys +# HTTP 201 = created +``` + +Forgejo regenerates the git user's `authorized_keys` from the database, so `ssh -p git@host` authenticates immediately afterward — no restart needed. + +## "The password keeps changing" — it (probably) isn't + +If a self-hosted Forgejo admin password *seems* to reset itself, a stock Forgejo container does **not** reset admin passwords. Rule out the server first: + +- the compose has **no** admin/password env and no custom entrypoint; +- **no** cron, systemd timer, or script runs `forgejo admin user change-password`; +- the data volume is persistent (re-creation keeps the DB, password included). + +If all three hold, nothing server-side is changing it — the "changing" password is a **client-side** artifact: a duplicate or stale entry in your password manager autofilling different values. Delete the duplicates and keep one. + +## See also + +- Forgejo — [Config Cheat Sheet → mailer](https://forgejo.org/docs/latest/admin/config-cheat-sheet/) diff --git a/SUMMARY.md b/SUMMARY.md index 68af294..9ec1f25 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -102,6 +102,7 @@ updated: 2026-05-15T09:00 * [Gemini CLI Manual Update](05-troubleshooting/gemini-cli-manual-update.md) * [MajorWiki Setup & Publishing Pipeline](05-troubleshooting/majwiki-setup-and-pipeline.md) * [Gitea Actions Runner: Boot Race Condition Fix](05-troubleshooting/gitea-runner-boot-race-network-target.md) + * [Forgejo: Account Recovery & CLI Admin When Locked Out of the GUI](05-troubleshooting/forgejo-mailer-and-cli-recovery.md) * [Cron Heartbeat False Alarm: /var/run Cleared by Reboot](05-troubleshooting/cron-heartbeat-tmpfs-reboot-false-alarm.md) * [SELinux: Fixing Dovecot Mail Spool Context (/var/vmail)](05-troubleshooting/selinux-dovecot-vmail-context.md) * [SELinux: Wrong /etc/localtime Label Silently Breaks Timezone Changes](05-troubleshooting/selinux-localtime-label-breaks-timezone.md)