Gitea returns HTTP 200 on a failed web login (the page is simply re-rendered with an error), so the existing traefik-auth jail, which matches on DownstreamStatus, is structurally blind to brute force on the login form. Adds a dedicated gitea-auth jail reading the forwarded application log (routed by the new 50-gitea.conf on top of #20's generic receiver), plus a Traefik-side rate limit on /user/login, /user/sign_up and /user/forgot_password as a second layer. [DEFAULT] ignoreip in jail.local guards every jail (not just gitea-auth) so a misconfigured X-Forwarded-For chain can't ban the tailnet itself — this must be in place before the jail, since the whole point is to make that failure mode structurally impossible rather than caught after the fact. conf.d/gitea.yml gains a highest-priority router that denies public access to /metrics (ipAllowList limited to loopback) while leaving it reachable on the tailnet, where Gitea's own tailscale serve answers directly. The backend also moves to https://gitea.taila5ad8.ts.net (443, no port) — the result of #19's tailscale serve --https=443 — which is NOT compatible with the previous :3000 backend of the old community-scripts deployment; only apply once the data migration has actually happened. Verified with real fail2ban/rsyslog/Traefik v3 in disposable containers: the gitea filter matches real failure log lines and misses the normal login-page line; 5 failed logins from an external IP bans it while 5 from the Tailscale CGNAT range never do (ignoreip in effect); a forwarded "gitea"-tagged message lands in /var/log/gitea/gitea.log; Traefik loads all three routers, both middlewares, and the stated priorities without error. Closes #21
Proxy Server
Deploys a secure reverse proxy with Tailscale + Nginx Proxy Manager.
Quick Start
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh | bash
Components
- Tailscale: Private network access (SSH, admin panel)
- Nginx Proxy Manager: Public reverse proxy (HTTP/HTTPS)
- UFW: Firewall (only 80/443 exposed publicly)
- fail2ban + unattended-upgrades: Basic hardening
Environment Variables
| Variable | Default | Description |
|---|---|---|
PROXY_HOSTNAME |
proxy |
Server hostname |
TZ |
Europe/Paris |
Timezone |
Example:
PROXY_HOSTNAME=myproxy TZ=America/New_York curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh | bash
What it does
- Sets hostname
- Installs base packages (vim, fail2ban, unattended-upgrades, at)
- Installs and connects Tailscale (will prompt for authentication)
- Configures sysctl for exit-node capability
- Installs Docker
- Configures UFW (80/443 public, everything else via Tailscale only)
- Deploys Nginx Proxy Manager
- Exposes NPM admin panel via Tailscale serve
- Temporarily opens SSH port 22 for 5 minutes (safety net)
SSH Safety Net
During installation, SSH port 22 is temporarily opened for 5 minutes to prevent lockout if you're connected via public IP. After 5 minutes, it will be automatically closed and only Tailscale SSH will work.
# List scheduled jobs
sudo atq
# Cancel the scheduled SSH closure (replace N with job number)
sudo atrm N
# Manually close SSH port 22 if needed
sudo ufw delete allow 22/tcp
Post-install
- Access NPM admin:
https://proxy.<your-tailnet>.ts.net - Default credentials:
admin@example.com/changeme - Optionally approve exit-node in Tailscale admin console
Centralized log reception (rsyslog)
A fail2ban jail running inside an exposed service's own LXC only ever sees this proxy's tailnet IP as the connection source, so it would end up banning the proxy itself instead of the actual client. Detection has to stay where the signal is (the service's application log); banning has to happen here, where public connections terminate. Services forward their logs to this proxy over TCP so a jail here can act on them.
| File | Purpose |
|---|---|
/etc/rsyslog.d/10-remote-receiver.conf |
Generic imtcp listener, port RSYSLOG_PORT (default 5514). Anything not claimed by a more specific routing file lands in /var/log/remote/<sender-hostname>.log. |
/etc/rsyslog.d/50-<service>.conf |
One per exposed service. Routes by tag/programname into that service's own logfile for its dedicated fail2ban jail. |
/etc/logrotate.d/remote-logs |
Rotation for everything under /var/log/remote/ (copytruncate, so fail2ban never loses its file descriptor across a rotation). |
Adding a new exposed service is a matter of dropping its 50-<service>.conf
here — nothing else in this list needs to change. A minimal example that
routes messages tagged myservice into their own file, in addition to the
generic catch-all:
$RuleSet remoteLogs
if $programname == 'myservice' then {
action(type="omfile" file="/var/log/myservice/myservice.log")
}
$RuleSet RSYSLOG_DefaultRuleset
RSYSLOG_PORT and RSYSLOG_BIND_ADDR (default 0.0.0.0) are overridable
via environment. The default bind is safe as-is: UFW's default-deny only
opens 80/tcp and 443/tcp publicly, so port 5514 is reachable
exclusively over tailscale0 regardless of the bind address. Binding to
the tailnet IP directly was considered and rejected — it would require
tailscale up to have already succeeded before rsyslog is configured,
which complicates the script's flow (a missing TS_AUTHKEY is tolerated
today). The residual risk is log injection from anything that reaches the
port (which can trigger a false fail2ban ban); narrow RSYSLOG_BIND_ADDR
to a specific tailnet IP if that risk becomes a concern.