| Version | Security fixes |
|---|---|
| 1.1.x | Yes |
| 1.0.x | Yes |
Please do not open a public GitHub issue for security vulnerabilities.
Open a private security advisory at github.com/The-Bugger/Net-Guard/security/advisories, or email the maintainers via the contact on the The Bugger profile with:
- A description of the vulnerability and the affected component
- Steps to reproduce (proof-of-concept if possible)
- Potential impact in your assessment
You will receive an acknowledgement within 72 hours. We aim to issue a fix within 14 days for critical issues and 30 days for other severity levels.
NetGuard runs on a single Linux machine with root privileges and direct access to the network interface and iptables. The threat model below documents what NetGuard protects against and what it does not.
| Asset | Description |
|---|---|
| Monitored network | Traffic on the interface NetGuard captures |
| SQLite database | Events, blocks, whitelist, settings, logs |
| iptables ruleset | The system firewall NetGuard manages |
| Configuration | config/config.yaml and .env |
| Log files | logs/system.log, detections.log, errors.log |
| Threat | Mitigation |
|---|---|
| TCP SYN flood | Auto-block via iptables DROP within seconds of threshold breach |
| Port reconnaissance | Auto-block; alert with scanned port list |
| SQL injection (HTTP) | Alert; auto-block; matched pattern in evidence |
| Brute-force login (SSH/HTTP/FTP) | Alert; auto-block |
| ARP spoofing / MitM | Alert (blocking ARP-level attacks via IP is limited; see Known Limitations) |
| Repeated attacker | Whitelist check prevents blocking trusted IPs; cooldown prevents alert storms |
- Encrypted traffic inspection (TLS/HTTPS payloads are not decrypted)
- Application-layer auth (NetGuard does not inspect HTTP response codes)
- IPv6 firewall management (iptables is IPv4-only; ip6tables is not called)
- Host intrusion detection (file integrity, process monitoring)
- Distributed attacks from many source IPs below per-IP thresholds
- Physical network access
- Attacks that originate from whitelisted IPs
NetGuard requires root to:
- Open a raw socket for Scapy packet capture
- Execute
iptablescommands viasubprocess.run()
The Flask API itself does not require elevated privileges but runs in the same
process. In production, consider using CAP_NET_ADMIN and CAP_NET_RAW
capabilities instead of full root.
All IP addresses are validated with ipaddress.ip_address() before any:
- Database insert
- iptables command execution
- Whitelist operation
All iptables commands use shlex.quote(ip) to prevent shell injection.
Integer settings (thresholds, durations) are range-checked by
ConfigurationManager.validate_settings() before application.
SECRET_KEYis the Flask session secret. Change it before any production or public deployment. The default valuechange-me-before-productionis intentionally obvious.- The
.envfile is excluded from version control via.gitignore. - The
LoggingEngineredacts any metadata key matchingpassword,passwd,secret,token,private_key,api_key,auth, orcredentialbefore writing to log files or the database.
- CORS is enabled for all
/api/*origins. Restrict this in production to your dashboard origin. - There is no authentication on the REST API. In production, place NetGuard behind a reverse proxy (nginx) with HTTP Basic Auth or mutual TLS.
- All API responses use a standard envelope; internal error details are not leaked to clients (services log details server-side).
- SQLite WAL mode is enabled for concurrent read performance.
- Foreign key constraints are enforced at the SQLite level.
- The database file is local to the server; no remote DB connections.
| Limitation | Impact | Notes |
|---|---|---|
| No TLS inspection | SQL injection in HTTPS traffic is invisible | Would require SSL bump proxy |
| TCP-level brute-force proxy | False positives on high-traffic auth endpoints | Connection count, not actual auth failures |
| ARP block has limited effect | ARP spoofing detection cannot be stopped by IP-layer iptables rules | Alert operators; consider static ARP entries or 802.1X |
| Single-machine deployment | No distributed correlation | Per-IP thresholds can be bypassed by low-rate distributed attacks |
| No IPv6 iptables | IPv6 attackers are detected but not blocked | Extend with ip6tables if IPv6 is used |
| No API authentication | Dashboard is accessible to anyone on the network | Add a reverse proxy with auth in production |
| Root required | Process runs as root | Use Linux capabilities as a hardening step |
A single shared secret (NETGUARD_API_KEY env var) is compared against the
X-API-Key request header using hmac.compare_digest (constant-time comparison,
preventing timing-oracle attacks). The check is enforced by the check_api_key()
before_request hook registered in create_app(), after input validation and rate
limiting.
Authentication is required for all mutating methods (POST, PUT, DELETE, PATCH).
Read-only GET requests are open by default; set REQUIRE_AUTH_FOR_READS=true to
require auth on GET as well (see SocketIO Exclusion section for the one exception).
When NETGUARD_API_KEY is not set, check_api_key() returns None and all
requests pass through. This keeps local development frictionless. There is no
placeholder or dummy key that grants access — absence of the env var is the explicit
dev-mode signal.
| Property | Value |
|---|---|
| Key rotation | Manual (restart required) |
| Per-client scoping | None — single shared secret |
| Revocation | Remove/change env var and restart |
| Brute-force resistance | Rate limiter + constant-time compare |
This model is appropriate for a single-server dev/demo deployment. For production at scale, replace with a secrets manager and per-client tokens.
SocketIO upgrade handshakes arrive as GET requests to /socket.io/. Because
REQUIRE_AUTH_FOR_READS defaults to false, GET requests pass through the auth
hook without requiring a key — which means SocketIO connections are never blocked
by the authentication middleware.
Even when REQUIRE_AUTH_FOR_READS=true, the check_api_key() hook explicitly
skips paths that start with /socket.io/, so the real-time dashboard feed remains
available regardless of the auth configuration.
Trade-off: Any client that can reach the server can subscribe to the SocketIO event stream (threat alerts, IP blocks) without presenting an API key. This is intentional for the current dev/demo deployment model. In production, place NetGuard behind a reverse proxy with WebSocket authentication (e.g., cookie-based session or mutual TLS) if the SocketIO feed should be restricted.
The RateLimiter._client_ip() method determines which IP to rate-limit per
request. Its behaviour is controlled by the TRUST_PROXY_HEADERS environment
variable (default: false):
| Value | Behaviour |
|---|---|
false (default) |
Always uses request.remote_addr — the TCP peer address |
true |
Reads the leftmost IP from X-Forwarded-For header |
Set TRUST_PROXY_HEADERS=true only when NetGuard sits behind a reverse proxy
(nginx, HAProxy, etc.) that overwrites (not appends to) the X-Forwarded-For
header with the real client IP. In that topology, remote_addr is always the
proxy's address, and reading X-Forwarded-For gives the actual client.
If TRUST_PROXY_HEADERS=true is set without a proper sanitising proxy in front,
any client can forge X-Forwarded-For: 1.2.3.4 in their request and effectively
choose their own rate-limit identity. This allows a single attacker to bypass
per-IP rate limiting entirely. Do not enable this flag on a server directly
exposed to the internet.
See DEPLOYMENT.md for specific proxy configuration guidance.
The PreventionEngine.block_ip() method refuses to add an iptables DROP rule for
any address that falls in a protected range, unless explicitly overridden. Protected
ranges are:
| Range | Description |
|---|---|
10.0.0.0/8 |
RFC 1918 private |
172.16.0.0/12 |
RFC 1918 private |
192.168.0.0/16 |
RFC 1918 private |
127.0.0.0/8 |
IPv4 loopback |
169.254.0.0/16 |
Link-local |
224.0.0.0/4 |
IPv4 multicast |
::1/128 |
IPv6 loopback |
fe80::/10 |
IPv6 link-local |
ff00::/8 |
IPv6 multicast |
In addition, any IP that matches one of the server's own interface addresses
(detected via psutil.net_if_addrs()) is refused regardless of whether it
falls in the ranges above.
When a block is refused, a WARNING is logged and block_ip() returns False.
No iptables rule is inserted and no block record is written to the database.
Pass allow_private_block=True to block_ip() to bypass both checks. This kwarg
is intended exclusively for integration tests and administrative scripts that
intentionally target private-range IPs in a controlled environment. Normal
detection-engine flow never passes this flag.