wiki: Forgejo account recovery & CLI admin when locked out of the GUI

Covers enabling the [mailer] for password recovery (relay via a tailnet mail
server, no-auth/mynetworks, FORCE_TRUST_SERVER_CERT for IP targets), CLI password
reset + the must-change-password=true gotcha, adding an SSH key via the basic-auth
API when locked out, and ruling out a server-side cause for a 'changing' password.
This commit is contained in:
Marcus Summers 2026-06-12 17:36:54 -04:00
parent fecae727d1
commit 14cc1ba4b8
2 changed files with 106 additions and 0 deletions

View file

@ -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 </dev/null && echo REACHABLE`
## Part 2 — Recover from the CLI (already locked out)
Forgejo's admin CLI runs inside the container as the git user (UID 1000) and needs no login.
**Reset a password:**
```bash
docker exec -u 1000 forgejo forgejo admin user change-password -u <user> -p '<newpass>'
```
> ⚠️ **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 <user>
> ```
**Add an SSH key without the GUI** (basic-auth API — works only if 2FA is off):
```bash
curl -u <user>:'<pass>' -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 <port> 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/)

View file

@ -102,6 +102,7 @@ updated: 2026-05-15T09:00
* [Gemini CLI Manual Update](05-troubleshooting/gemini-cli-manual-update.md) * [Gemini CLI Manual Update](05-troubleshooting/gemini-cli-manual-update.md)
* [MajorWiki Setup & Publishing Pipeline](05-troubleshooting/majwiki-setup-and-pipeline.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) * [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) * [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: 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) * [SELinux: Wrong /etc/localtime Label Silently Breaks Timezone Changes](05-troubleshooting/selinux-localtime-label-breaks-timezone.md)