Skip to content

Security Architecture

Antonios Voulvoulis edited this page Apr 26, 2026 · 12 revisions

Security Architecture

NFTBan uses Polkit-based privilege separation, FHS auto-heal, and a 3-group access control model. No sudo required for service management.


Table of Contents


Reporting Vulnerabilities

Do NOT report security vulnerabilities through public GitHub issues.

Report via email: security@itcms.gr

See the Security Policy (SECURITY.md) for disclosure timeline and supported versions.


Access Control Model

NFTBan uses a 3-group security model:

Group Purpose Permissions
nftban Admin/Operator - humans, CLI Full access (config, actions, service management via Polkit)
nftban-auditor Read-only - compliance, SOC, reports View only (status, logs, reports - NO actions)
nftban-panel Panel Operator - cPanel, DirectAdmin, Plesk Limited actions (ban/unban, status - NO config, NO restart)

Authorization Hierarchy

CanConfig() → nftban only (configuration changes)
CanAct()    → nftban OR nftban-panel (runtime actions: ban/unban)
CanRead()   → nftban OR nftban-auditor OR nftban-panel (view logs/status)

Deprecated: The nftban-cli group is no longer used. Use nftban for admin access.

nftban Group (Service)

System group for daemon operations and Polkit-based service management.

Service management (no sudo via Polkit):

systemctl start nftban-core.service
systemctl stop nftban-login-monitor.service
systemctl restart nftban-core-feeds.service
systemctl enable nftban-core-geoip.timer
systemctl restart suricata.service
systemctl daemon-reload

Allowed units (whitelist in Polkit rule):

  • nftband.service, nftban-core.service, nftban-core-feeds.service/.timer
  • nftban-core-geoip.service/.timer, nftban-login-monitor.service
  • nftban-health.service/.timer, nftban-health-fix.service
  • nftban-maintenance.service/.timer, nftban-watchdog.service/.timer
  • nftban-queue.service/.timer, nftban-unified-exporter.service/.timer
  • nftban-snapshot.service/.timer, nftban-rollback.service/.timer
  • suricata.service, suricata-update.service/.timer

CLI commands:

nftban status
nftban health summary
nftban stats
nftban list
nftban search 1.2.3.4
nftban feeds list
nftban ban 1.2.3.4
nftban unban 1.2.3.4
nftban feeds enable
nftban geoban block CN
nftban whitelist add 10.0.0.1

Historical note (≤ v1.100.0). Releases up to v1.100.0 also exposed a Web GUI (https://SERVER:3940, PAM-authenticated) for nftban group members — dashboard, ban/unban, whitelist, module configuration, reports. The GUI was retired in v1.100.1b.A and removed entirely by v1.100.1b.D; current releases provide CLI access only. See archive/Web-GUI-and-nftban-ui-retired.

nftban-auditor Group (Read-Only)

Security auditors and compliance officers with read-only access.

File access:

/var/log/nftban/*.log           # read-only
/var/lib/nftban/*               # read-only
/etc/nftban/*.conf              # read-only
/var/lib/nftban/reports/auditors # write allowed (audit reports)

Allowed systemd actions (via Polkit):

systemctl status nftban-core.service    # service status
systemctl show nftban-core.service      # service properties
systemctl is-active nftban-*.service    # check if running
systemctl is-enabled nftban-*.service   # check if enabled
systemctl is-failed nftban-*.service    # check if failed
systemctl list-units 'nftban-*'         # list units
systemctl list-unit-files 'nftban-*'    # list unit files

Allowed CLI commands:

nftban status
nftban health
nftban list
nftban search IP
nftban stats
nftban report
nftban config --view

Cannot: ban/unban, modify config, start/stop/restart services, enable/disable modules.

nftban-panel Group (Panel Integration)

Hosting control panel service accounts (DirectAdmin, cPanel, Plesk, Webmin) with reload-only access.

Panel UI → nftban-panelctl wrapper → Polkit checks → systemctl reload

Security layers: Unix group membership → Polkit authorization (30-nftban-panel.rules) → nftban-panelctl wrapper (command registry RBAC).

Allowed operations:

systemctl status nftban-core.service
systemctl is-active nftban-*.service
systemctl reload nftban-core.service       # reload ONLY
systemctl reload nftban-core-feeds.service # reload ONLY

Cannot: start/stop/restart, enable/disable, modify rules directly, ban/unban, use pkexec.

Permission Matrix

Action nftban nftban-auditor nftban-panel
Start/stop/restart services Yes (Polkit) No No
Reload services Yes (Polkit) No Yes (limited)
Enable/disable services Yes (Polkit) No No
Query service status Yes Yes (Polkit) Yes
Ban/unban IPs Yes No Yes
Modify whitelist Yes No Yes
Read configs Yes Yes Yes
Read logs Yes Yes Yes
Write configs Yes No No
Write audit reports No Yes No
Use nftban CLI Yes Yes (read-only) Yes (limited)

Adding Users

# Admin/Operators (CLI, full access)
sudo usermod -aG nftban username

# Auditors (read-only)
sudo usermod -aG nftban-auditor username

# Panel integration (operator actions only)
sudo usermod -aG nftban-panel panel_account

# Verify
id username

# User must re-login for groups to take effect, or:
newgrp nftban

Setup Examples

Full administrator:

sudo usermod -aG nftban alice
# Alice: all CLI, manage services (Polkit), ban/unban, config changes

Read-only auditor:

sudo usermod -aG nftban-auditor auditor
# auditor: read logs, view status, generate compliance reports

DirectAdmin panel integration:

sudo useradd -r -s /sbin/nologin nftban-da
sudo usermod -aG nftban-panel nftban-da
# nftban-da: ban/unban, status, reload - NO config changes, NO restart

cPanel panel integration:

sudo useradd -r -s /sbin/nologin nftban-cp
sudo usermod -aG nftban-panel nftban-cp
# nftban-cp: operator actions via nftban-panelctl wrapper

Polkit Integration

NFTBan uses Polkit instead of sudo for privilege separation. No root escalation, scoped permissions, full audit trail via systemd journal.

Service Management Rules

File: /etc/polkit-1/rules.d/10-nftban-systemd.rules

polkit.addRule(function(action, subject) {
    if (!(action.id == "org.freedesktop.systemd1.manage-units" ||
          action.id == "org.freedesktop.systemd1.manage-unit-files")) {
        return polkit.Result.NOT_HANDLED;
    }

    if (!subject.isInGroup("nftban")) {
        return polkit.Result.NOT_HANDLED;
    }

    var unit = action.lookup("unit");
    var verb = action.lookup("verb");

    // Only allow managing NFTBan units by prefix
    if (!unit || unit.indexOf("nftban") !== 0) {
        return polkit.Result.NOT_HANDLED;
    }

    var allowedVerbs = [
        "start", "stop", "restart", "reload",
        "enable", "disable", "try-restart",
        "reload-or-restart", "try-reload-or-restart"
    ];

    if (allowedVerbs.indexOf(verb) >= 0) {
        return polkit.Result.YES;
    }

    return polkit.Result.NOT_HANDLED;
});

Auditor Rules

File: /etc/polkit-1/rules.d/20-nftban-auditor.rules

Read-only systemd queries for nftban-auditor group members.

Allowed verbs: status, show, is-active, is-enabled, is-failed, list-units, list-unit-files

Denied verbs: start, stop, restart, reload, enable, disable, try-restart, reload-or-restart, kill, reset-failed

Denied actions: daemon-reload, pkexec (no command execution)

Panel Rules

File: /etc/polkit-1/rules.d/30-nftban-panel.rules

Reload-only for nftban-panel group members. Start/stop/restart/enable/disable are denied.

Rule processing: Polkit evaluates rules in priority order (10 → 20 → 30).

Attack Surface Analysis

Compromised nftban-group user can:
  - Stop/start nftban-* and suricata services
  - Read /etc/nftban/*.conf (group read)

Compromised nftban-group user CANNOT:
  - Modify /usr/lib/nftban/* (owned by root, not writable)
  - Modify /etc/nftban/* (owned by root, read-only for group)
  - Manage other services (sshd, httpd, postgresql)
  - Escalate to root
  - Install backdoors in code
  - Modify polkit rules

Mitigations:
  - All actions logged to systemd journal
  - Monitor for unexpected service restarts
  - Review nftban group membership regularly

FHS Auto-Heal

Automatic detection and correction of permission/ownership issues. Privilege-aware: fixes what the running user owns, reports what needs root.

How It Works

  1. Check — Scan all FHS directories for missing dirs, wrong permissions, wrong ownership
  2. Fix — If directory owned by nftban and parent is writable: create and chmod. If running as root: create, chown, chmod. Otherwise: report and suggest sudo nftban health fix all
  3. Report — Summary of fixed items and items needing root
  4. Log — All actions to systemd journal

Commands

# Check FHS compliance (no changes)
nftban fhs status
nftban fhs detailed    # full table output
nftban fhs summary     # one-line: "FHS: 28/28 OK (100%)"
nftban fhs json        # JSON output for automation
nftban fhs html-report # HTML report with charts
nftban fhs mail-report admin@example.com

# Auto-fix (smart healing)
nftban health fix all              # as nftban user (fixes owned dirs)
sudo nftban health fix all         # as root (fixes everything)
nftban health fix directories      # fix only directories
nftban health fix permissions      # fix only permissions

Example output (as nftban user):

$ nftban health fix all

[1/2] Creating missing directories...
  Created /var/lib/nftban/exports (750 nftban:nftban)
  Created /var/log/nftban/reports (750 nftban:nftban)

  Cannot create 1 directory (need root):
     /usr/lib/nftban/bin → 755 root:root

[2/2] Fixing permissions and ownership...
  Fixed /var/lib/nftban → perms: 750
  Fixed /var/log/nftban → perms: 750

  Cannot fix 2 issues (need root):
     /etc/nftban: chmod 750 (currently 755, owned by root)

Summary: Fixed 5 items | Needs root: 3 items
→ sudo nftban health fix all

Automatic Daily Heal

Timer: nftban-health.timer — runs daily at 03:00 with 30m jitter.

# /etc/systemd/system/nftban-health.service
[Service]
Type=oneshot
User=nftban
Group=nftban
ExecStart=/usr/bin/flock -n /var/cache/nftban/health.lock \
    /usr/sbin/nftban health check --auto-heal --cache-status
# View heal logs
journalctl -u nftban-health.service -n 50
journalctl -u nftban-health.service --since today

FHS Compliance Reporting

$ nftban fhs status

┌──────────────────────────────────────────────────────────────────┐
│ Path                          │ Status │ Perms │ Owner:Group     │
├──────────────────────────────────────────────────────────────────┤
│ /usr/lib/nftban               │   ✓    │ 755   │ root:root       │
│ /etc/nftban                   │   ✓    │ 750   │ root:nftban     │
│ /var/lib/nftban               │   ✓    │ 750   │ nftban:nftban   │
│ /var/log/nftban               │   ✓    │ 750   │ nftban:nftban   │
│ /var/cache/nftban             │   ✓    │ 755   │ nftban:nftban   │
│ /run/nftban                   │   ✓    │ 755   │ nftban:nftban   │
└──────────────────────────────────────────────────────────────────┘

Summary: 28/28 directories OK (100% compliant)

HTML reports include compliance chart, full path table, issues highlighted, auto-fix suggestions.


FHS Directory Specification

Single source of truth: build/fhs-spec.yaml

# System directories (root:root, 755) — code
/usr/sbin                         755|root|root
/usr/lib/nftban                   755|root|root
/usr/lib/nftban/core              755|root|root
/usr/lib/nftban/cli               755|root|root
/usr/lib/nftban/bin               755|root|root

# Configuration (root:nftban, 750) — daemon reads via group
/etc/nftban                       750|root|nftban
/etc/nftban/conf.d                750|root|nftban

# Runtime data (nftban:nftban, 750) — nftban user owns
/var/lib/nftban                   750|nftban|nftban
/var/lib/nftban/reports           750|nftban|nftban
/var/lib/nftban/reports/auditors  770|root|nftban-auditor
/var/lib/nftban/metrics           750|nftban|nftban
/var/lib/nftban/snapshots         750|nftban|nftban
/var/lib/nftban/exports           750|nftban|nftban
/var/lib/nftban/geoip             750|nftban|nftban

# Logs (nftban:nftban, 750)
/var/log/nftban                   750|nftban|nftban
/var/log/nftban/reports           750|nftban|nftban

# Cache and runtime (nftban:nftban, 755)
/var/cache/nftban                 755|nftban|nftban
/run/nftban                       755|nftban|nftban

# Shared data (root:root, 755) — read-only
/usr/share/nftban                 755|root|root
/usr/share/nftban/templates       755|root|root

Key principle: root owns code and config, nftban user owns runtime data.

See FHS Compliance for the complete specification.


Security Testing

Test 1: FHS Auto-Heal (as nftban user)

sudo rm -rf /var/lib/nftban/exports
sudo -u nftban nftban health fix all
# Expected: Creates /var/lib/nftban/exports successfully

Test 2: FHS Auto-Heal (as root)

sudo rm -rf /usr/lib/nftban/bin
sudo nftban health fix all
# Expected: Creates /usr/lib/nftban/bin with root:root 755

Test 3: Polkit Service Management (Allowed)

# As user in nftban group
systemctl restart nftban-core.service
# Expected: Succeeds without password prompt

Test 4: Polkit Scope Check (Denied)

systemctl restart sshd
# Expected: Access denied

Test 5: File Permission Protection

echo "test" >> /usr/lib/nftban/core/firewall.sh
# Expected: Permission denied

echo "test" >> /etc/nftban/nftban.conf
# Expected: Permission denied

Platform Security Tiers

Security fixes are prioritized by tier:

Tier Platforms Priority
Tier 0 Ubuntu 24.04, Debian 12, Rocky 9 Immediate
Tier 1 Debian 13, Rocky 10, Ubuntu 26.04 High
Tier 2 Ubuntu 22.04, Debian 11, Rocky 8 Best-effort

See Supported Platforms for the full platform contract.


IPC Security Architecture (v1.19.27+)

NFTBan uses a defense-in-depth approach to prevent command injection in nftables operations.

Validation Layers

User Input → CLI Validation → IPC Layer → Go Daemon Validation → nft Command
     │              │              │                │                 │
     ▼              ▼              ▼                ▼                 ▼
  cmd_ban.sh   cmd_validate_ip   JSON/socket   net.ParseIP()    nft add element
Layer Location Validation Protection
CLI Entry cmd_ban.sh:105 cmd_validate_ip() Rejects malformed input early
IPC Transport nft_ipc.sh JSON protocol via jq No shell interpolation in transport
Go Daemon main.go:1466 net.ParseIP() + net.ParseCIDR() Authoritative validation
Emergency Mode nft_ipc.sh:357 _nft_ipc_validate_ip() Defense-in-depth for bypass path

Why Defense-in-Depth?

Even if the CLI validates input, the lower-level functions also validate because:

  1. Code paths change — Future code might call these functions directly
  2. Emergency mode bypass — Shell fallback when daemon is down
  3. Security boundary — Each layer assumes untrusted input

Validation Functions

Shell layer (validation.sh):

nftban_validate_ip "$ip"      # IPv4 or IPv6
nftban_validate_ipv4 "$ip"    # Strict IPv4: 4 octets, 0-255
nftban_validate_ipv6 "$ip"    # Hex + colons only
nftban_validate_cidr "$cidr"  # IP/prefix format

IPC layer (nft_ipc.sh):

_nft_ipc_validate_ip "$ip"    # Local validation for emergency mode

Go layer (pkg/netutil/ip.go):

net.ParseIP(ip)               // Returns nil if invalid
net.ParseCIDR(cidr)           // Returns error if invalid
netutil.IsValidIP(ipStr)      // Boolean wrapper

Character Whitelists

All validation uses strict character whitelists:

Type Allowed Characters Example
IPv4 0-9. 192.168.1.1
IPv6 0-9a-fA-F: 2001:db8::1
CIDR IPv4/IPv6 + /0-9 10.0.0.0/8
Port 0-9 8080

Rejected characters: ` $ " \ ; | & > < ( ) { } — prevents shell injection

Files with nft Command Security (v1.19.27)

File Function Validation Added
nft_ipc.sh nft_emergency_ban() _nft_ipc_validate_ip()
nft_ipc.sh nft_emergency_unban() _nft_ipc_validate_ip()
cmd_whitelist.sh nftban_whitelist_add_ip() nftban_validate_ip/cidr()
cmd_whitelist.sh nftban_whitelist_remove_ip() nftban_validate_ip/cidr()
maintenance.sh whitelist sync Inline regex validation
cmd_firewall.sh backup restore Character whitelist regex
cmd_flush.sh system IP restore Character whitelist regex

Testing Validation

# Test CLI validation
nftban ban '; rm -rf /'           # Expected: "Invalid IP address"
nftban ban '$(whoami)'            # Expected: "Invalid IP address"
nftban ban '192.168.1.1; echo x'  # Expected: "Invalid IP address"

# Test IPC validation (Go daemon logs)
journalctl -u nftband -f
# Then attempt:
echo '{"method":"ban","params":{"ip":"$(id)"}}' | socat - /run/nftban/nftband.sock
# Expected: {"success":false,"error":"invalid IP address"}

# Verify Go validation
grep "invalid IP" /var/log/nftban/daemon.log

False Positive Prevention

To avoid rejecting legitimate IPs:

  1. IPv6 compressed notation::1, fe80::1 are valid
  2. CIDR notation10.0.0.0/8 includes the slash
  3. IPv4-mapped IPv6::ffff:192.168.1.1 is handled

Validation functions are tested against RFC 5321 (IPv4) and RFC 4291 (IPv6) formats.


Ban-First Architecture (v1.19.27+)

NFTBan uses a ban-first architecture where enforcement is immediate and enrichment is asynchronous. This ensures minimal latency in the hot path.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        BAN REQUEST PATH (SYNC)                      │
│                                                                     │
│   nftban ban <ip>  ──►  Whitelist Check  ──►  backend.Ban()  ──►  OK │
│         │                    │ (BLOCKING)          │ (IMMEDIATE)    │
│         │                    │                     │                │
│         ▼                    ▼                     ▼                │
│   CLI validation      main.go:1474         main.go:1507-1512       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
                                     │
                                     │ (POST-BAN, NON-BLOCKING)
                                     ▼
┌─────────────────────────────────────────────────────────────────────┐
│                     ENRICHMENT PATH (INFORMATIONAL)                 │
│                                                                     │
│   GeoIP Lookup  ──►  Source Tagging  ──►  Log Entry  ──►  Metrics  │
│   main.go:1544       main.go:1531         main.go:1545              │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                      FEED SYNC PATH (ASYNC)                         │
│                                                                     │
│   feeds.conf  ──►  Timer Trigger  ──►  Download  ──►  nft load     │
│   (enabled)        (every 2 min)       (curl)        (atomic)      │
│                                                                     │
│   Result: IPs added to blacklist_ipv4/ipv6 sets BEFORE requests    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Lookup Classification

Lookup Classification Path Justification
Input validation Decision-critical SYNC Invalid IP cannot be banned
Whitelist check Decision-critical SYNC Whitelisted IPs must never be banned
Set selection (IPv4/IPv6) Decision-critical SYNC Determines target nftables set
backend.Ban() Decision-critical SYNC Authoritative enforcement action
Feed lookup Precomputed ASYNC (timer) Feeds loaded into sets beforehand
GeoIP lookup Metadata only Post-ban Only affects logging, not ban decision
ASN/reverse DNS Metadata only Post-ban Only affects analytics

Key Design Decisions

1. Feeds are Precomputed, Not Per-Ban

Feeds are synchronized asynchronously into nftables sets:

  • Timer: nftban-core-feeds.timer (every 2 minutes)
  • Files: /var/lib/nftban/feeds/*.txt
  • Sets: blacklist_ipv4, blacklist_ipv6

When a ban request arrives, the IP is added to the same sets. There is no per-ban feed lookup.

2. GeoIP is Post-Ban Informational

// main.go:1507-1512 — Ban executes FIRST
result, err := d.backend.Ban(d.ctx, nftbackend.BanRequest{
    IP: ip, Timeout: timeout, Reason: reason, Source: source,
})

// main.go:1544 — GeoIP lookup AFTER ban (informational only)
country := lookupCountry(ip)
_ = banlog.LogBanWithReason(ip, banSource, country, reason)

GeoIP affects:

  • Log entry country field
  • Prometheus metric labels
  • Analytics aggregation

GeoIP does NOT affect:

  • Whether to ban
  • Ban TTL/timeout
  • Target set selection
  • Action type

3. Enrichment Never Blocks Ban

If any enrichment lookup fails:

  • Ban still succeeds
  • Log entry uses fallback values (e.g., country="unknown")
  • Metrics still recorded

Evidence Summary

File Line Finding
cmd/nftband/main.go 1459-1571 handleBanRequest(): Whitelist check → Ban() → GeoIP
cmd/nftband/main.go 1507-1512 Ban execution: No feed dependency
cmd/nftband/main.go 1544 GeoIP lookup: Post-ban, informational
pkg/nftbackend/backend.go 224-229 Set selection: Based on IP type only
nftban_feeds.sh 651-680 Feeds: Timer-based sync, not per-request

Performance Characteristics

Path Target Latency Blocking?
Ban execution < 10ms P99 Yes (minimal)
GeoIP lookup < 5ms Yes (post-ban)
Feed sync 30-60s No (timer-based)
Enrichment queue N/A No (future work)

Future Optimizations (Optional)

These are micro-optimizations, not architectural fixes:

  1. GeoIP async decoupling — Move to enrichment worker
  2. Latency metrics — Add nftban_ban_request_duration_seconds
  3. Enrichment queue — Async metadata updates with BanID correlation

Note: The current architecture is already correct. These are polish items.


High-Risk Areas

Changes to these require extra security review:

  • systemd unit files, timers, sockets
  • polkit rules or helpers
  • nftables rule generation
  • installer and maintainer scripts (deb/rpm)
  • receipt schema or validation logic
  • any code paths that modify /etc/nftban or firewall state

Migration from Previous Versions

From v0.x (4-Group Model)

If upgrading from the old 4-group model (nftban, nftban-cli, nftban-web, nftban-auditors):

# Migrate nftban-cli and nftban-web users to nftban group
for old_group in nftban-cli nftban-web; do
    if getent group "$old_group" >/dev/null 2>&1; then
        for user in $(getent group "$old_group" | cut -d: -f4 | tr ',' ' '); do
            echo "Migrating $user from $old_group to nftban"
            sudo usermod -aG nftban "$user"
        done
    fi
done

# Migrate nftban-auditors (plural) to nftban-auditor (singular)
if getent group nftban-auditors >/dev/null 2>&1; then
    for user in $(getent group nftban-auditors | cut -d: -f4 | tr ',' ' '); do
        echo "Migrating $user from nftban-auditors to nftban-auditor"
        sudo usermod -aG nftban-auditor "$user"
    done
fi

# Clean up old groups after verification
# sudo groupdel nftban-cli
# sudo groupdel nftban-web
# sudo groupdel nftban-auditors

Troubleshooting

User Cannot Manage Services

# 1. Check group membership
id username | grep nftban

# 2. Add to group if missing
sudo usermod -aG nftban username

# 3. Re-login or activate immediately
newgrp nftban

# 4. Verify polkit rule exists
ls -la /etc/polkit-1/rules.d/10-nftban-systemd.rules

Security Best Practices

  • Only add users who actually manage the firewall to nftban
  • Use nftban-auditor for read-only access — do not add auditors to nftban
  • Keep panel accounts in nftban-panel only — never in nftban
  • Review group membership quarterly:
getent group nftban
getent group nftban-auditor
getent group nftban-panel

References

Clone this wiki locally