Docs: translate README and guides to German

README and all guides under docs/ are now in German, with German
anchors for the cross references. Code and configuration blocks are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schiwagoaandClaude Opus 5.5 committed 2026-10-25 15:41:59 +01:00
1 parent 2e3f79236c
commit 019c74a418
8 files changed
+469 -463

No files matched your search

+40 -40
View File
@@ -1,34 +1,34 @@
# fail2ban-webui
A small, self-hosted web interface to **view, ban and unban fail2ban IPs on several hosts** –
bare metal, VMs, LXC containers or Docker hosts.
Eine schlanke, selbst gehostete Weboberfläche, um **fail2ban-Sperren auf mehreren Hosts
anzuzeigen, zu setzen und aufzuheben** – Bare Metal, VMs, LXC-Container oder Docker-Hosts.
- One page for all hosts: current bans per jail, origin (GeoIP) with a world map, search and filters
- Ban an IP permanently on any selection of hosts, unban per jail or everywhere
- Audit log of every action (who, when, from where)
- Optional: log history per IP from **Loki**, links into **Grafana**
- Sign-in with a local user, **OpenID Connect** (e.g. Authentik) or your reverse proxy
- English and German UI, more languages via one JSON file
- Runs as a hardened container (read-only, no capabilities, non-root), `amd64` and `arm64`
- Eine Seite für alle Hosts: aktuelle Sperren pro Jail, Herkunft (GeoIP) mit Weltkarte, Suche und Filter
- IP dauerhaft auf beliebigen Hosts sperren, pro Jail oder überall entsperren
- Protokoll jeder Aktion (wer, wann, von wo)
- Optional: Log-Verlauf pro IP aus **Loki**, Links nach **Grafana**
- Anmeldung mit lokalem Benutzer, **OpenID Connect** (z. B. Authentik) oder über den Reverse Proxy
- Oberfläche auf Englisch und Deutsch, weitere Sprachen über eine JSON-Datei
- Läuft als gehärteter Container (schreibgeschützt, ohne Capabilities, nicht als root), `amd64` und `arm64`
```
Browser ──HTTPS──▶ reverse proxy ──▶ fail2ban-webui (Docker)
│ SSH, one restricted key
Browser ──HTTPS──▶ Reverse Proxy ──▶ fail2ban-webui (Docker)
│ SSH, ein eingeschränkter Schlüssel
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
host A (bare) LXC container Docker host
Host A (Bare Metal) LXC-Container Docker-Host
f2b-webctl f2b-webctl f2b-webctl
fail2ban fail2ban fail2ban
```
The web UI never gets a shell on your hosts. Every host runs a tiny agent,
[`f2b-webctl`](agent/f2b-webctl), as SSH *forced command*. It only knows
`list`, `ban <ip>`, `unban <jail> <ip>`, `unban-all <ip>` and `version`, refuses to ban
private networks, the host's own addresses or anything on its protected list, and logs
every action to syslog. Access to the fail2ban socket would be equivalent to root –
the agent is the security boundary, not the web UI.
Die Weboberfläche bekommt nie eine Shell auf deinen Hosts. Auf jedem Host läuft ein kleiner
Agent, [`f2b-webctl`](agent/f2b-webctl), als SSH-*Forced-Command*. Er kennt nur
`list`, `ban <ip>`, `unban <jail> <ip>`, `unban-all <ip>` und `version`, sperrt niemals
private Netze, die eigenen Adressen des Hosts oder Einträge seiner Schutzliste und
protokolliert jede Aktion im Syslog. Zugriff auf den fail2ban-Socket wäre gleichbedeutend
mit root – die Sicherheitsgrenze ist der Agent, nicht die Weboberfläche.
## Quick start
## Schnellstart
```bash
mkdir -p /opt/fail2ban-webui/{config,data} && cd /opt/fail2ban-webui
@@ -37,40 +37,40 @@ curl -fsSLo .env https://git.retronerd.at/schiwagoa/fail2ban-webui
curl -fsSLo config/hosts.yaml https://git.retronerd.at/schiwagoa/fail2ban-webui/raw/branch/main/examples/hosts.yaml.example
chown -R 10050:10050 data
docker compose run --rm -it fail2ban-webui hash-password # paste the line into .env
docker compose run --rm fail2ban-webui keygen # public key for the agents
# on every fail2ban host: install the agent (docs/agent.md), then:
docker compose run --rm -it fail2ban-webui hash-password # Zeile in die .env übernehmen
docker compose run --rm fail2ban-webui keygen # öffentlicher Schlüssel für die Agenten
# auf jedem fail2ban-Host den Agenten installieren (docs/agent.md), danach:
docker compose run --rm -it fail2ban-webui keyscan
docker compose run --rm fail2ban-webui check
docker compose up -d
```
The UI listens on `127.0.0.1:8080` – put a reverse proxy with TLS in front of it.
Die Oberfläche lauscht auf `127.0.0.1:8080` – davor gehört ein Reverse Proxy mit TLS.
## Documentation
## Dokumentation
| | |
|---|---|
| [Installation](docs/installation.md) | web UI step by step, from `mkdir` to the first login |
| [Agent](docs/agent.md) | installing `f2b-webctl` on bare metal, LXC and Docker hosts |
| [Configuration](docs/configuration.md) | every setting in `.env` and `hosts.yaml` |
| [Authentication](docs/authentication.md) | local login, OpenID Connect (Authentik), reverse-proxy auth |
| [Reverse proxy](docs/reverse-proxy.md) | Nginx Proxy Manager, Caddy, Traefik, nginx |
| [Operations](docs/operations.md) | logs, updates, backups, troubleshooting |
| [Development](docs/development.md) | local testbed, tests, adding a language |
| [Security](SECURITY.md) | security model and reporting vulnerabilities |
| [Installation](docs/installation.md) | Weboberfläche Schritt für Schritt, von `mkdir` bis zur ersten Anmeldung |
| [Agent](docs/agent.md) | `f2b-webctl` auf Bare Metal, LXC und Docker-Hosts installieren |
| [Konfiguration](docs/configuration.md) | alle Einstellungen in `.env` und `hosts.yaml` |
| [Anmeldung](docs/authentication.md) | lokaler Login, OpenID Connect (Authentik), Anmeldung über den Reverse Proxy |
| [Reverse Proxy](docs/reverse-proxy.md) | Nginx Proxy Manager, Caddy, Traefik, nginx |
| [Betrieb](docs/operations.md) | Logs, Updates, Backups, Fehlersuche |
| [Entwicklung](docs/development.md) | lokales Testbed, Tests, neue Sprache hinzufügen |
| [Sicherheit](SECURITY.md) | Sicherheitsmodell und Melden von Schwachstellen |
## Requirements
## Voraussetzungen
- Web UI: Docker with Compose v2, `amd64` or `arm64`
- Every fail2ban host: fail2ban ≥ 0.11 (start/end times need `--with-time`), `python3`,
`sshd`, `sudo` (or a root key), `bash`
- Weboberfläche: Docker mit Compose v2, `amd64` oder `arm64`
- Jeder fail2ban-Host: fail2ban ≥ 0.11 (Start-/Endzeiten brauchen `--with-time`), `python3`,
`sshd`, `sudo` (oder ein root-Schlüssel), `bash`
## License
## Lizenz
[MIT](LICENSE). IP geolocation by [DB-IP Lite](https://db-ip.com) (CC BY 4.0), downloaded
only when `GEOIP_AUTO_DOWNLOAD=1`. World map from [Natural Earth](https://www.naturalearthdata.com)
(public domain), bundled with the app.
[MIT](LICENSE). IP-Geolokalisierung durch [DB-IP Lite](https://db-ip.com) (CC BY 4.0), wird
nur bei `GEOIP_AUTO_DOWNLOAD=1` heruntergeladen. Weltkarte von [Natural Earth](https://www.naturalearthdata.com)
(Public Domain), in der App enthalten.
---
+104 -103
View File
@@ -1,61 +1,61 @@
# Agent – f2b-webctl
Every fail2ban host runs the agent [`f2b-webctl`](../agent/f2b-webctl): a small Python
script (standard library only) started as SSH **forced command** of the web UI's key.
The web UI cannot run anything else on the host.
Auf jedem fail2ban-Host läuft der Agent [`f2b-webctl`](../agent/f2b-webctl): ein kleines
Python-Skript (nur Standardbibliothek), das als SSH-**Forced-Command** für den Schlüssel der
Weboberfläche gestartet wird. Etwas anderes kann die Weboberfläche auf dem Host nicht ausführen.
| Command (sent by the web UI) | Effect |
| Befehl (von der Weboberfläche) | Wirkung |
|---|---|
| `list` | all jails with their banned IPs (JSON) |
| `ban <ip>` | ban permanently in the manual jail (`manual-block`) |
| `unban <jail> <ip>` | unban in one jail |
| `unban-all <ip>` | unban in all jails |
| `version` | agent version |
| `list` | alle Jails mit ihren gesperrten IPs (JSON) |
| `ban <ip>` | dauerhaft im manuellen Jail (`manual-block`) sperren |
| `unban <jail> <ip>` | in einem Jail entsperren |
| `unban-all <ip>` | in allen Jails entsperren |
| `version` | Agent-Version |
Never banned, whatever the web UI sends:
Wird nie gesperrt, egal was die Weboberfläche schickt:
- private, loopback, link-local, CGNAT, multicast and reserved ranges
- private, Loopback-, Link-Local-, CGNAT-, Multicast- und reservierte Bereiche
(`10/8`, `172.16/12`, `192.168/16`, `100.64/10`, `127/8`, `169.254/16`, `fc00::/7`, `fe80::/10`, …)
- all addresses of the host itself and the address the web UI connects from
- everything listed in `PROTECTED` in `/etc/f2b-webctl.conf` (e.g. your home IP)
- alle Adressen des Hosts selbst und die Adresse, von der die Weboberfläche zugreift
- alles, was unter `PROTECTED` in `/etc/f2b-webctl.conf` steht (z. B. deine Heim-IP)
Every action is logged to syslog (facility `auth`, tag `f2b-webctl`), including rejected ones.
Jede Aktion landet im Syslog (Facility `auth`, Tag `f2b-webctl`), auch abgelehnte.
## Requirements
## Voraussetzungen
| | |
|---|---|
| fail2ban | ≥ 0.11, running (`fail2ban-client ping`) – older versions without `--with-time` work, but without start/end times |
| fail2ban | ≥ 0.11, laufend (`fail2ban-client ping`) – ältere Versionen ohne `--with-time` funktionieren, aber ohne Start-/Endzeiten |
| python3 | ≥ 3.8 |
| sshd | reachable from the web UI host |
| sudo | for the default user `f2bweb` (`apt install sudo`); not needed with `--user root` |
| bash, ssh-keygen | for the installer |
| sshd | vom Webui-Host erreichbar |
| sudo | für den Standardbenutzer `f2bweb` (`apt install sudo`); mit `--user root` nicht nötig |
| bash, ssh-keygen | für den Installer |
Works on Debian, Ubuntu, Proxmox VE and other systemd or non-systemd Linux systems –
bare metal, VMs, LXC containers (privileged or unprivileged) and Docker hosts.
Läuft auf Debian, Ubuntu, Proxmox VE und anderen Linux-Systemen mit oder ohne systemd –
Bare Metal, VMs, LXC-Container (privilegiert oder unprivilegiert) und Docker-Hosts.
## Installation
You need the public key printed by `fail2ban-webui keygen` and the address the web UI
connects from (the public IP of the Docker host, or its LAN address).
Gebraucht werden der öffentliche Schlüssel aus `fail2ban-webui keygen` und die Adresse, von
der die Weboberfläche zugreift (öffentliche IP des Docker-Hosts oder seine LAN-Adresse).
```bash
# as root on the fail2ban host
# als root auf dem fail2ban-Host
git clone https://git.retronerd.at/schiwagoa/fail2ban-webui.git /tmp/fail2ban-webui
cd /tmp/fail2ban-webui/agent
echo 'ssh-ed25519 AAAA… fail2ban-webui' > /tmp/webui.pub
# see what would happen
# anzeigen, was passieren würde
bash install.sh --key-file /tmp/webui.pub --from 198.51.100.10 --dry-run
# install
# installieren
bash install.sh --key-file /tmp/webui.pub --from 198.51.100.10
```
Without `git`: download the three files `f2b-webctl`, `install.sh` and `uninstall.sh` from
the `agent/` directory into one directory and run `install.sh` from there.
Ohne `git`: die drei Dateien `f2b-webctl`, `install.sh` und `uninstall.sh` aus dem Ordner
`agent/` in ein Verzeichnis laden und `install.sh` von dort starten.
The installer shows a summary and asks before it changes anything:
Der Installer zeigt eine Zusammenfassung und fragt, bevor er etwas ändert:
```
fail2ban-webui agent 1.0.0 – fail2ban 1.1.0 – web01
@@ -71,50 +71,51 @@ fail2ban-webui agent 1.0.0 – fail2ban 1.1.0 – web01
backups /var/backups/fail2ban-webui-agent
```
What it does:
Was er tut:
1. backs up every file it is going to change to `--backup-dir`
2. installs `/usr/local/sbin/f2b-webctl` (`root:root 0750`)
3. writes `/etc/f2b-webctl.conf` (kept if it already exists)
4. creates the system user `f2bweb` (home `/var/lib/f2bweb`, no password) and the sudo rule
1. sichert jede Datei, die er ändert, nach `--backup-dir`
2. installiert `/usr/local/sbin/f2b-webctl` (`root:root 0750`)
3. schreibt `/etc/f2b-webctl.conf` (bleibt erhalten, wenn sie schon existiert)
4. legt den Systembenutzer `f2bweb` an (Home `/var/lib/f2bweb`, ohne Passwort) und die sudo-Regel
```
Defaults!/usr/local/sbin/f2b-webctl env_keep += "SSH_ORIGINAL_COMMAND SSH_CLIENT"
f2bweb ALL=(root) NOPASSWD: /usr/local/sbin/f2b-webctl
```
5. adds the key with all restrictions:
5. trägt den Schlüssel mit allen Einschränkungen ein:
```
restrict,from="198.51.100.10",command="sudo -n /usr/local/sbin/f2b-webctl" ssh-ed25519 AAAA… fail2ban-webui
```
6. creates the jail `manual-block` if it does not exist (see below)
7. runs a self test through the same sudo path the web UI uses
6. legt das Jail `manual-block` an, falls es noch nicht existiert (siehe unten)
7. führt einen Selbsttest über denselben sudo-Weg aus, den die Weboberfläche nutzt
Running the installer again is safe: it updates the script, replaces the key entry and
keeps your configuration.
Den Installer erneut auszuführen ist unbedenklich: Er aktualisiert das Skript, ersetzt den
Schlüsseleintrag und lässt deine Konfiguration unverändert.
### Options
### Optionen
| Option | Default | Meaning |
| Option | Standard | Bedeutung |
|---|---|---|
| `--key-file FILE` / `--key "KEY"` | – | public key of the web UI (required) |
| `--from ADDRESSES` | any | only accept the key from these IPs/networks (comma separated). **Strongly recommended.** |
| `--user NAME` | `f2bweb` | SSH user; `root` works without sudo |
| `--authorized-keys-file PATH` | `~USER/.ssh/authorized_keys` | where the key entry goes – see [managed keys](#hosts-with-managed-authorized_keys) |
| `--manual-jail NAME` | `manual-block` | jail for manual bans |
| `--banaction ACTION` | from `jail.local` | banaction of a newly created manual jail |
| `--protect NETWORKS` | – | additional protected addresses (only when the config file is written) |
| `--no-jail` | – | do not create the manual jail |
| `--force-config` | – | overwrite an existing `/etc/f2b-webctl.conf` |
| `--backup-dir DIR` | `/var/backups/fail2ban-webui-agent` | backups of changed files |
| `--dry-run` | – | only show what would be done |
| `-y`, `--yes` | – | do not ask |
| `--key-file DATEI` / `--key "SCHLÜSSEL"` | – | öffentlicher Schlüssel der Weboberfläche (Pflicht) |
| `--from ADRESSEN` | jede | Schlüssel nur von diesen IPs/Netzen akzeptieren (durch Komma getrennt). **Dringend empfohlen.** |
| `--user NAME` | `f2bweb` | SSH-Benutzer; `root` funktioniert ohne sudo |
| `--authorized-keys-file PFAD` | `~USER/.ssh/authorized_keys` | wohin der Schlüsseleintrag kommt – siehe [verwaltete Schlüssel](#hosts-mit-verwalteten-authorized_keys) |
| `--manual-jail NAME` | `manual-block` | Jail für manuelle Sperren |
| `--banaction AKTION` | aus `jail.local` | Banaction für ein neu angelegtes manuelles Jail |
| `--protect NETZE` | – | zusätzliche geschützte Adressen (nur wenn die Konfigurationsdatei geschrieben wird) |
| `--no-jail` | – | kein manuelles Jail anlegen |
| `--force-config` | – | vorhandene `/etc/f2b-webctl.conf` überschreiben |
| `--backup-dir VERZEICHNIS` | `/var/backups/fail2ban-webui-agent` | Sicherungen geänderter Dateien |
| `--dry-run` | – | nur anzeigen, was passieren würde |
| `-y`, `--yes` | – | nicht nachfragen |
The installer warns when sshd would not read the chosen key file (`AuthorizedKeysFile`),
when `AllowUsers`/`AllowGroups` are set, or when `--user root` meets `PermitRootLogin no`.
Der Installer warnt, wenn sshd die gewählte Schlüsseldatei gar nicht liest
(`AuthorizedKeysFile`), wenn `AllowUsers`/`AllowGroups` gesetzt sind oder wenn `--user root`
auf `PermitRootLogin no` trifft.
## The manual jail
## Das manuelle Jail
Bans from the web UI go into one jail, `manual-block` by default, with `bantime = -1`
(permanent). If the jail does not exist, the installer creates:
Sperren aus der Weboberfläche landen in einem Jail, standardmäßig `manual-block`, mit
`bantime = -1` (dauerhaft). Gibt es das Jail noch nicht, legt der Installer an:
`/etc/fail2ban/filter.d/manual-block.conf`
@@ -139,64 +140,64 @@ maxretry = 1
bantime = -1
```
The jail uses your default `banaction` from `jail.local`. Bans survive restarts through
the fail2ban database.
Das Jail nutzt deine Standard-`banaction` aus `jail.local`. Sperren überstehen Neustarts
über die fail2ban-Datenbank.
An existing jail with that name is never touched – if you already have a jail for manual
bans, pass its name with `--manual-jail`.
Ein vorhandenes Jail mit diesem Namen wird nie angefasst – gibt es schon ein Jail für
manuelle Sperren, dessen Namen mit `--manual-jail` übergeben.
### Docker hosts
### Docker-Hosts
fail2ban's default actions block in the `INPUT` chain. Traffic to **published Docker
ports** does not pass `INPUT` but `FORWARD` → `DOCKER-USER`, so those bans would not
affect your containers. For a Docker host, create the manual jail with an action that
uses the `DOCKER-USER` chain, e.g. with iptables:
Die Standardaktionen von fail2ban sperren in der Kette `INPUT`. Verkehr zu **veröffentlichten
Docker-Ports** läuft aber nicht über `INPUT`, sondern über `FORWARD` → `DOCKER-USER`; solche
Sperren würden deine Container also nicht schützen. Auf einem Docker-Host das manuelle Jail
deshalb mit einer Aktion anlegen, die die Kette `DOCKER-USER` nutzt, z. B. mit iptables:
```bash
bash install.sh --key-file /tmp/webui.pub --from 198.51.100.10 \
--banaction "iptables-allports[chain=DOCKER-USER]"
```
Check the result after the first ban:
Nach der ersten Sperre prüfen:
```bash
iptables -S DOCKER-USER | head
```
If the web UI runs on the same Docker host, use `address: host.docker.internal` in
`hosts.yaml` and allow SSH from the Docker network if a firewall (e.g. UFW) blocks it:
Läuft die Weboberfläche auf demselben Docker-Host, in `hosts.yaml` `address: host.docker.internal`
verwenden und SSH aus dem Docker-Netz erlauben, falls eine Firewall (z. B. UFW) es blockiert:
```bash
ufw allow from 172.23.51.0/24 to any port 22 proto tcp comment 'fail2ban-webui'
```
and install the agent with `--from 172.23.51.10`.
und den Agenten mit `--from 172.23.51.10` installieren.
### LXC containers
### LXC-Container
The agent runs inside the container where fail2ban runs – nothing special is needed.
In **unprivileged** containers fail2ban itself must be able to change the container's
firewall (nftables/iptables inside the CT); test with a manual ban:
Der Agent läuft im Container, in dem fail2ban läuft – sonst ist nichts Besonderes nötig.
In **unprivilegierten** Containern muss fail2ban selbst die Firewall des Containers ändern
können (nftables/iptables im CT); mit einer manuellen Sperre testen:
```bash
fail2ban-client set manual-block banip 203.0.113.1 && nft list ruleset | grep 203.0.113.1
fail2ban-client set manual-block unbanip 203.0.113.1
```
### Hosts with managed authorized_keys
### Hosts mit verwalteten authorized_keys
Some tools (Ansible, Keywarden, cloud-init, …) overwrite `~/.ssh/authorized_keys`.
Put the key into a file they do not manage and make sure sshd reads it:
Manche Werkzeuge (Ansible, Keywarden, cloud-init, …) überschreiben `~/.ssh/authorized_keys`.
Den Schlüssel dann in eine Datei legen, die sie nicht verwalten, und sicherstellen, dass sshd sie liest:
```bash
grep -i '^AuthorizedKeysFile' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf
# e.g. AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
# z. B. AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
bash install.sh --key-file /tmp/webui.pub --from 198.51.100.10 \
--user root --authorized-keys-file /root/.ssh/authorized_keys2
```
## Configuration – /etc/f2b-webctl.conf
## Konfiguration – /etc/f2b-webctl.conf
```ini
# fail2ban-webui agent (f2b-webctl) – settings
@@ -210,60 +211,60 @@ PROTECT_OWN_ADDRESSES=yes
PROTECT_CLIENT=yes
```
Changes take effect immediately – the agent reads the file on every call.
Änderungen wirken sofort – der Agent liest die Datei bei jedem Aufruf.
## Testing on the host
## Test auf dem Host
```bash
# as root, directly
# direkt als root
f2b-webctl list | python3 -m json.tool | head -30
f2b-webctl ban 10.0.0.1 # → rejected, protected
f2b-webctl ban 10.0.0.1 # → abgelehnt, geschützt
# the same path as the web UI (user f2bweb through sudo)
# derselbe Weg wie die Weboberfläche (Benutzer f2bweb über sudo)
runuser -u f2bweb -- env SSH_ORIGINAL_COMMAND=version sudo -n /usr/local/sbin/f2b-webctl
# what the agent did
journalctl -t f2b-webctl --since today # or: grep f2b-webctl /var/log/auth.log
# was der Agent getan hat
journalctl -t f2b-webctl --since today # oder: grep f2b-webctl /var/log/auth.log
```
## Update
## Aktualisieren
Run the installer of the new version again with the same options. Your configuration
and the jail are kept; the script and the key entry are replaced.
Den Installer der neuen Version mit denselben Optionen erneut ausführen. Konfiguration und
Jail bleiben erhalten; Skript und Schlüsseleintrag werden ersetzt.
## Uninstall
## Deinstallieren
```bash
bash uninstall.sh --dry-run
bash uninstall.sh
```
| Option | Meaning |
| Option | Bedeutung |
|---|---|
| `--user NAME` | agent user (default `f2bweb`) |
| `--authorized-keys-file PATH` | file with the key entry |
| `--remove-user` | also delete the user and its home |
| `--remove-jail` | also delete the manual jail – **lifts all manual bans** (they are listed first) |
| `--backup-dir DIR`, `--dry-run`, `--yes` | as for the installer |
| `--user NAME` | Agent-Benutzer (Standard `f2bweb`) |
| `--authorized-keys-file PFAD` | Datei mit dem Schlüsseleintrag |
| `--remove-user` | auch den Benutzer samt Home löschen |
| `--remove-jail` | auch das manuelle Jail löschen – **hebt alle manuellen Sperren auf** (sie werden vorher aufgelistet) |
| `--backup-dir VERZEICHNIS`, `--dry-run`, `--yes` | wie beim Installer |
## Manual installation
## Manuelle Installation
If you prefer to do it by hand, these are all the steps the installer performs:
Wer es lieber von Hand macht – das sind alle Schritte des Installers:
```bash
install -o root -g root -m 0750 f2b-webctl /usr/local/sbin/f2b-webctl
nano /etc/f2b-webctl.conf # content see above
nano /etc/f2b-webctl.conf # Inhalt siehe oben
useradd --system --create-home --home-dir /var/lib/f2bweb --shell /bin/sh f2bweb
usermod -p '*' f2bweb
visudo -f /etc/sudoers.d/f2b-webctl # content see "What it does", step 4
visudo -f /etc/sudoers.d/f2b-webctl # Inhalt siehe "Was er tut", Schritt 4
install -d -o f2bweb -g f2bweb -m 700 /var/lib/f2bweb/.ssh
nano /var/lib/f2bweb/.ssh/authorized_keys # line see step 5
nano /var/lib/f2bweb/.ssh/authorized_keys # Zeile siehe Schritt 5
chown f2bweb: /var/lib/f2bweb/.ssh/authorized_keys
chmod 600 /var/lib/f2bweb/.ssh/authorized_keys
# manual jail: the two files from "The manual jail", then
# manuelles Jail: die zwei Dateien aus "Das manuelle Jail", dann
touch /var/log/fail2ban-manual-block.log
fail2ban-client reload
```
+57 -57
View File
@@ -1,19 +1,19 @@
# Authentication
# Anmeldung
`AUTH_MODE` selects how users sign in. The container refuses to start without a working
configuration – there is no unauthenticated mode.
`AUTH_MODE` legt fest, wie sich Benutzer anmelden. Ohne funktionierende Konfiguration startet
der Container nicht – einen Modus ohne Anmeldung gibt es nicht.
| Mode | Use when |
| Modus | Einsatz |
|---|---|
| `local` | one shared admin account is enough |
| `oidc` | you have an identity provider (Authentik, Keycloak, Authelia, Zitadel, …) |
| `local,oidc` | SSO with a local fallback account |
| `proxy` | your reverse proxy already authenticates (forward auth, basic auth, IP allow list) |
| `local` | ein gemeinsames Admin-Konto reicht |
| `oidc` | ein Identity Provider ist vorhanden (Authentik, Keycloak, Authelia, Zitadel, …) |
| `local,oidc` | Single Sign-on mit lokalem Notfallkonto |
| `proxy` | der Reverse Proxy meldet bereits an (Forward Auth, Basic Auth, IP-Liste) |
Every change (ban/unban) additionally needs the per-session CSRF token and a matching
`Origin` header, and every action is written to the audit log with the user name.
Jede Änderung (Sperren/Entsperren) braucht zusätzlich das CSRF-Token der Sitzung und einen
passenden `Origin`-Header; jede Aktion wird mit Benutzernamen protokolliert.
## Local user
## Lokaler Benutzer
```bash
docker compose run --rm -it fail2ban-webui hash-password
@@ -25,42 +25,43 @@ AUTH_USER=admin
AUTH_PASSWORD_HASH='scrypt:32768:8:1$…'
```
- Passwords need at least 12 characters; the hash is scrypt (Werkzeug).
- After `LOGIN_MAX_ATTEMPTS` (5) failures a client IP is locked for `LOGIN_LOCK_MINUTES` (15).
- Failed logins are logged as `LOGIN_FAILED user=… ip=…` – see
[operations.md](operations.md#protecting-the-login-with-fail2ban) to let fail2ban ban
them at the proxy.
- Set `TRUSTED_PROXY_HOPS=1` behind a reverse proxy, otherwise all clients appear with the
proxy's address and share one lockout counter.
- Passwörter brauchen mindestens 12 Zeichen; der Hash ist scrypt (Werkzeug).
- Nach `LOGIN_MAX_ATTEMPTS` (5) Fehlversuchen wird eine Client-IP für `LOGIN_LOCK_MINUTES` (15) gesperrt.
- Fehlgeschlagene Anmeldungen werden als `LOGIN_FAILED user=… ip=…` protokolliert – siehe
[operations.md](operations.md#login-mit-fail2ban-absichern), um sie von fail2ban am Proxy
sperren zu lassen.
- Hinter einem Reverse Proxy `TRUSTED_PROXY_HOPS=1` setzen, sonst erscheinen alle Clients
mit der Adresse des Proxys und teilen sich einen Sperrzähler.
## OpenID Connect
Requirements: `PUBLIC_URL` is set to the external `https://` URL, and the provider allows
the redirect URI **`<PUBLIC_URL>/auth/callback`**.
Voraussetzungen: `PUBLIC_URL` ist auf die externe `https://`-URL gesetzt, und der Provider
erlaubt die Redirect-URI **`<PUBLIC_URL>/auth/callback`**.
The UI uses the authorization code flow with PKCE (`S256`) and a confidential client.
User name: `OIDC_USERNAME_CLAIM` (default `preferred_username`); groups:
`OIDC_GROUPS_CLAIM` (default `groups`).
Die Oberfläche nutzt den Authorization Code Flow mit PKCE (`S256`) und einen vertraulichen
Client. Benutzername: `OIDC_USERNAME_CLAIM` (Standard `preferred_username`); Gruppen:
`OIDC_GROUPS_CLAIM` (Standard `groups`).
**Always restrict who may sign in** with `OIDC_ALLOWED_GROUPS` and/or
`OIDC_ALLOWED_USERS` – or with an access policy in the provider. Without either, every
account that the provider lets use this client can ban IPs (a warning is logged at start).
**Immer einschränken, wer sich anmelden darf** – mit `OIDC_ALLOWED_GROUPS` und/oder
`OIDC_ALLOWED_USERS` oder mit einer Zugriffsrichtlinie im Provider. Ohne beides kann jedes
Konto, das der Provider für diesen Client zulässt, IPs sperren (beim Start wird eine Warnung
protokolliert).
### Authentik
1. **Applications → Providers → Create → OAuth2/OpenID Provider**
- Name: `fail2ban-webui`
- Authorization flow: `default-provider-authorization-explicit-consent` (or implicit)
- Client type: **Confidential** – note *Client ID* and *Client Secret*
- Authorization flow: `default-provider-authorization-explicit-consent` (oder implicit)
- Client type: **Confidential** – *Client ID* und *Client Secret* notieren
- Redirect URIs: `strict` · `https://fail2ban.example.org/auth/callback`
- Signing key: any certificate (e.g. `authentik Self-signed Certificate`)
- Signing key: ein beliebiges Zertifikat (z. B. `authentik Self-signed Certificate`)
- Advanced protocol settings → Scopes: `openid`, `email`, `profile`
(the default *profile* mapping contains the `groups` claim)
(das Standard-Mapping *profile* enthält den Claim `groups`)
2. **Applications → Applications → Create**
- Name: `fail2ban-webui`, Slug: `fail2ban-webui`, Provider: the one from step 1
3. **Directory → Groups → Create** `fail2ban-admins` and add the users.
4. Optional, recommended: in the application under **Policy / Group / User Bindings**
bind the group `fail2ban-admins`, so nobody else even reaches the consent screen.
- Name: `fail2ban-webui`, Slug: `fail2ban-webui`, Provider: der aus Schritt 1
3. **Directory → Groups → Create** `fail2ban-admins` anlegen und die Benutzer hinzufügen.
4. Optional, empfohlen: in der Application unter **Policy / Group / User Bindings** die
Gruppe `fail2ban-admins` binden – dann kommt sonst niemand bis zur Zustimmungsseite.
5. `.env`:
```ini
@@ -73,27 +74,27 @@ account that the provider lets use this client can ban IPs (a warning is logged
OIDC_BUTTON_LABEL=Authentik
```
6. `docker compose up -d`, open `PUBLIC_URL`, click **Sign in with Authentik**.
6. `docker compose up -d`, `PUBLIC_URL` öffnen, auf **Anmelden mit Authentik** klicken.
The issuer URL is shown in Authentik on the provider page (*OpenID Configuration Issuer*).
Die Issuer-URL zeigt Authentik auf der Provider-Seite (*OpenID Configuration Issuer*).
### Other providers
### Andere Provider
| Provider | `OIDC_ISSUER` | Groups |
| Provider | `OIDC_ISSUER` | Gruppen |
|---|---|---|
| Keycloak | `https://kc.example.org/realms/<realm>` | add a *Group Membership* mapper with claim name `groups`, "Full group path" off |
| Authelia | `https://auth.example.org` | `groups` scope; set `OIDC_SCOPES=openid email profile groups` |
| Zitadel | `https://<instance>.zitadel.cloud` | roles instead of groups – use `OIDC_ALLOWED_USERS` or a project role check in Zitadel |
| Keycloak | `https://kc.example.org/realms/<realm>` | einen *Group Membership*-Mapper mit Claim-Name `groups` anlegen, „Full group path" aus |
| Authelia | `https://auth.example.org` | Scope `groups`; `OIDC_SCOPES=openid email profile groups` setzen |
| Zitadel | `https://<instanz>.zitadel.cloud` | Rollen statt Gruppen – `OIDC_ALLOWED_USERS` oder eine Projektrollen-Prüfung in Zitadel verwenden |
### Sign out
### Abmelden
"Sign out" ends the session in the web UI. The session at the provider stays active, so
the next "Sign in with …" usually logs in again without a password.
„Abmelden" beendet die Sitzung in der Weboberfläche. Die Sitzung beim Provider bleibt aktiv;
der nächste Klick auf „Anmelden mit …" meldet daher meist ohne Passwort wieder an.
## Reverse-proxy mode
## Proxy-Modus
The proxy does the authentication; the UI only checks where requests come from and,
optionally, which user the proxy reports.
Der Proxy übernimmt die Anmeldung; die Oberfläche prüft nur, woher Anfragen kommen, und
optional, welchen Benutzer der Proxy meldet.
```ini
AUTH_MODE=proxy
@@ -106,14 +107,13 @@ AUTH_PROXY_HEADER=X-authentik-username
AUTH_PROXY_LOGOUT_URL=https://fail2ban.example.org/outpost.goauthentik.io/sign_out
```
- `AUTH_PROXY_ALLOWED_IPS` is checked against the **direct** peer of the container, not
against `X-Forwarded-For`. If the proxy runs on the same Docker host and reaches the
container via the published port, that is the network gateway (`172.23.51.1` in the
example compose file); check with `docker compose logs` after a request.
- With `AUTH_PROXY_HEADER` set, requests without the header are rejected (401). Make sure
the proxy **removes** that header from client requests – forward-auth integrations
(Authentik, Authelia) do that.
- Without `AUTH_PROXY_HEADER` the audit log shows the user `proxy`.
- `AUTH_PROXY_ALLOWED_IPS` wird gegen den **direkten** Gegenüber des Containers geprüft,
nicht gegen `X-Forwarded-For`. Läuft der Proxy auf demselben Docker-Host und erreicht den
Container über den veröffentlichten Port, ist das das Netzwerk-Gateway (`172.23.51.1` in
der Beispiel-Compose); nach einer Anfrage mit `docker compose logs` prüfen.
- Ist `AUTH_PROXY_HEADER` gesetzt, werden Anfragen ohne diesen Header abgelehnt (401). Der
Proxy muss diesen Header aus Client-Anfragen **entfernen** – Forward-Auth-Integrationen
(Authentik, Authelia) tun das.
- Ohne `AUTH_PROXY_HEADER` steht im Protokoll der Benutzer `proxy`.
Examples for Nginx Proxy Manager, Traefik and Caddy are in
[reverse-proxy.md](reverse-proxy.md).
Beispiele für Nginx Proxy Manager, Traefik und Caddy stehen in [reverse-proxy.md](reverse-proxy.md).
+77 -77
View File
@@ -1,12 +1,12 @@
# Configuration
# Konfiguration
The web UI is configured with environment variables (`.env`, loaded via `env_file:`) and
the host list `config/hosts.yaml`. A complete, commented template is
[`examples/.env.example`](../examples/.env.example). Changes need
`docker compose up -d` (which recreates the container).
Die Weboberfläche wird über Umgebungsvariablen (`.env`, geladen per `env_file:`) und die
Hostliste `config/hosts.yaml` konfiguriert. Eine vollständige, kommentierte Vorlage ist
[`examples/.env.example`](../examples/.env.example). Änderungen brauchen
`docker compose up -d` (das legt den Container neu an).
Values containing `$` (password hashes, some secrets) must be wrapped in **single quotes**
in `.env`, otherwise Docker Compose expands them.
Werte mit `$` (Passwort-Hashes, manche Secrets) müssen in der `.env` in **einfachen
Anführungszeichen** stehen, sonst wertet Docker Compose sie als Variablen aus.
## Hosts – config/hosts.yaml
@@ -18,7 +18,7 @@ hosts:
user: f2bweb # optional, default SSH_USER
```
After adding a host run `keyscan` again, then restart:
Nach dem Hinzufügen eines Hosts `keyscan` erneut ausführen, dann neu starten:
```bash
docker compose run --rm -it fail2ban-webui keyscan
@@ -26,99 +26,99 @@ docker compose run --rm fail2ban-webui check
docker compose up -d --force-recreate
```
`F2B_HOSTS=name=address,name2=address2` (port 22, user `SSH_USER`) is still accepted for
compatibility; entries from both are combined.
`F2B_HOSTS=name=adresse,name2=adresse2` (Port 22, Benutzer `SSH_USER`) wird aus
Kompatibilitätsgründen weiter akzeptiert; Einträge aus beiden Quellen werden zusammengeführt.
## General
## Allgemein
| Variable | Default | Meaning |
| Variable | Standard | Bedeutung |
|---|---|---|
| `PUBLIC_URL` | – | external URL, e.g. `https://fail2ban.example.org`. Required for OIDC; with `https://` session cookies get the `Secure` flag |
| `DEFAULT_LANG` | `en` | language until a user picks one in the dropdown (`en`, `de`, …) |
| `TRUSTED_PROXY_HOPS` | `0` | number of reverse proxies whose `X-Forwarded-For`/`-Proto` headers are trusted. Behind one proxy: `1`. Wrong values make the audit log and the login lockout use the wrong client IP |
| `SESSION_HOURS` | `12` | session lifetime |
| `SESSION_COOKIE_SECURE` | from `PUBLIC_URL` | force the `Secure` cookie flag on/off |
| `SECRET_KEY` | generated | key that signs session cookies; by default created once in `data/secret_key` |
| `TZ` | `Europe/Vienna` (compose) | time zone of the log and audit timestamps |
| `CONFIG_FILE` | `/config/hosts.yaml` | path of the host list inside the container |
| `DATA_DIR` | `/data` | data directory inside the container |
| `PUBLIC_URL` | – | externe URL, z. B. `https://fail2ban.example.org`. Pflicht für OIDC; mit `https://` bekommen Sitzungs-Cookies das `Secure`-Flag |
| `DEFAULT_LANG` | `en` | Sprache, solange ein Benutzer im Dropdown keine gewählt hat (`en`, `de`, …) |
| `TRUSTED_PROXY_HOPS` | `0` | Anzahl der Reverse Proxys, deren `X-Forwarded-For`/`-Proto`-Header vertraut wird. Hinter einem Proxy: `1`. Falsche Werte führen dazu, dass Protokoll und Login-Sperre die falsche Client-IP verwenden |
| `SESSION_HOURS` | `12` | Gültigkeit einer Sitzung |
| `SESSION_COOKIE_SECURE` | aus `PUBLIC_URL` | `Secure`-Flag des Cookies erzwingen oder abschalten |
| `SECRET_KEY` | wird erzeugt | Schlüssel zum Signieren der Sitzungs-Cookies; standardmäßig einmalig in `data/secret_key` angelegt |
| `TZ` | `Europe/Vienna` (Compose) | Zeitzone für Log- und Protokoll-Zeitstempel |
| `CONFIG_FILE` | `/config/hosts.yaml` | Pfad der Hostliste im Container |
| `DATA_DIR` | `/data` | Datenverzeichnis im Container |
## Authentication
## Anmeldung
See [authentication.md](authentication.md) for the complete setup.
Die vollständige Einrichtung steht in [authentication.md](authentication.md).
| Variable | Default | Meaning |
| Variable | Standard | Bedeutung |
|---|---|---|
| `AUTH_MODE` | `local` | `local`, `oidc`, `local,oidc` or `proxy` |
| `AUTH_USER` | `admin` | local user name |
| `AUTH_PASSWORD_HASH` | – | scrypt hash from `hash-password` (single quotes!) |
| `AUTH_PASSWORD_HASH_FILE` | – | alternative: file containing the hash (e.g. a Docker secret) |
| `LOGIN_MAX_ATTEMPTS` | `5` | failed logins per client IP before the lockout |
| `LOGIN_LOCK_MINUTES` | `15` | duration of the lockout window |
| `OIDC_ISSUER` | – | issuer URL; the discovery document is read from `<issuer>/.well-known/openid-configuration` |
| `OIDC_DISCOVERY_URL` | – | full discovery URL instead of `OIDC_ISSUER` |
| `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` | – | client credentials |
| `OIDC_SCOPES` | `openid email profile` | requested scopes |
| `OIDC_USERNAME_CLAIM` | `preferred_username` | claim shown as user name (falls back to `email`, then `sub`) |
| `OIDC_GROUPS_CLAIM` | `groups` | claim with the group list |
| `OIDC_ALLOWED_GROUPS` | – | only members of these groups may sign in (comma separated) |
| `OIDC_ALLOWED_USERS` | – | only these user names may sign in |
| `OIDC_BUTTON_LABEL` | `SSO` | text on the login button ("Sign in with …") |
| `AUTH_PROXY_ALLOWED_IPS` | – | proxy mode: only accept requests from these addresses/networks |
| `AUTH_PROXY_HEADER` | – | proxy mode: header with the user name, e.g. `X-authentik-username` |
| `AUTH_PROXY_LOGOUT_URL` | – | proxy mode: target of the "Sign out" button |
| `AUTH_MODE` | `local` | `local`, `oidc`, `local,oidc` oder `proxy` |
| `AUTH_USER` | `admin` | lokaler Benutzername |
| `AUTH_PASSWORD_HASH` | – | scrypt-Hash aus `hash-password` (einfache Anführungszeichen!) |
| `AUTH_PASSWORD_HASH_FILE` | – | Alternative: Datei mit dem Hash (z. B. ein Docker-Secret) |
| `LOGIN_MAX_ATTEMPTS` | `5` | Fehlversuche pro Client-IP bis zur Sperre |
| `LOGIN_LOCK_MINUTES` | `15` | Dauer des Sperrfensters |
| `OIDC_ISSUER` | – | Issuer-URL; das Discovery-Dokument wird von `<issuer>/.well-known/openid-configuration` gelesen |
| `OIDC_DISCOVERY_URL` | – | vollständige Discovery-URL statt `OIDC_ISSUER` |
| `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` | – | Client-Zugangsdaten |
| `OIDC_SCOPES` | `openid email profile` | angeforderte Scopes |
| `OIDC_USERNAME_CLAIM` | `preferred_username` | Claim, der als Benutzername angezeigt wird (Rückfall auf `email`, dann `sub`) |
| `OIDC_GROUPS_CLAIM` | `groups` | Claim mit der Gruppenliste |
| `OIDC_ALLOWED_GROUPS` | – | nur Mitglieder dieser Gruppen dürfen sich anmelden (durch Komma getrennt) |
| `OIDC_ALLOWED_USERS` | – | nur diese Benutzernamen dürfen sich anmelden |
| `OIDC_BUTTON_LABEL` | `SSO` | Text auf dem Anmeldeknopf („Anmelden mit …") |
| `AUTH_PROXY_ALLOWED_IPS` | – | Proxy-Modus: nur Anfragen von diesen Adressen/Netzen annehmen |
| `AUTH_PROXY_HEADER` | – | Proxy-Modus: Header mit dem Benutzernamen, z. B. `X-authentik-username` |
| `AUTH_PROXY_LOGOUT_URL` | – | Proxy-Modus: Ziel des „Abmelden"-Knopfs |
## Agents (SSH)
## Agenten (SSH)
| Variable | Default | Meaning |
| Variable | Standard | Bedeutung |
|---|---|---|
| `SSH_USER` | `f2bweb` | default user for hosts without `user:` |
| `SSH_KEY` | `/data/ssh/id_ed25519` | private key |
| `KNOWN_HOSTS` | `/data/ssh/known_hosts` | pinned host keys (written by `keyscan`) |
| `SSH_TIMEOUT` | `40` | seconds per agent call |
| `CACHE_SECONDS` | `15` | how long the ban list is cached between page refreshes |
| `MANUAL_JAIL` | `manual-block` | jail name shown in the UI hint – the agent decides the real jail (`/etc/f2b-webctl.conf`) |
| `AUDIT_LOG` | `/data/audit.log` | audit log (JSON lines) |
| `SSH_USER` | `f2bweb` | Standardbenutzer für Hosts ohne `user:` |
| `SSH_KEY` | `/data/ssh/id_ed25519` | privater Schlüssel |
| `KNOWN_HOSTS` | `/data/ssh/known_hosts` | festgelegte Host-Schlüssel (geschrieben von `keyscan`) |
| `SSH_TIMEOUT` | `40` | Sekunden pro Agent-Aufruf |
| `CACHE_SECONDS` | `15` | wie lange die Sperrliste zwischen zwei Seitenaufrufen zwischengespeichert wird |
| `MANUAL_JAIL` | `manual-block` | Jail-Name im Hinweis der Oberfläche – das tatsächliche Jail bestimmt der Agent (`/etc/f2b-webctl.conf`) |
| `AUDIT_LOG` | `/data/audit.log` | Protokoll (JSON-Zeilen) |
The SSH client ignores the system `ssh_config`, uses only the configured key, never
asks questions and refuses unknown host keys.
Der SSH-Client ignoriert die System-`ssh_config`, verwendet nur den konfigurierten
Schlüssel, stellt nie Rückfragen und lehnt unbekannte Host-Schlüssel ab.
## GeoIP
| Variable | Default | Meaning |
| Variable | Standard | Bedeutung |
|---|---|---|
| `GEOIP_AUTO_DOWNLOAD` | `0` | `1`: download the free [DB-IP Lite](https://db-ip.com/db/lite.php) city database (CC BY 4.0) and refresh it monthly |
| `GEOIP_DB` | `/data/geoip/dbip-city-lite.mmdb` | path of the database – any MaxMind-format city database works (e.g. GeoLite2 City) |
| `GEOIP_AUTO_DOWNLOAD` | `0` | `1`: die freie Städte-Datenbank [DB-IP Lite](https://db-ip.com/db/lite.php) (CC BY 4.0) herunterladen und monatlich erneuern |
| `GEOIP_DB` | `/data/geoip/dbip-city-lite.mmdb` | Pfad der Datenbank – jede Städte-Datenbank im MaxMind-Format funktioniert (z. B. GeoLite2 City) |
With a city database the UI also shows the **origin map** (country-only databases have no
coordinates). The map is bundled with the app (Natural Earth, public domain) – no external
tile server is contacted. Without a database the "Origin" column stays empty and the map
is hidden.
Mit einer Städte-Datenbank zeigt die Oberfläche auch die **Herkunftskarte** (Datenbanken
nur mit Ländern haben keine Koordinaten). Die Karte ist Teil der App (Natural Earth, Public
Domain) – es wird kein externer Kartendienst abgefragt. Ohne Datenbank bleibt die Spalte
„Herkunft" leer und die Karte ausgeblendet.
## Loki and Grafana
## Loki und Grafana
With Loki the UI gets a **History** button per IP showing the matching log lines of the
last days; with Grafana every ban gets a link to Explore.
Mit Loki bekommt die Oberfläche pro IP einen **Verlauf**-Knopf mit den passenden Logzeilen
der letzten Tage; mit Grafana bekommt jede Sperre einen Link nach Explore.
| Variable | Default | Meaning |
| Variable | Standard | Bedeutung |
|---|---|---|
| `LOKI_URL` | – | e.g. `http://loki:3100`; empty = history disabled |
| `LOKI_SELECTOR` | `{job=~".+"}` | LogQL stream selector for the search, e.g. `{job=~"fail2ban\|sshd\|nginx"}` |
| `LOKI_DAYS` | `7` | how far back to search |
| `LOKI_LIMIT` | `50` | maximum lines |
| `LOKI_USER` / `LOKI_PASSWORD` | – | basic auth |
| `LOKI_TENANT` | – | `X-Scope-OrgID` for multi-tenant Loki |
| `GRAFANA_URL` | – | e.g. `https://grafana.example.org` |
| `GRAFANA_DASHBOARD` | – | path of a dashboard for the header link, e.g. `/d/abc123/security` |
| `GRAFANA_LOKI_UID` | `loki` | UID of the Loki data source in Grafana (for Explore links) |
| `LOKI_URL` | – | z. B. `http://loki:3100`; leer = Verlauf ausgeschaltet |
| `LOKI_SELECTOR` | `{job=~".+"}` | LogQL-Stream-Selektor für die Suche, z. B. `{job=~"fail2ban\|sshd\|nginx"}` |
| `LOKI_DAYS` | `7` | wie weit zurück gesucht wird |
| `LOKI_LIMIT` | `50` | maximale Anzahl Zeilen |
| `LOKI_USER` / `LOKI_PASSWORD` | – | Basic Auth |
| `LOKI_TENANT` | – | `X-Scope-OrgID` für mandantenfähiges Loki |
| `GRAFANA_URL` | – | z. B. `https://grafana.example.org` |
| `GRAFANA_DASHBOARD` | – | Pfad eines Dashboards für den Link in der Kopfzeile, z. B. `/d/abc123/security` |
| `GRAFANA_LOKI_UID` | `loki` | UID der Loki-Datenquelle in Grafana (für Explore-Links) |
Example – exclude the UI's own access log from the search:
Beispiel – das eigene Zugriffslog der Oberfläche von der Suche ausnehmen:
```ini
LOKI_SELECTOR={job=~"fail2ban|ufw|nginx|sshd", domain!="fail2ban.example.org"}
```
## Agent settings
## Agent-Einstellungen
Protected addresses and the manual jail are configured **on each host** in
`/etc/f2b-webctl.conf` – see [agent.md](agent.md#configuration--etcf2b-webctlconf).
Geschützte Adressen und das manuelle Jail werden **auf jedem Host** in
`/etc/f2b-webctl.conf` eingestellt – siehe [agent.md](agent.md#konfiguration--etcf2b-webctlconf).
+46 -45
View File
@@ -1,24 +1,24 @@
# Development
# Entwicklung
## Layout
## Aufbau
```
app/webui/ Flask application (package "webui")
app.py routes, security headers, audit log
auth.py local / OIDC / proxy authentication, login lockout
backend.py SSH to the agents, GeoIP, Loki
app/webui/ Flask-Anwendung (Paket "webui")
app.py Routen, Sicherheits-Header, Protokoll
auth.py Anmeldung lokal / OIDC / Proxy, Login-Sperre
backend.py SSH zu den Agenten, GeoIP, Loki
cli.py serve, hash-password, keygen, keyscan, check
config.py settings from the environment, hosts.yaml
i18n.py, locales/ translations (one JSON file per language)
templates/, static/
config.py Einstellungen aus der Umgebung, hosts.yaml
i18n.py, locales/ Übersetzungen (eine JSON-Datei pro Sprache)
templates/, static/ (static/world.svg: Weltkarte, Natural Earth)
agent/ f2b-webctl, install.sh, uninstall.sh
docker/Dockerfile production image
docker/Dockerfile Produktions-Image
examples/ docker-compose.yaml, .env.example, hosts.yaml.example
dev/testbed/ web UI + two fail2ban hosts in Docker
tests/smoke_test.py web UI tests without Docker/SSH
dev/testbed/ Weboberfläche + zwei fail2ban-Hosts in Docker
tests/smoke_test.py Tests der Weboberfläche ohne Docker/SSH
```
## Smoke test (no Docker needed)
## Smoke-Test (ohne Docker)
```bash
python3 -m venv .venv
@@ -26,20 +26,20 @@ python3 -m venv .venv
.venv/bin/python tests/smoke_test.py
```
The agents are simulated. Covered: login, lockout, CSRF/Origin checks, translations,
ban/unban validation, audit log, proxy mode, config validation, SSH command line, log
injection.
Die Agenten werden simuliert. Abgedeckt: Anmeldung, Login-Sperre, CSRF-/Origin-Prüfung,
Übersetzungen, Prüfung beim Sperren/Entsperren, Protokoll, Proxy-Modus, Prüfung der
Konfiguration, SSH-Befehlszeile, Log-Injection.
## Testbed (Docker)
`dev/testbed` starts the web UI and two Debian hosts with sshd and fail2ban (dummy ban
actions – nothing is blocked). The agents are installed with `agent/install.sh` at
container start:
`dev/testbed` startet die Weboberfläche und zwei Debian-Hosts mit sshd und fail2ban
(Dummy-Banactions – es wird nichts gesperrt). Die Agenten werden beim Containerstart mit
`agent/install.sh` installiert:
| Host | Agent setup |
| Host | Agent-Einrichtung |
|---|---|
| `agent1` | user `f2bweb` + sudo, `--from` the testbed network, extra protected IP |
| `agent2` | user `root`, key in `authorized_keys2`, manual jail named `blocklist` |
| `agent1` | Benutzer `f2bweb` + sudo, `--from` auf das Testbed-Netz, zusätzliche geschützte IP |
| `agent2` | Benutzer `root`, Schlüssel in `authorized_keys2`, manuelles Jail namens `blocklist` |
```bash
cd dev/testbed
@@ -52,42 +52,43 @@ docker compose up -d agent1 agent2
docker compose run --rm webui keyscan --yes
docker compose run --rm webui check
docker compose up -d webui # http://127.0.0.1:18080
docker compose down -v # remove everything
docker compose down -v # alles entfernen
```
`hosts.yaml` is written into the data volume because Docker Desktop cannot bind-mount
network drives. After recreating an agent its host key changes – run `keyscan` again.
`hosts.yaml` wird ins Daten-Volume geschrieben, weil Docker Desktop Netzlaufwerke nicht
einbinden kann. Nach dem Neuanlegen eines Agenten ändert sich sein Host-Schlüssel –
`keyscan` erneut ausführen.
On Windows with Git Bash, prefix `docker compose exec … /path` with `MSYS_NO_PATHCONV=1`,
otherwise the path is rewritten to a Windows path.
Unter Windows mit Git Bash `docker compose exec … /pfad` mit `MSYS_NO_PATHCONV=1`
voranstellen, sonst wird der Pfad in einen Windows-Pfad umgeschrieben.
## Adding a language
## Neue Sprache hinzufügen
1. Copy `app/webui/locales/en.json` to `app/webui/locales/<code>.json` (e.g. `fr.json`).
2. Set `"_name"` to the language's own name (`"Français"`) – it appears in the dropdown.
3. Translate the values; keep `{placeholders}` unchanged. Missing keys fall back to English.
4. Run the smoke test and check the UI.
1. `app/webui/locales/en.json` nach `app/webui/locales/<code>.json` kopieren (z. B. `fr.json`).
2. `"_name"` auf den Eigennamen der Sprache setzen (`"Français"`) – so erscheint sie im Dropdown.
3. Die Werte übersetzen; `{Platzhalter}` unverändert lassen. Fehlende Schlüssel fallen auf Englisch zurück.
4. Smoke-Test ausführen und die Oberfläche prüfen.
## Building the image
## Image bauen
```bash
docker build -f docker/Dockerfile --build-arg VERSION=1.0.0 -t fail2ban-webui:1.0.0 .
# multi-arch, as in CI
# Multi-Arch, wie in der CI
docker buildx build -f docker/Dockerfile --platform linux/amd64,linux/arm64 \
--build-arg VERSION=1.0.0 -t git.retronerd.at/schiwagoa/fail2ban-webui:1.0.0 --push .
```
Scan before publishing:
Vor dem Veröffentlichen scannen:
```bash
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.58.1 \
image --scanners vuln --ignore-unfixed --severity HIGH,CRITICAL fail2ban-webui:1.0.0
```
## Agent protocol
## Agent-Protokoll
The web UI runs `ssh … <user>@<host> "<command>"`; the agent reads the command from
`SSH_ORIGINAL_COMMAND` and prints exactly one JSON line:
Die Weboberfläche führt `ssh … <benutzer>@<host> "<befehl>"` aus; der Agent liest den Befehl
aus `SSH_ORIGINAL_COMMAND` und gibt genau eine JSON-Zeile aus:
```json
{"ok": true, "host": "web01", "agent_version": "1.0.0", "manual_jail": "manual-block",
@@ -95,12 +96,12 @@ The web UI runs `ssh … <user>@<host> "<command>"`; the agent reads the command
"bantime": 600, "end": "2026-10-25 10:10:00"}], "total": 42}}}
```
Errors: `{"ok": false, "error_key": "agent_protected", "error_args": {"ip": "…"}, "error": "English text"}`.
The web UI translates `error_key` with `locales/*.json` (keys starting with `agent_`);
unknown keys show the English `error`.
Fehler: `{"ok": false, "error_key": "agent_protected", "error_args": {"ip": "…"}, "error": "English text"}`.
Die Weboberfläche übersetzt `error_key` mit `locales/*.json` (Schlüssel mit Präfix `agent_`);
unbekannte Schlüssel zeigen den englischen `error`-Text.
## Versioning
## Versionierung
- Version numbers follow SemVer and are set at build time (`VERSION` build arg →
`APP_VERSION`); the agent has its own `VERSION` in `agent/f2b-webctl`.
- Every change goes into `CHANGELOG.md` under *Unreleased*.
- Versionsnummern folgen SemVer und werden beim Bauen gesetzt (Build-Argument `VERSION` →
`APP_VERSION`); der Agent hat eine eigene `VERSION` in `agent/f2b-webctl`.
- Jede Änderung kommt in `CHANGELOG.md` unter *Unreleased*.
+57 -54
View File
@@ -1,43 +1,43 @@
# Installation – web UI
# Installation – Weboberfläche
This guide sets up the web UI on a Docker host, step by step. The agents on your
fail2ban hosts are covered in [agent.md](agent.md); you need both.
Diese Anleitung richtet die Weboberfläche Schritt für Schritt auf einem Docker-Host ein.
Die Agenten auf den fail2ban-Hosts sind in [agent.md](agent.md) beschrieben; du brauchst beides.
- Source: <https://git.retronerd.at/schiwagoa/fail2ban-webui>
- Quelle: <https://git.retronerd.at/schiwagoa/fail2ban-webui>
- Image: `git.retronerd.at/schiwagoa/fail2ban-webui` (`amd64`, `arm64`)
- Tags: `1.0.0` (exact), `1` (latest 1.x), `latest`
- Tags: `1.0.0` (genau diese Version), `1` (neueste 1.x), `latest`
## 1. Prerequisites
## 1. Voraussetzungen
- Docker Engine with Compose v2 (`docker compose version`)
- Outgoing SSH (TCP 22 or your port) from the Docker host to every fail2ban host
- A DNS name and a reverse proxy with TLS for the UI (see [reverse-proxy.md](reverse-proxy.md))
- Docker Engine mit Compose v2 (`docker compose version`)
- Ausgehendes SSH (TCP 22 oder dein Port) vom Docker-Host zu jedem fail2ban-Host
- Ein DNS-Name und ein Reverse Proxy mit TLS für die Oberfläche (siehe [reverse-proxy.md](reverse-proxy.md))
Check that the subnet used in the example (`172.23.51.0/24`) is free:
Prüfen, ob das Subnetz aus dem Beispiel (`172.23.51.0/24`) frei ist:
```bash
docker network inspect $(docker network ls -q) --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}}{{end}}' | grep 172.23.51 \
|| echo "172.23.51.0/24 is free"
```
If it is taken, pick another `/24` and change it in step 3.
Ist es belegt, ein anderes `/24` wählen und in Schritt 3 anpassen.
## 2. Directories
## 2. Verzeichnisse
```bash
mkdir -p /opt/fail2ban-webui/config /opt/fail2ban-webui/data
cd /opt/fail2ban-webui
# the container runs as UID 10050 and needs to write to data/
# der Container läuft als UID 10050 und muss in data/ schreiben
chown -R 10050:10050 data
chmod 700 data
```
| Path | Content |
| Pfad | Inhalt |
|---|---|
| `docker-compose.yaml` | the service |
| `.env` | settings and secrets (password hash, OIDC secret) |
| `config/hosts.yaml` | the fail2ban hosts (mounted read-only) |
| `data/` | SSH key, `known_hosts`, session secret, audit log, GeoIP database |
| `docker-compose.yaml` | der Dienst |
| `.env` | Einstellungen und Geheimnisse (Passwort-Hash, OIDC-Secret) |
| `config/hosts.yaml` | die fail2ban-Hosts (schreibgeschützt eingebunden) |
| `data/` | SSH-Schlüssel, `known_hosts`, Sitzungsschlüssel, Protokoll, GeoIP-Datenbank |
## 3. docker-compose.yaml
@@ -102,13 +102,13 @@ networks:
ip_range: 172.23.51.128/25
```
If the reverse proxy runs on another machine, bind the port to the LAN address instead,
e.g. `"192.168.1.10:8080:8080"` – never to `0.0.0.0` without a firewall in front.
Läuft der Reverse Proxy auf einer anderen Maschine, den Port stattdessen an die LAN-Adresse
binden, z. B. `"192.168.1.10:8080:8080"` – niemals ohne Firewall davor an `0.0.0.0`.
## 4. .env
Start with this minimal file; every option is described in
[configuration.md](configuration.md) and in [`examples/.env.example`](../examples/.env.example).
Mit dieser minimalen Datei beginnen; alle Optionen stehen in
[configuration.md](configuration.md) und in [`examples/.env.example`](../examples/.env.example).
```bash
nano .env
@@ -129,35 +129,38 @@ MANUAL_JAIL=manual-block
GEOIP_AUTO_DOWNLOAD=1
```
### Password hash
`DEFAULT_LANG=de` stellt die Oberfläche standardmäßig auf Deutsch; jeder Benutzer kann die
Sprache zusätzlich im Dropdown wählen.
### Passwort-Hash
```bash
docker compose run --rm -it fail2ban-webui hash-password
```
Copy the printed line (`AUTH_PASSWORD_HASH='scrypt:…'`) into `.env` and replace the
placeholder. **Keep the single quotes** – the hash contains `$` signs that Docker Compose
would otherwise try to expand. For single sign-on instead see
[authentication.md](authentication.md).
Die ausgegebene Zeile (`AUTH_PASSWORD_HASH='scrypt:…'`) in die `.env` kopieren und den
Platzhalter ersetzen. **Die einfachen Anführungszeichen beibehalten** – der Hash enthält
`$`-Zeichen, die Docker Compose sonst als Variablen auswerten würde. Für Single Sign-on
stattdessen siehe [authentication.md](authentication.md).
## 5. SSH key
## 5. SSH-Schlüssel
```bash
docker compose run --rm fail2ban-webui keygen
```
The command creates `data/ssh/id_ed25519` and prints the public key
(`ssh-ed25519 AAAA… fail2ban-webui`). Copy it – you need it for the agent installer.
Der Befehl erzeugt `data/ssh/id_ed25519` und gibt den öffentlichen Schlüssel aus
(`ssh-ed25519 AAAA… fail2ban-webui`). Kopieren – er wird für den Agent-Installer gebraucht.
## 6. Agents
## 6. Agenten
Install the agent on every fail2ban host as described in [agent.md](agent.md). In short:
Den Agenten auf jedem fail2ban-Host installieren, wie in [agent.md](agent.md) beschrieben. Kurzfassung:
```bash
# on each fail2ban host, as root
# auf jedem fail2ban-Host, als root
git clone https://git.retronerd.at/schiwagoa/fail2ban-webui.git /tmp/fail2ban-webui
echo 'ssh-ed25519 AAAA… fail2ban-webui' > /tmp/webui.pub
bash /tmp/fail2ban-webui/agent/install.sh --key-file /tmp/webui.pub --from <public IP of the web UI host>
bash /tmp/fail2ban-webui/agent/install.sh --key-file /tmp/webui.pub --from <öffentliche IP des Webui-Hosts>
```
## 7. config/hosts.yaml
@@ -180,24 +183,24 @@ hosts:
address: host.docker.internal
```
`user` defaults to `SSH_USER` (`f2bweb`). Set `user: root` for hosts where the agent was
installed with `--user root`.
`user` ist standardmäßig `SSH_USER` (`f2bweb`). Für Hosts, auf denen der Agent mit
`--user root` installiert wurde, `user: root` setzen.
## 8. Host keys and connection test
## 8. Host-Schlüssel und Verbindungstest
```bash
docker compose run --rm -it fail2ban-webui keyscan
```
`keyscan` shows the fingerprint of every host and asks before writing
`data/ssh/known_hosts`. Compare the fingerprints with the hosts:
`keyscan` zeigt den Fingerprint jedes Hosts und fragt vor dem Schreiben von
`data/ssh/known_hosts` nach. Die Fingerprints mit den Hosts vergleichen:
```bash
# on the fail2ban host
# auf dem fail2ban-Host
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
```
Then test every agent:
Danach jeden Agenten testen:
```bash
docker compose run --rm fail2ban-webui check
@@ -209,33 +212,33 @@ OK lxc-proxy agent 1.0.0, 2 jails, 0 bans
OK dockerhost agent 1.0.0, 4 jails, 7 bans
```
Every host must show `OK` – see [operations.md](operations.md#troubleshooting) otherwise.
Jeder Host muss `OK` melden – sonst siehe [operations.md](operations.md#fehlersuche).
## 9. Start
## 9. Starten
```bash
docker compose up -d
docker compose ps # STATUS: Up … (healthy)
docker compose logs -f fail2ban-webui # Ctrl+C to leave
docker compose logs -f fail2ban-webui # mit Strg+C verlassen
```
The log shows a summary like
Das Log zeigt eine Zusammenfassung wie
`fail2ban-webui 1.0.0 – hosts: web01, lxc-proxy, dockerhost – auth: local – loki: off – geoip: on`.
Local test without the proxy:
Lokaler Test ohne Proxy:
```bash
curl -s http://127.0.0.1:8080/healthz # ok
```
## 10. Reverse proxy
## 10. Reverse Proxy
Publish `http://127.0.0.1:8080` (or the LAN address from step 3) under your DNS name with
TLS – examples for Nginx Proxy Manager, Caddy, Traefik and nginx are in
[reverse-proxy.md](reverse-proxy.md). Then open `PUBLIC_URL` and sign in.
`http://127.0.0.1:8080` (oder die LAN-Adresse aus Schritt 3) unter deinem DNS-Namen mit
TLS veröffentlichen – Beispiele für Nginx Proxy Manager, Caddy, Traefik und nginx stehen in
[reverse-proxy.md](reverse-proxy.md). Dann `PUBLIC_URL` öffnen und anmelden.
## Next steps
## Nächste Schritte
- [Protect the login with fail2ban](operations.md#protecting-the-login-with-fail2ban)
- [Updates and backups](operations.md)
- Optional: [Loki history and Grafana links](configuration.md#loki-and-grafana)
- [Login mit fail2ban absichern](operations.md#login-mit-fail2ban-absichern)
- [Updates und Backups](operations.md)
- Optional: [Loki-Verlauf und Grafana-Links](configuration.md#loki-und-grafana)
+62 -61
View File
@@ -1,32 +1,32 @@
# Operations
# Betrieb
All commands are run in the directory with `docker-compose.yaml` (e.g. `/opt/fail2ban-webui`).
Alle Befehle im Verzeichnis mit der `docker-compose.yaml` ausführen (z. B. `/opt/fail2ban-webui`).
## Status and logs
## Status und Logs
```bash
docker compose ps # Up … (healthy)
docker compose logs --tail 100 fail2ban-webui
docker compose logs -f fail2ban-webui | grep -E 'AUDIT|LOGIN|ERROR|WARNING'
docker compose run --rm fail2ban-webui check # every agent reachable?
docker compose run --rm fail2ban-webui check # jeder Agent erreichbar?
```
Log lines of interest:
Wichtige Logzeilen:
| Line | Meaning |
| Zeile | Bedeutung |
|---|---|
| `fail2ban-webui 1.0.0 – hosts: … – auth: … – loki: … – geoip: …` | start summary |
| `AUDIT {…}` | ban/unban with user, client IP, hosts and result |
| `LOGIN_OK` / `LOGIN_FAILED` / `LOGIN_LOCKED` / `LOGOUT` | sign-in events |
| `OIDC_FAILED` / `OIDC_DENIED` | SSO errors / account not allowed |
| `fail2ban-webui 1.0.0 – hosts: … – auth: … – loki: … – geoip: …` | Zusammenfassung beim Start |
| `AUDIT {…}` | Sperren/Entsperren mit Benutzer, Client-IP, Hosts und Ergebnis |
| `LOGIN_OK` / `LOGIN_FAILED` / `LOGIN_LOCKED` / `LOGOUT` | Anmeldeereignisse |
| `OIDC_FAILED` / `OIDC_DENIED` | SSO-Fehler / Konto nicht zugelassen |
The audit log is also kept in `data/audit.log` (one JSON object per line):
Das Protokoll liegt zusätzlich in `data/audit.log` (ein JSON-Objekt pro Zeile):
```bash
tail -n 20 data/audit.log | python3 -m json.tool --json-lines
```
On the hosts every agent action is in syslog:
Auf den Hosts steht jede Agent-Aktion im Syslog:
```bash
journalctl -t f2b-webctl --since today
@@ -35,63 +35,64 @@ journalctl -t f2b-webctl --since today
## Update
```bash
cp -a .env docker-compose.yaml config /backups/fail2ban-webui_$(date +%F)/ # or your backup location
cp -a .env docker-compose.yaml config /backups/fail2ban-webui_$(date +%F)/ # oder dein Backup-Ort
docker compose pull
docker compose up -d
docker compose ps
docker compose run --rm fail2ban-webui check
```
Read the [changelog](../CHANGELOG.md) first – if it mentions a new agent version, update
the agents as well (re-run `install.sh` with the same options, see [agent.md](agent.md#update)).
The `1` tag only moves within the 1.x series; breaking changes get a new major version.
Vorher das [Changelog](../CHANGELOG.md) lesen – nennt es eine neue Agent-Version, auch die
Agenten aktualisieren (`install.sh` mit denselben Optionen erneut ausführen, siehe
[agent.md](agent.md#aktualisieren)). Der Tag `1` bewegt sich nur innerhalb von 1.x;
inkompatible Änderungen bekommen eine neue Hauptversion.
Rebuild/pull regularly even without new features: the image is rebuilt with current
Debian security updates.
Auch ohne neue Funktionen regelmäßig neu ziehen bzw. bauen: Das Image wird mit aktuellen
Debian-Sicherheitsupdates neu gebaut.
## Backup
| What | Why |
| Was | Warum |
|---|---|
| `.env` | settings, password hash, OIDC secret |
| `config/hosts.yaml` | host list |
| `data/ssh/` | private key and pinned host keys – without them every agent needs the new key |
| `data/audit.log` | audit trail |
| `data/secret_key` | only keeps sessions valid; losing it signs everybody out |
| `.env` | Einstellungen, Passwort-Hash, OIDC-Secret |
| `config/hosts.yaml` | Hostliste |
| `data/ssh/` | privater Schlüssel und festgelegte Host-Schlüssel – ohne sie braucht jeder Agent den neuen Schlüssel |
| `data/audit.log` | Protokoll |
| `data/secret_key` | hält nur Sitzungen gültig; geht er verloren, werden alle abgemeldet |
`data/geoip/` is downloaded again automatically.
`data/geoip/` wird automatisch neu heruntergeladen.
```bash
tar -cpzf /backups/fail2ban-webui_$(date +%F_%H-%M).tar.gz .env docker-compose.yaml config data
sha256sum /backups/fail2ban-webui_*.tar.gz | tail -n1
```
The archive contains the private SSH key – store it accordingly.
Das Archiv enthält den privaten SSH-Schlüssel – entsprechend sicher aufbewahren.
## Moving to another host
## Umzug auf einen anderen Host
1. `docker compose stop` on the old host, create the archive as above.
2. Copy it, unpack with `tar -xpzf` (keeps owner 10050 and permissions).
3. On **every agent** add the new address to `from=` – easiest by re-running
`install.sh … --from <new address>`.
4. `docker compose up -d`, then `check`.
1. Auf dem alten Host `docker compose stop`, Archiv wie oben erstellen.
2. Kopieren, mit `tar -xpzf` entpacken (behält Besitzer 10050 und Rechte).
3. Auf **jedem Agenten** die neue Adresse in `from=` aufnehmen – am einfachsten
`install.sh … --from <neue Adresse>` erneut ausführen.
4. `docker compose up -d`, danach `check`.
## Rotating the SSH key
## SSH-Schlüssel tauschen
```bash
docker compose run --rm fail2ban-webui keygen --force
# on every host:
bash install.sh --key-file new.pub --from … (same options as before)
# auf jedem Host:
bash install.sh --key-file new.pub --from … (dieselben Optionen wie zuvor)
```
The installer replaces only entries with the same key; remove the old one with
`uninstall.sh` first or delete its line from `authorized_keys` (it contains
Der Installer ersetzt nur Einträge mit demselben Schlüssel; den alten vorher mit
`uninstall.sh` entfernen oder seine Zeile aus `authorized_keys` löschen (sie enthält
`command="…f2b-webctl"`).
## Protecting the login with fail2ban
## Login mit fail2ban absichern
The UI locks a client after 5 failed logins itself. To ban persistent attackers on the
firewall, let fail2ban on the **proxy host** read the container log.
Die Oberfläche sperrt einen Client nach 5 Fehlversuchen selbst. Um hartnäckige Angreifer an
der Firewall zu sperren, liest fail2ban auf dem **Proxy-Host** das Container-Log.
`/etc/fail2ban/filter.d/fail2ban-webui.conf`
@@ -124,27 +125,27 @@ fail2ban-client reload
fail2ban-regex /var/lib/docker/containers/*/*-json.log /etc/fail2ban/filter.d/fail2ban-webui.conf
```
This only works when the container sees the real client address (`TRUSTED_PROXY_HOPS`
set correctly). User names are logged with spaces and control characters replaced, so a
crafted user name cannot inject a second log line.
Das funktioniert nur, wenn der Container die echte Client-Adresse sieht (`TRUSTED_PROXY_HOPS`
richtig gesetzt). Benutzernamen werden mit ersetzten Leer- und Steuerzeichen protokolliert;
ein präparierter Benutzername kann also keine zweite Logzeile einschleusen.
## Troubleshooting
## Fehlersuche
| Symptom | Cause / fix |
| Symptom | Ursache / Lösung |
|---|---|
| Container exits: `… is not writable for UID 10050` | `chown -R 10050:10050 data` |
| Container exits: `AUTH_MODE=local needs AUTH_PASSWORD_HASH` | run `hash-password`, paste the line with single quotes |
| Container exits: `No hosts configured` | `config/hosts.yaml` missing or empty; on Windows/Docker Desktop: the drive cannot be bind-mounted |
| Login always fails after pasting the hash | single quotes missing in `.env` – Compose ate the `$` parts |
| Everybody locked out together / audit shows the proxy IP | `TRUSTED_PROXY_HOPS` not set |
| `Host key verification failed` | host not in `known_hosts` or its key changed → `keyscan`, compare fingerprints |
| `Permission denied (publickey)` | key not installed for that user, wrong `user:` in `hosts.yaml`, wrong `from=` (the address the host sees!), or sshd does not read that `authorized_keys` file |
| `sudo: a password is required` | sudo rule missing → re-run `install.sh` |
| `The agent does not run as root` | forced command without `sudo -n` for a non-root user → re-run `install.sh` |
| `Timeout talking to …` | firewall/port, or the host is slow – raise `SSH_TIMEOUT` |
| `The jail manual-block does not exist on this host` | installed with `--no-jail` or with another `--manual-jail`; check `MANUAL_JAIL` in `/etc/f2b-webctl.conf` |
| Bans have no start/end time | fail2ban older than 0.11 (no `--with-time`) |
| Banned IP still reaches a Docker service | banaction blocks only `INPUT` – see [Docker hosts](agent.md#docker-hosts) |
| OIDC: `redirect_uri` mismatch | provider must allow exactly `<PUBLIC_URL>/auth/callback` |
| OIDC: `mismatching_state` | session cookie lost between redirect and callback: `PUBLIC_URL` host differs from the one in the browser, or `https` without `Secure`/proxy headers |
| OIDC: "account … is not allowed" | user not in `OIDC_ALLOWED_GROUPS`; check that the provider sends the `groups` claim |
| Container beendet sich: `… is not writable for UID 10050` | `chown -R 10050:10050 data` |
| Container beendet sich: `AUTH_MODE=local needs AUTH_PASSWORD_HASH` | `hash-password` ausführen, Zeile mit einfachen Anführungszeichen einfügen |
| Container beendet sich: `No hosts configured` | `config/hosts.yaml` fehlt oder ist leer; unter Windows/Docker Desktop: das Laufwerk lässt sich nicht einbinden |
| Anmeldung scheitert immer, nachdem der Hash eingefügt wurde | einfache Anführungszeichen in der `.env` fehlen – Compose hat die `$`-Teile verschluckt |
| Alle werden gemeinsam gesperrt / Protokoll zeigt die Proxy-IP | `TRUSTED_PROXY_HOPS` nicht gesetzt |
| `Host key verification failed` | Host nicht in `known_hosts` oder sein Schlüssel hat sich geändert → `keyscan`, Fingerprints vergleichen |
| `Permission denied (publickey)` | Schlüssel für diesen Benutzer nicht installiert, falsches `user:` in `hosts.yaml`, falsches `from=` (die Adresse, die der Host sieht!) oder sshd liest diese `authorized_keys`-Datei nicht |
| `sudo: a password is required` | sudo-Regel fehlt → `install.sh` erneut ausführen |
| `The agent does not run as root` | Forced-Command ohne `sudo -n` für einen Nicht-root-Benutzer → `install.sh` erneut ausführen |
| `Timeout talking to …` | Firewall/Port, oder der Host ist langsam – `SSH_TIMEOUT` erhöhen |
| `The jail manual-block does not exist on this host` | mit `--no-jail` oder einem anderen `--manual-jail` installiert; `MANUAL_JAIL` in `/etc/f2b-webctl.conf` prüfen |
| Sperren haben keine Start-/Endzeit | fail2ban älter als 0.11 (kein `--with-time`) |
| Gesperrte IP erreicht trotzdem einen Docker-Dienst | Banaction sperrt nur `INPUT` – siehe [Docker-Hosts](agent.md#docker-hosts) |
| OIDC: `redirect_uri` passt nicht | Provider muss genau `<PUBLIC_URL>/auth/callback` erlauben |
| OIDC: `mismatching_state` | Sitzungs-Cookie zwischen Weiterleitung und Callback verloren: Host in `PUBLIC_URL` weicht vom Browser ab, oder `https` ohne `Secure`/Proxy-Header |
| OIDC: „Konto … darf diese Anwendung nicht verwenden" | Benutzer nicht in `OIDC_ALLOWED_GROUPS`; prüfen, ob der Provider den Claim `groups` mitschickt |
+26 -26
View File
@@ -1,42 +1,42 @@
# Reverse proxy
# Reverse Proxy
The container serves plain HTTP on port 8080 (published as `127.0.0.1:8080` in the
example). Put a reverse proxy with TLS in front of it and set in `.env`:
Der Container liefert reines HTTP auf Port 8080 (im Beispiel veröffentlicht als
`127.0.0.1:8080`). Davor gehört ein Reverse Proxy mit TLS; in der `.env` dazu setzen:
```ini
PUBLIC_URL=https://fail2ban.example.org
TRUSTED_PROXY_HOPS=1
```
The proxy must pass `Host` unchanged and set `X-Forwarded-For` and `X-Forwarded-Proto` –
all examples below do. Restricting access to known IP addresses at the proxy is a cheap
extra layer and strongly recommended.
Der Proxy muss `Host` unverändert weitergeben und `X-Forwarded-For` sowie `X-Forwarded-Proto`
setzen – alle Beispiele unten tun das. Den Zugriff am Proxy zusätzlich auf bekannte
IP-Adressen zu beschränken kostet wenig und ist dringend empfohlen.
## Nginx Proxy Manager
**Hosts → Proxy Hosts → Add Proxy Host**
| Field | Value |
| Feld | Wert |
|---|---|
| Domain Names | `fail2ban.example.org` |
| Scheme / Forward Hostname / Port | `http` · IP of the Docker host (or `127.0.0.1` if NPM runs with host networking) · `8080` |
| Block Common Exploits | on |
| SSL | request a certificate, *Force SSL*, *HTTP/2*, *HSTS* |
| Access List | optional: an access list with your IP addresses (*Satisfy any* off) |
| Scheme / Forward Hostname / Port | `http` · IP des Docker-Hosts (oder `127.0.0.1`, wenn NPM im Host-Netzwerk läuft) · `8080` |
| Block Common Exploits | an |
| SSL | Zertifikat anfordern, *Force SSL*, *HTTP/2*, *HSTS* |
| Access List | optional: eine Access List mit deinen IP-Adressen (*Satisfy any* aus) |
If NPM runs in another Docker network on the same host, bind the UI port to the host's
LAN address (e.g. `"192.168.1.10:8080:8080"`) or attach the UI to NPM's network and use
`fail2ban-webui:8080` as target.
Läuft NPM auf demselben Host in einem anderen Docker-Netz, den Port der Oberfläche an die
LAN-Adresse des Hosts binden (z. B. `"192.168.1.10:8080:8080"`) oder die Oberfläche an das
Netz von NPM hängen und `fail2ban-webui:8080` als Ziel verwenden.
Response headers belong into **Advanced → Custom Nginx Configuration**; NPM's image
includes the *headers-more* module, which also applies them to proxied responses:
Antwort-Header gehören nach **Advanced → Custom Nginx Configuration**; das NPM-Image enthält
das Modul *headers-more*, das sie auch auf weitergeleitete Antworten anwendet:
```nginx
more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains";
```
(The app sets CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy` and
`Cache-Control` itself.)
(CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy` und `Cache-Control`
setzt die App selbst.)
## Caddy
@@ -50,11 +50,11 @@ fail2ban.example.org {
}
```
Caddy sets `X-Forwarded-For`/`-Proto` automatically.
Caddy setzt `X-Forwarded-For`/`-Proto` automatisch.
## Traefik (Docker labels)
## Traefik (Docker-Labels)
Attach the container to Traefik's network and add labels instead of publishing the port:
Den Container an das Netz von Traefik hängen und Labels setzen, statt den Port zu veröffentlichen:
```yaml
services:
@@ -73,7 +73,7 @@ services:
- traefik
```
### Traefik with Authentik forward auth (proxy mode)
### Traefik mit Authentik Forward Auth (Proxy-Modus)
```yaml
- traefik.http.routers.f2bwebui.middlewares=authentik@docker
@@ -110,12 +110,12 @@ server {
}
```
## Check
## Prüfen
```bash
curl -sI https://fail2ban.example.org/login | grep -iE '^(HTTP|content-security|set-cookie)'
```
Expect `HTTP/2 200`, the `Content-Security-Policy` header and a session cookie with
`Secure; HttpOnly; SameSite=Lax`. A cookie without `Secure` means `PUBLIC_URL` does not
start with `https://`.
Erwartet werden `HTTP/2 200`, der Header `Content-Security-Policy` und ein Sitzungs-Cookie
mit `Secure; HttpOnly; SameSite=Lax`. Fehlt `Secure` am Cookie, beginnt `PUBLIC_URL` nicht
mit `https://`.