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:
parent
fecae727d1
commit
14cc1ba4b8
2 changed files with 106 additions and 0 deletions
105
05-troubleshooting/forgejo-mailer-and-cli-recovery.md
Normal file
105
05-troubleshooting/forgejo-mailer-and-cli-recovery.md
Normal 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/)
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue