diff --git a/.gitignore b/.gitignore index 89863b7..ea32dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -93,7 +93,15 @@ Thumbs.db *~ # ── Docker dev config ───────────────────────── -Docker/fail2ban-dev-config/ +# Ignore auto-generated linuxserver/fail2ban config files, +# but track our custom filter, jail, and documentation. +Docker/fail2ban-dev-config/** +!Docker/fail2ban-dev-config/README.md +!Docker/fail2ban-dev-config/fail2ban/ +!Docker/fail2ban-dev-config/fail2ban/filter.d/ +!Docker/fail2ban-dev-config/fail2ban/filter.d/bangui-sim.conf +!Docker/fail2ban-dev-config/fail2ban/jail.d/ +!Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-sim.conf # ── Misc ────────────────────────────────────── *.log diff --git a/Docker/check_ban_status.sh b/Docker/check_ban_status.sh new file mode 100644 index 0000000..74a10f1 --- /dev/null +++ b/Docker/check_ban_status.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────── +# check_ban_status.sh +# +# Queries the bangui-sim jail inside the running fail2ban +# container and optionally unbans a specific IP. +# +# Usage: +# bash Docker/check_ban_status.sh +# bash Docker/check_ban_status.sh --unban 192.168.100.99 +# +# Requirements: +# The bangui-fail2ban-dev container must be running. +# (docker compose -f Docker/compose.debug.yml up -d fail2ban) +# ────────────────────────────────────────────────────────────── + +set -euo pipefail + +readonly CONTAINER="bangui-fail2ban-dev" +readonly JAIL="bangui-sim" + +# ── Helper: run a fail2ban-client command inside the container ─ +f2b() { + docker exec "${CONTAINER}" fail2ban-client "$@" +} + +# ── Parse arguments ─────────────────────────────────────────── +UNBAN_IP="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --unban) + if [[ -z "${2:-}" ]]; then + echo "ERROR: --unban requires an IP address argument." >&2 + exit 1 + fi + UNBAN_IP="$2" + shift 2 + ;; + *) + echo "ERROR: Unknown argument: '$1'" >&2 + echo "Usage: $0 [--unban ]" >&2 + exit 1 + ;; + esac +done + +# ── Unban mode ──────────────────────────────────────────────── +if [[ -n "${UNBAN_IP}" ]]; then + echo "Unbanning ${UNBAN_IP} from jail '${JAIL}' ..." + f2b set "${JAIL}" unbanip "${UNBAN_IP}" + echo "Done. '${UNBAN_IP}' has been removed from the ban list." + echo "" +fi + +# ── Jail status ─────────────────────────────────────────────── +echo "═══════════════════════════════════════════" +echo " Jail status: ${JAIL}" +echo "═══════════════════════════════════════════" +f2b status "${JAIL}" + +# ── Banned IPs with timestamps ──────────────────────────────── +echo "" +echo "═══════════════════════════════════════════" +echo " Banned IPs with timestamps: ${JAIL}" +echo "═══════════════════════════════════════════" +f2b get "${JAIL}" banip --with-time || echo "(no IPs currently banned)" diff --git a/Docker/compose.debug.yml b/Docker/compose.debug.yml index d0e32c3..4e185c0 100644 --- a/Docker/compose.debug.yml +++ b/Docker/compose.debug.yml @@ -34,6 +34,7 @@ services: - ./fail2ban-dev-config:/config - fail2ban-dev-run:/var/run/fail2ban - /var/log:/var/log:ro + - ./logs:/remotelogs/bangui healthcheck: test: ["CMD", "fail2ban-client", "ping"] interval: 15s diff --git a/Docker/fail2ban-dev-config/README.md b/Docker/fail2ban-dev-config/README.md new file mode 100644 index 0000000..e23e31f --- /dev/null +++ b/Docker/fail2ban-dev-config/README.md @@ -0,0 +1,141 @@ +# BanGUI — Fail2ban Dev Test Environment + +This directory contains the fail2ban configuration and supporting scripts for a +self-contained development test environment. A simulation script writes fake +authentication-failure log lines, fail2ban detects them via the `bangui-sim` +jail, and bans the offending IP — giving a fully reproducible ban/unban cycle +without a real service. + +--- + +## Prerequisites + +- Docker or Podman installed and running. +- `docker compose` (v2) or `podman-compose` available on the `PATH`. +- The repo checked out; all commands run from the **repo root**. + +--- + +## Quick Start + +### 1 — Start the fail2ban container + +```bash +docker compose -f Docker/compose.debug.yml up -d fail2ban +# or: make up (starts the full dev stack) +``` + +Wait ~15 s for the health-check to pass (`docker ps` shows `healthy`). + +### 2 — Run the login-failure simulation + +```bash +bash Docker/simulate_failed_logins.sh +``` + +Default: writes **5** failure lines for IP `192.168.100.99` to +`Docker/logs/auth.log`. +Optional overrides: + +```bash +bash Docker/simulate_failed_logins.sh +# e.g. bash Docker/simulate_failed_logins.sh 10 203.0.113.42 +``` + +### 3 — Verify the IP was banned + +```bash +bash Docker/check_ban_status.sh +``` + +The output shows the current jail counters and the list of banned IPs with their +ban expiry timestamps. + +### 4 — Unban and re-test + +```bash +bash Docker/check_ban_status.sh --unban 192.168.100.99 +``` + +### One-command smoke test (Makefile shortcut) + +```bash +make dev-ban-test +``` + +Chains steps 1–3 automatically with appropriate sleep intervals. + +--- + +## Configuration Reference + +| File | Purpose | +|------|---------| +| `fail2ban/filter.d/bangui-sim.conf` | Defines the `failregex` that matches simulation log lines | +| `fail2ban/jail.d/bangui-sim.conf` | Jail settings: `maxretry=3`, `bantime=60s`, `findtime=120s` | +| `Docker/logs/auth.log` | Log file written by the simulation script (host path) | + +Inside the container the log file is mounted at `/remotelogs/bangui/auth.log` +(see `fail2ban/paths-lsio.conf` — `remote_logs_path = /remotelogs`). + +To change sensitivity, edit `fail2ban/jail.d/bangui-sim.conf`: + +```ini +maxretry = 3 # failures before a ban +findtime = 120 # look-back window in seconds +bantime = 60 # ban duration in seconds +``` + +--- + +## Troubleshooting + +### Log file not detected + +The jail uses `backend = polling` for reliability inside Docker containers. +If fail2ban still does not pick up new lines, verify the volume mount in +`Docker/compose.debug.yml`: + +```yaml +- ./logs:/remotelogs/bangui +``` + +and confirm `Docker/logs/auth.log` exists after running the simulation script. + +### Filter regex mismatch + +Test the regex manually: + +```bash +docker exec bangui-fail2ban-dev \ + fail2ban-regex /remotelogs/bangui/auth.log bangui-sim +``` + +The output should show matched lines. If nothing matches, check that the log +lines produced by `simulate_failed_logins.sh` match this pattern exactly: + +``` +YYYY-MM-DD HH:MM:SS bangui-auth: authentication failure from +``` + +### iptables / permission errors + +The fail2ban container requires `NET_ADMIN` and `NET_RAW` capabilities and +`network_mode: host`. Both are already set in `Docker/compose.debug.yml`. If +you see iptables errors, check that the host kernel has iptables loaded: + +```bash +sudo modprobe ip_tables +``` + +### IP not banned despite enough failures + +Check whether the source IP falls inside the `ignoreip` range defined in +`fail2ban/jail.d/bangui-sim.conf`: + +```ini +ignoreip = 127.0.0.0/8 ::1 172.16.0.0/12 +``` + +The default simulation IP `192.168.100.99` is outside these ranges and will be +banned normally. diff --git a/Docker/fail2ban-dev-config/fail2ban/filter.d/bangui-sim.conf b/Docker/fail2ban-dev-config/fail2ban/filter.d/bangui-sim.conf new file mode 100644 index 0000000..275b83f --- /dev/null +++ b/Docker/fail2ban-dev-config/fail2ban/filter.d/bangui-sim.conf @@ -0,0 +1,12 @@ +# ────────────────────────────────────────────────────────────── +# BanGUI — Simulated authentication failure filter +# +# Matches lines written by Docker/simulate_failed_logins.sh +# Format: bangui-auth: authentication failure from +# ────────────────────────────────────────────────────────────── + +[Definition] + +failregex = ^.* bangui-auth: authentication failure from \s*$ + +ignoreregex = diff --git a/Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-sim.conf b/Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-sim.conf new file mode 100644 index 0000000..59cb310 --- /dev/null +++ b/Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-sim.conf @@ -0,0 +1,20 @@ +# ────────────────────────────────────────────────────────────── +# BanGUI — Simulated authentication failure jail +# +# Watches Docker/logs/auth.log (mounted at /remotelogs/bangui) +# for lines produced by Docker/simulate_failed_logins.sh. +# ────────────────────────────────────────────────────────────── + +[bangui-sim] + +enabled = true +filter = bangui-sim +logpath = /remotelogs/bangui/auth.log +backend = polling +maxretry = 3 +findtime = 120 +bantime = 60 +banaction = iptables-allports + +# Never ban localhost, the Docker bridge network, or the host machine. +ignoreip = 127.0.0.0/8 ::1 172.16.0.0/12 diff --git a/Docker/simulate_failed_logins.sh b/Docker/simulate_failed_logins.sh new file mode 100644 index 0000000..3a01691 --- /dev/null +++ b/Docker/simulate_failed_logins.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────── +# simulate_failed_logins.sh +# +# Writes synthetic authentication-failure log lines to a file +# that matches the bangui-sim fail2ban filter. +# +# Usage: +# bash Docker/simulate_failed_logins.sh [COUNT] [SOURCE_IP] [LOG_FILE] +# +# Defaults: +# COUNT : 5 +# SOURCE_IP: 192.168.100.99 +# LOG_FILE : Docker/logs/auth.log (relative to repo root) +# +# Log line format (must match bangui-sim failregex exactly): +# YYYY-MM-DD HH:MM:SS bangui-auth: authentication failure from +# ────────────────────────────────────────────────────────────── + +set -euo pipefail + +# ── Defaults ────────────────────────────────────────────────── +readonly DEFAULT_COUNT=5 +readonly DEFAULT_IP="192.168.100.99" + +# Resolve script location so defaults work regardless of cwd. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly DEFAULT_LOG_FILE="${SCRIPT_DIR}/logs/auth.log" + +# ── Arguments ───────────────────────────────────────────────── +COUNT="${1:-${DEFAULT_COUNT}}" +SOURCE_IP="${2:-${DEFAULT_IP}}" +LOG_FILE="${3:-${DEFAULT_LOG_FILE}}" + +# ── Validate COUNT is a positive integer ────────────────────── +if ! [[ "${COUNT}" =~ ^[1-9][0-9]*$ ]]; then + echo "ERROR: COUNT must be a positive integer, got: '${COUNT}'" >&2 + exit 1 +fi + +# ── Ensure log directory exists ─────────────────────────────── +LOG_DIR="$(dirname "${LOG_FILE}")" +mkdir -p "${LOG_DIR}" + +# ── Write failure lines ─────────────────────────────────────── +echo "Writing ${COUNT} authentication-failure line(s) for ${SOURCE_IP} to ${LOG_FILE} ..." + +for ((i = 1; i <= COUNT; i++)); do + TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" + printf '%s bangui-auth: authentication failure from %s\n' \ + "${TIMESTAMP}" "${SOURCE_IP}" >> "${LOG_FILE}" + sleep 0.5 +done + +# ── Summary ─────────────────────────────────────────────────── +echo "Done." +echo " Lines written : ${COUNT}" +echo " Source IP : ${SOURCE_IP}" +echo " Log file : ${LOG_FILE}" diff --git a/Docs/Tasks.md b/Docs/Tasks.md index e66167c..99fd2a0 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -4,14 +4,122 @@ This document breaks the entire BanGUI project into development stages, ordered --- -## ✅ FIXED — Access list 404: `/api/api/dashboard/accesses` double prefix (2026-03-01) +## Stage 0 — Fail2ban Dev Test Environment -**Root cause:** `fetchAccesses` in `src/api/dashboard.ts` passed the hardcoded absolute path -`/api/dashboard/accesses` directly to `get()`. Because `get()` prepends `BASE_URL` (`/api`), -the resulting URL became `/api/api/dashboard/accesses`, which has no backend route. +**Goal:** Set up a self-contained test environment where a script simulates failed login attempts, those attempts are written to a log file in `Docker/logs/`, fail2ban monitors that log and bans the offending IP. This provides a reproducible way to test the ban/unban lifecycle without a real service. -**Fix:** Added `dashboardAccesses: "/dashboard/accesses"` to `ENDPOINTS` in `src/api/endpoints.ts` -and changed `fetchAccesses` to use `ENDPOINTS.dashboardAccesses` — consistent with every other -function in the same file. +**Status: ✅ Complete** — all tasks 0.1–0.8 implemented. -**Commit:** _Fix double /api prefix in fetchAccesses by using ENDPOINTS constant_ \ No newline at end of file +--- + +### Task 0.1 — Create the custom fail2ban filter + +Create a new filter definition file at `Docker/fail2ban-dev-config/fail2ban/filter.d/bangui-sim.conf`. This filter must define a `failregex` that matches the log lines the simulation script will produce. Use a simple, deterministic log format such as: + +``` + bangui-auth: authentication failure from +``` + +The filter file should contain: + +- A `[Definition]` section. +- A `failregex` line that captures `` from lines matching the format above (e.g. `^.* bangui-auth: authentication failure from \s*$`). +- An empty `ignoreregex`. + +**Reference:** existing filters in `Docker/fail2ban-dev-config/fail2ban/filter.d/` for syntax examples. + +--- + +### Task 0.2 — Create a jail definition for the simulated service + +Create `Docker/fail2ban-dev-config/fail2ban/jail.d/bangui-sim.conf` (or add to a `.local` file). This jail must: + +- Set `enabled = true`. +- Reference the filter from Task 0.1 (`filter = bangui-sim`). +- Point `logpath` to the log file inside the container. The log file in `Docker/logs/` will be mounted into the fail2ban container; choose a mount target such as `/remotelogs/bangui/auth.log` and set `logpath` accordingly. +- Use a low `maxretry` (e.g. `3`) and a short `bantime` (e.g. `60` seconds) so testing is fast. +- Set `findtime` to something reasonable (e.g. `120` seconds). +- Set `banaction` to `iptables-allports` (or whichever action is appropriate for the container — the linuxserver/fail2ban image supports iptables). +- Optionally set `backend = auto` (pyinotify/polling). + +**Reference:** existing jails in `Docker/fail2ban-dev-config/fail2ban/jail.d/` and `jail.conf`. + +--- + +### Task 0.3 — Mount `Docker/logs/` into the fail2ban container + +Update `Docker/compose.debug.yml` so the fail2ban service mounts the host directory `./logs` into the container at a path that matches the `logpath` configured in Task 0.2 (e.g. `/remotelogs/bangui`). Add an entry like: + +```yaml +volumes: + - ./logs:/remotelogs/bangui +``` + +Make sure the path is consistent with `paths-lsio.conf` (`remote_logs_path = /remotelogs`). If you chose a different mount target, adjust the jail's `logpath` to match. + +--- + +### Task 0.4 — Create the failed-login simulation script + +Create a script at `Docker/simulate_failed_logins.sh` (bash). The script must: + +1. Accept optional arguments: number of failed attempts (default `5`), source IP to simulate (default `192.168.100.99`), and target log file path (default `./logs/auth.log`). +2. Create the log directory if it does not exist. +3. In a loop, append lines to the log file matching the exact format the filter expects, e.g.: + ``` + 2026-03-03 12:00:01 bangui-auth: authentication failure from 192.168.100.99 + ``` + Use the current timestamp for each line (via `date`). Sleep briefly between writes (e.g. 0.5 s) so the timestamps differ. +4. After all lines are written, print a summary: how many failure lines were written, which IP, and which file. +5. Mark the script as executable (`chmod +x`). + +**Reference:** the `failregex` in Task 0.1 — the log format must match exactly. + +--- + +### Task 0.5 — Create a verification / status-check script + +Create `Docker/check_ban_status.sh` that: + +1. Runs `docker exec bangui-fail2ban-dev fail2ban-client status bangui-sim` to show the jail status (current failures, banned IPs). +2. Runs `docker exec bangui-fail2ban-dev fail2ban-client get bangui-sim banip --with-time` (or equivalent) to list banned IPs with timestamps. +3. Accepts an optional `--unban ` flag that calls `docker exec bangui-fail2ban-dev fail2ban-client set bangui-sim unbanip ` so the tester can quickly reset. +4. Mark the script as executable. + +--- + +### Task 0.6 — Add an `ignoreip` safeguard + +In the jail created in Task 0.2, add `ignoreip = 127.0.0.0/8 ::1` (and optionally the Docker bridge subnet) so that the host machine and localhost are never accidentally banned during development. + +--- + +### Task 0.7 — End-to-end manual test & documentation + +Write a brief section in `Docker/fail2ban-dev-config/README.md` that documents: + +1. **Prerequisites:** Docker / Podman running, compose available. +2. **Quick start:** + - `docker compose -f Docker/compose.debug.yml up -d fail2ban` (start only the fail2ban service). + - `bash Docker/simulate_failed_logins.sh` (run the simulation). + - `bash Docker/check_ban_status.sh` (verify the IP was banned). + - `bash Docker/check_ban_status.sh --unban 192.168.100.99` (unban and re-test). +3. **Configuration reference:** where the filter, jail, and log file live; how to change `maxretry`, `bantime`, etc. +4. **Troubleshooting:** common issues (log file not mounted, filter regex mismatch, container not seeing file changes — suggest `backend = polling` if inotify doesn't work inside Docker). + +--- + +### Task 0.8 — (Optional) Add a Makefile target + +Add a `dev-ban-test` target to the top-level `Makefile` that chains the workflow: + +```makefile +dev-ban-test: + docker compose -f Docker/compose.debug.yml up -d fail2ban + sleep 5 + bash Docker/simulate_failed_logins.sh + sleep 3 + bash Docker/check_ban_status.sh +``` + +This lets a developer (or CI) run `make dev-ban-test` for a one-command smoke test of the ban pipeline. diff --git a/Makefile b/Makefile index 65a747b..6845123 100644 --- a/Makefile +++ b/Makefile @@ -5,12 +5,13 @@ # Auto-detects which compose binary is available. # # Usage: -# make up — start the debug stack -# make down — stop the debug stack -# make build — (re)build the backend image without starting -# make clean — stop, remove all containers, volumes, and local images -# make logs — tail logs for all debug services -# make restart — restart the debug stack +# make up — start the debug stack +# make down — stop the debug stack +# make build — (re)build the backend image without starting +# make clean — stop, remove all containers, volumes, and local images +# make logs — tail logs for all debug services +# make restart — restart the debug stack +# make dev-ban-test — one-command smoke test of the ban pipeline # ────────────────────────────────────────────────────────────── COMPOSE_FILE := Docker/compose.debug.yml @@ -38,7 +39,7 @@ COMPOSE := $(shell command -v podman-compose 2>/dev/null \ # Detect available container runtime (podman or docker). RUNTIME := $(shell command -v podman 2>/dev/null || echo "docker") -.PHONY: up down build restart logs clean +.PHONY: up down build restart logs clean dev-ban-test ## Start the debug stack (detached). up: @@ -66,3 +67,12 @@ clean: $(RUNTIME) volume rm $(DEV_VOLUMES) 2>/dev/null || true $(RUNTIME) rmi $(DEV_IMAGES) 2>/dev/null || true @echo "All debug volumes and local images removed. Run 'make up' to rebuild and start fresh." + +## One-command smoke test for the ban pipeline: +## 1. Start fail2ban, 2. write failure lines, 3. check ban status. +dev-ban-test: + $(COMPOSE) -f $(COMPOSE_FILE) up -d fail2ban + sleep 5 + bash Docker/simulate_failed_logins.sh + sleep 3 + bash Docker/check_ban_status.sh