Skip to content

Jamkris/Bastion

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

35 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ›ก Bastion

CI License: MIT Python

A lightweight fail2ban + nftables security dashboard for a single self-hosted server. See your firewall state, ban and unban IPs, manage an allowlist, and get push alerts on attack spikes โ€” all from the browser, in one small container.

English ยท ํ•œ๊ตญ์–ด


Features

Observability

  • Banned IPs โ€” per-jail fail2ban ban list with country flags (GeoIP)
  • Top attackers โ€” failed-login aggregation from auth.log / journald
  • Open ports โ€” listening sockets from ss -tlnp with the owning process
  • Firewall rules โ€” chains, rules and sets from nft -j list ruleset
  • Client-side search/filter and click-to-sort on every table; 30s live refresh (HTMX)
  • Trend sparklines on the home page for banned IPs and attackers (recorded over time; also available as JSON at GET /api/history)

Management

  • Ban / unban any IP through fail2ban-client, with SweetAlert2 confirmations
  • Bulk ban selected or all Top Attackers in one action
  • Allowlist โ€” add/remove trusted IPs (or CIDRs) in an nftables set

Alerting

  • ntfy push notifications on a ban spike (configurable threshold + window), on new attacker IPs, and on open-port changes โ€” each independently toggleable, with a one-click Send test button

Security & UX

  • Single-password login gate (HMAC-signed session cookie)
  • Login rate limiting per client IP (brute-force protection)
  • Settings page to configure notifications, allowlist target, and rate limits at runtime โ€” no redeploy needed
  • English / Korean UI, switchable from the header (persisted via cookie)

Every input is strictly validated (ipaddress + name allowlists), commands run as argv lists (never a shell string), and all write actions are audit-logged. Write actions require authentication.

Architecture

runner.py     run one command -> stdout       (fail2ban-client, nft, ss)
services/     raw text -> parse -> dataclass   (pure functions, fully tested)
prefs.py      runtime user preferences         (JSON on disk, layered on env)
web/          FastAPI + HTMX pages + JSON API

All logic lives in services/ / prefs.py, so the HTMX pages and the JSON API reuse the same code. Parsers are pure functions tested against real-server fixtures.

Install โ€” Docker (recommended)

cp .env.example .env       # set BASTION_AUTH_PASSWORD (recommended)
docker compose up -d --build

Reading the host firewall requires network_mode: host + cap_add: NET_ADMIN, plus mounts for the fail2ban socket and /var/log (all in the compose file). A named volume bastion_data persists your Settings-page preferences across restarts. The app listens on :8009.

Security: this is an admin tool โ€” never expose it to the internet. Put it behind Cloudflare Access or a VPN (e.g. wg-easy).

Install โ€” systemd (bare metal)

The same code runs without a container.

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
BASTION_SUDO=true .venv/bin/uvicorn bastion.web.app:app --host 127.0.0.1 --port 8009

BASTION_SUDO=true prefixes commands with sudo. Add a NOPASSWD allowlist in sudoers for the exact commands Bastion runs (nft, fail2ban-client).

Settings (configured in-app)

Open โš™ Settings in the sidebar. Changes are written to data/prefs.json and take effect immediately.

  • Notifications (ntfy) โ€” enable alerts, set the ntfy server URL, topic, and an optional access token, and tune the ban-spike threshold and window. Use Send test to verify delivery. A background poller checks fail2ban every 60s and pushes one alert when the threshold is crossed within the window.
  • Allowlist โ€” two modes:
    • fail2ban ignoreip (default) โ€” trusted IPs fail2ban will never ban, managed per jail over the fail2ban socket. Works with any firewall (UFW/iptables/nftables). Changes apply live; enable Persist to jail.local in Settings to also write them to jail.local so they survive a fail2ban restart (mount it read-write).
    • nftables set โ€” manage IPs in a dedicated nftables set (family / table / set). The set must exist in your ruleset (a first-run guide + Create set button helps). Manage entries on the โœ… Allowlist page either way.
  • Login security โ€” max failed attempts and the lockout window for per-IP login rate limiting.

Authentication

Three modes, in order of precedence:

  1. Multi-user โ€” add accounts under Settings โ†’ Users; login is then username + password (PBKDF2-hashed, stored in data/users.json; sessions are signed with a key at data/.secret or BASTION_SECRET_KEY).
  2. Single password โ€” with no users but BASTION_AUTH_PASSWORD set, the whole dashboard is gated behind one shared password (HMAC-signed cookie).
  3. Open โ€” neither configured; a warning is logged on startup.

Repeated failures from one IP are locked out (see Login security above). /healthz, /login and static assets stay public. Header-only clients can use HTTP Basic auth (username + password in multi-user mode, or any username + the shared password otherwise). Keep the app behind HTTPS regardless.

GeoIP (country flags)

Docker builds automatically download the free DB-IP IP-to-Country Lite database (CC BY 4.0, no account) and set BASTION_GEOIP_DB, so country flags work out of the box. To refresh, rebuild the image. For bare metal:

scripts/download-geoip.sh /path/to/dbip-country-lite.mmdb
export BASTION_GEOIP_DB=/path/to/dbip-country-lite.mmdb

MaxMind GeoLite2-Country works too (same .mmdb format). IP geolocation data ยฉ DB-IP (CC BY 4.0).

JSON API & dashboard widgets

Bastion exposes a small JSON API (also behind auth). GET /api/stats returns flat counters handy for dashboards:

{ "total_banned": 12, "jail_count": 2, "open_ports": 9, "attackers": 34 }

Also available: /api/banned, /api/attackers, /api/ports (each {data, error}) and /api/history?limit=N (recorded counter time-series).

Header-only clients can't do the cookie login, so the API also accepts HTTP Basic auth โ€” any username, the password being BASTION_AUTH_PASSWORD.

Example Homepage customapi widget showing three numbers:

widget:
  type: customapi
  url: http://<bastion-host>:8009/api/stats
  refreshInterval: 30000
  username: homepage           # ignored; any value
  password: <BASTION_AUTH_PASSWORD>   # omit both if auth is disabled
  mappings:
    - field: total_banned
      label: Banned
      format: number
    - field: attackers
      label: Attackers
      format: number
    - field: open_ports
      label: Open ports
      format: number

Demo mode

Set BASTION_DEMO=true to serve sample data instead of running commands โ€” handy for previewing the UI on a machine without the Linux CLIs (e.g. macOS).

Configuration (environment variables)

Infrastructure knobs only โ€” everything else lives in the Settings page.

Variable Default Description
BASTION_AUTH_PASSWORD (empty) Login password; empty = open
BASTION_SUDO false Prefix commands with sudo (bare metal)
BASTION_DEMO false Serve sample data
BASTION_JAILS (auto) Jails to query, comma-separated
BASTION_GEOIP_DB (auto in Docker) Path to a DB-IP/GeoLite2 .mmdb
BASTION_AUTH_LOG /var/log/auth.log Auth log path
BASTION_DATA_DIR data Where prefs.json is stored

Development

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest httpx
.venv/bin/python -m pytest      # pure logic + fixtures, no live server needed

Tests are isolated from any local .env (see tests/conftest.py).

License

MIT


๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ์–ด

๋‹จ์ผ ์ž์ฒด ํ˜ธ์ŠคํŒ… ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ ๊ฐ€๋ฒผ์šด fail2ban + nftables ๋ณด์•ˆ ๋Œ€์‹œ๋ณด๋“œ์ž…๋‹ˆ๋‹ค. ๋ฐฉํ™”๋ฒฝ ์ƒํƒœ ํ™•์ธ, IP ์ฐจ๋‹จ/ํ•ด์ œ, ํ—ˆ์šฉ ๋ชฉ๋ก ๊ด€๋ฆฌ, ๊ณต๊ฒฉ ๊ธ‰์ฆ ์‹œ ํ‘ธ์‹œ ์•Œ๋ฆผ๊นŒ์ง€ โ€” ๋ชจ๋‘ ๋ธŒ๋ผ์šฐ์ €์—์„œ, ํ•˜๋‚˜์˜ ์ž‘์€ ์ปจํ…Œ์ด๋„ˆ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ๊ธฐ๋Šฅ

  • ๊ด€์ธก โ€” ์ฐจ๋‹จ๋œ IP(๊ตญ๊ฐ€ ํ”Œ๋ž˜๊ทธ ํฌํ•จ), ์ž์ฃผ ๊ณต๊ฒฉํ•˜๋Š” IP, ์—ด๋ฆฐ ํฌํŠธ, ๋ฐฉํ™”๋ฒฝ ๊ทœ์น™
  • ๊ด€๋ฆฌ โ€” fail2ban ๊ธฐ๋ฐ˜ IP ์ฐจ๋‹จ/ํ•ด์ œ, ์„ ํƒยท์ „์ฒด ์ผ๊ด„ ์ฐจ๋‹จ, nftables ํ—ˆ์šฉ ๋ชฉ๋ก ์ถ”๊ฐ€/์‚ญ์ œ
  • ์•Œ๋ฆผ โ€” ์ฐจ๋‹จ ๊ธ‰์ฆ ์‹œ ntfy ํ‘ธ์‹œ(์ž„๊ณ„๊ฐ’ยท๊ธฐ๊ฐ„ ์„ค์ • ๊ฐ€๋Šฅ, ํ…Œ์ŠคํŠธ ์ „์†ก ๋ฒ„ํŠผ ์ œ๊ณต)
  • ๋ณด์•ˆ/UX โ€” ๋‹จ์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊ทธ์ธ, IP๋ณ„ ๋กœ๊ทธ์ธ ์‹œ๋„ ์ œํ•œ(๋ฌด์ฐจ๋ณ„ ๋Œ€์ž… ๋ฐฉ์–ด), ๋Ÿฐํƒ€์ž„ ์„ค์ • ํŽ˜์ด์ง€, ์˜์–ด/ํ•œ๊ตญ์–ด UI

๋ชจ๋“  ์ž…๋ ฅ์€ ์—„๊ฒฉํžˆ ๊ฒ€์ฆ๋˜๊ณ (ipaddress + ์ด๋ฆ„ ํ—ˆ์šฉ ๋ชฉ๋ก), ๋ช…๋ น์€ ์…ธ ๋ฌธ์ž์—ด์ด ์•„๋‹Œ argv ๋ฆฌ์ŠคํŠธ๋กœ ์‹คํ–‰๋˜๋ฉฐ, ์“ฐ๊ธฐ ์ž‘์—…์€ ๊ฐ์‚ฌ ๋กœ๊ทธ์— ๋‚จ์Šต๋‹ˆ๋‹ค. ์“ฐ๊ธฐ ์ž‘์—…์€ ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์„ค์น˜ โ€” Docker (๊ถŒ์žฅ)

cp .env.example .env       # BASTION_AUTH_PASSWORD ์„ค์ • ๊ถŒ์žฅ
docker compose up -d --build

ํ˜ธ์ŠคํŠธ ๋ฐฉํ™”๋ฒฝ์„ ์ฝ์œผ๋ ค๋ฉด network_mode: host + cap_add: NET_ADMIN, ๊ทธ๋ฆฌ๊ณ  fail2ban ์†Œ์ผ“๊ณผ /var/log ๋งˆ์šดํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค(compose ํŒŒ์ผ์— ํฌํ•จ). ์„ค์ • ํŽ˜์ด์ง€์˜ ํ™˜๊ฒฝ์„ค์ •์€ bastion_data ๋ณผ๋ฅจ์— ์ €์žฅ๋˜์–ด ์žฌ์‹œ์ž‘ ํ›„์—๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ์•ฑ์€ :8009 ํฌํŠธ์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

๋ณด์•ˆ: ๊ด€๋ฆฌ ๋„๊ตฌ์ด๋ฏ€๋กœ ์ ˆ๋Œ€ ์ธํ„ฐ๋„ท์— ์ง์ ‘ ๋…ธ์ถœํ•˜์ง€ ๋งˆ์„ธ์š”. Cloudflare Access๋‚˜ VPN(wg-easy ๋“ฑ) ๋’ค์— ๋‘์„ธ์š”.

์„ค์ • ํŽ˜์ด์ง€

์‚ฌ์ด๋“œ๋ฐ”์˜ โš™ ์„ค์ •์—์„œ ์•Œ๋ฆผ(ntfy), ํ—ˆ์šฉ ๋ชฉ๋ก ๋Œ€์ƒ ์…‹, ๋กœ๊ทธ์ธ ์‹œ๋„ ์ œํ•œ์„ ๋Ÿฐํƒ€์ž„์— ๋ฐ”๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ data/prefs.json์— ์ €์žฅ๋˜๋ฉฐ ์ฆ‰์‹œ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

  • ์•Œ๋ฆผ(ntfy): ntfy ์„œ๋ฒ„ ์ฃผ์†Œยทํ† ํ”ฝยทํ† ํฐ๊ณผ ์ฐจ๋‹จ ๊ธ‰์ฆ ์ž„๊ณ„๊ฐ’/๊ธฐ๊ฐ„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํด๋Ÿฌ๊ฐ€ 60์ดˆ๋งˆ๋‹ค fail2ban์„ ํ™•์ธํ•ด ์ž„๊ณ„๊ฐ’ ์ดˆ๊ณผ ์‹œ ํ•œ ๋ฒˆ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.
  • ํ—ˆ์šฉ ๋ชฉ๋ก: ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹
    • fail2ban ignoreip(๊ธฐ๋ณธ): fail2ban์ด ์ ˆ๋Œ€ ์ฐจ๋‹จํ•˜์ง€ ์•Š๋Š” ์‹ ๋ขฐ IP๋ฅผ jail๋ณ„๋กœ ๊ด€๋ฆฌ. UFW/iptables/nftables ์–ด๋–ค ๋ฐฉํ™”๋ฒฝ์ด๋“  ๋™์ž‘. ๋ณ€๊ฒฝ์€ ์ฆ‰์‹œ ์ ์šฉ๋˜์ง€๋งŒ jail.local์—๋Š” ์ €์žฅ๋˜์ง€ ์•Š์œผ๋‹ˆ ์˜๊ตฌ ํ•ญ๋ชฉ์€ ๊ฑฐ๊ธฐ์— ์ถ”๊ฐ€ํ•˜์„ธ์š”.
    • nftables ์…‹: ์ „์šฉ nftables ์…‹(family/table/set)์˜ IP๋ฅผ ๊ด€๋ฆฌ. ์…‹์€ ruleset์— ์ด๋ฏธ ์กด์žฌํ•ด์•ผ ํ•˜๋ฉฐ(์—†์œผ๋ฉด ์•ˆ๋‚ด + ์…‹ ์ƒ์„ฑ ๋ฒ„ํŠผ ์ œ๊ณต).
  • ๋กœ๊ทธ์ธ ๋ณด์•ˆ: ์ตœ๋Œ€ ์‹คํŒจ ํšŸ์ˆ˜์™€ ์ž ๊ธˆ ๊ธฐ๊ฐ„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

์ธ์ฆ

BASTION_AUTH_PASSWORD๋ฅผ ์„ค์ •ํ•˜๋ฉด ์ „์ฒด ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ๋‹จ์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊ทธ์ธ์œผ๋กœ ๋ณดํ˜ธ๋ฉ๋‹ˆ๋‹ค (HMAC ์„œ๋ช… ์ฟ ํ‚ค ์„ธ์…˜). ํ•œ IP์—์„œ ์‹คํŒจ๊ฐ€ ๋ฐ˜๋ณต๋˜๋ฉด ์ž ๊น๋‹ˆ๋‹ค. ๋น„์›Œ๋‘๋ฉด ๊ฐœ๋ฐฉ ๋ชจ๋“œ๋กœ ์‹คํ–‰๋˜๋ฉฐ ์‹œ์ž‘ ์‹œ ๊ฒฝ๊ณ ๊ฐ€ ๋กœ๊ทธ์— ๋‚จ์Šต๋‹ˆ๋‹ค. /healthz, /login, ์ •์  ์ž์‚ฐ์€ ๊ณต๊ฐœ์ž…๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest httpx
.venv/bin/python -m pytest

๋ผ์ด์„ ์Šค: MIT

About

Self-hosted fail2ban & nftables security dashboard

Topics

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages