
Most VPS firewalls fail quietly. They block (or allow) traffic, but you still can’t answer basic questions quickly: “What’s hitting port 22?”, “Which IPs keep probing my admin panel?”, “Did we block the right thing?” This walkthrough sets up VPS firewall logging with nftables so the logs stay readable, your disk doesn’t get hammered, and you always have a clean way back if you mess up.
The example box is Debian 12 running an internal Go service on 127.0.0.1:9100, with Nginx on 443. SSH remains reachable, but only from your office CIDR. You’ll write a minimal nftables policy, add targeted log statements with clear prefixes, put rate limits on the noisy rules, and confirm the logs land where you’ll actually look: systemd-journald.
Why nftables logging beats “log everything”
nftables lets you log at the decision points that matter: the first packet of a new connection to a sensitive port, invalid states, and denied management access. That’s very different from dumping every packet to disk.
- Disk churn: a single scan can generate tens of thousands of lines per minute if you log every packet.
- Noisy signal: you stop looking because the useful entries are buried.
- Risky changes: a quick tweak turns into an accidental lockout.
You’ll build a default-deny inbound policy, allow only what you need, and add a few high-value logging rules with sane rate limiting. For a broader hardening baseline, pair this with VPS Incident Response Checklist (2026) so the logs you collect line up with the actions you take during triage.
Prerequisites
- A Debian 12 / Ubuntu 24.04+ VPS with root access (or sudo). Examples below assume Debian 12.
- Active SSH session plus a second recovery session (separate terminal or console access).
- Your allowed admin source IP/CIDR (example:
203.0.113.0/24). - Ports you intend to expose (example:
22,80,443).
If you’re doing this on a production box and want guardrails (snapshots, change assistance, quick recovery), a managed VPS hosting plan can be worth it. If you’re comfortable running changes yourself, a standard HostMyCode VPS is a clean fit for nftables-based setups.
Step 1: Confirm nftables is available (and what is currently managing your firewall)
-
Check whether nftables is installed and active:
sudo nft --version sudo systemctl status nftables --no-pagerExpected output: an nftables version line (e.g.
nftables v1.x), and either “active (exited)” or “active (running)” for the service. -
See if anything else is currently controlling filtering:
sudo systemctl is-active ufw || true sudo systemctl is-active firewalld || true sudo iptables -S 2>/dev/null | head -n 20 || true sudo nft list ruleset | head -n 60Decision: if UFW/firewalld is active, pause and make a conscious call about migrating. Two firewall managers fighting over rules is how you get confusing behavior. For this tutorial, nftables is the single source of truth.
If you’re coming from a UFW-based setup and want a reminder of SSH-safe change flow, skim UFW Firewall Setup for a VPS in 2026. The same discipline applies here: prove access first, then tighten policy.
Step 2: Create a safe rollout plan (so you don’t lock yourself out)
Before you touch the rules, set up a timed rollback. It’s dull work, and it’s also the part that saves you.
-
In a second SSH session, start a 3-minute rollback timer that flushes nft rules:
sudo bash -lc 'sleep 180; echo "ROLLBACK: flushing nft rules" | systemd-cat -t nft-rollback; nft flush ruleset' &If your new policy blocks your access, wait three minutes and the rules clear. If everything checks out, you’ll cancel this job later.
-
Back up your current ruleset (even if it’s empty):
sudo nft list ruleset > /root/nftables.ruleset.backup.$(date +%F-%H%M%S)
If you also want systemd-style safety nets for services (not just firewall rules), the patterns in systemd watchdog on a VPS pair nicely with this. Bad deploys often break networking and service health at the same time.
Step 3: Write an nftables ruleset with structured logging
Put your rules in a dedicated config file. On Debian, nftables loads /etc/nftables.conf by default.
-
Edit
/etc/nftables.conf:sudo nano /etc/nftables.conf -
Paste the ruleset below, then adjust the variables at the top. It uses an
inettable (IPv4+IPv6 in one place), default-drop inbound, and a small set of logs you can actually search later.#!/usr/sbin/nft -f flush ruleset define ADMIN_CIDR = { 203.0.113.0/24 } define WEB_PORTS = { 80, 443 } table inet hmc_filter { chain input { type filter hook input priority 0; policy drop; # 1) Always allow loopback iif "lo" accept # 2) Allow established/related traffic ct state established,related accept # 3) Drop invalid packets and log (rate-limited) ct state invalid limit rate 5/second burst 20 log prefix "NFT INVALID " level warning ct state invalid drop # 4) SSH: allow only from admin CIDR, log other attempts (rate-limited) tcp dport 22 ip saddr $ADMIN_CIDR ct state new accept tcp dport 22 ct state new limit rate 2/second burst 10 log prefix "NFT SSH DENY " level info tcp dport 22 drop # 5) HTTP/HTTPS tcp dport $WEB_PORTS ct state new accept # 6) (Example) Allow Node exporter locally only; if you expose it, do it intentionally tcp dport 9100 ip saddr 127.0.0.1 accept # 7) ICMP/ICMPv6 (basic reachability) ip protocol icmp accept ip6 nexthdr ipv6-icmp accept # 8) Final drop log for NEW connections only (avoid logging established flows) ct state new limit rate 10/second burst 40 log prefix "NFT DROP NEW " level info drop } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } }
What’s special here: the final “drop new” log gives you broad visibility into unexpected inbound attempts, but it stays bounded thanks to rate limiting and ct state new. SSH is even tighter: you only log denied NEW attempts, which are the ones you’ll investigate.
Step 4: Validate the ruleset before applying it
nftables can validate syntax without loading anything. Make that your default habit.
-
Run a dry parse:
sudo nft -c -f /etc/nftables.confExpected output: no output and exit code 0.
-
Apply the config:
sudo nft -f /etc/nftables.conf -
List the active rules:
sudo nft list rulesetExpected output: you should see
table inet hmc_filterand theinputchain with your rules in order.
Step 5: Enable nftables at boot (and confirm persistence)
-
Enable and start the service:
sudo systemctl enable --now nftables -
Verify it loads your config:
sudo systemctl status nftables --no-pager sudo grep -n "nftables.conf" -n /lib/systemd/system/nftables.service /usr/lib/systemd/system/nftables.service 2>/dev/null || true -
Reboot test (recommended for real production changes):
sudo rebootAfter reconnecting, run:
sudo nft list ruleset | head -n 40
Step 6: Verify that logging actually lands in journald
nftables logs via the kernel. On current Debian/Ubuntu systems, that usually means systemd-journald (and sometimes rsyslog too). Don’t assume it works—prove it with controlled tests.
-
Tail firewall logs in one terminal:
sudo journalctl -k -f -
From a machine outside your
ADMIN_CIDR, try an SSH connection (or just a TCP probe):ssh -o ConnectTimeout=5 root@YOUR_SERVER_IPExpected result: connection should fail. In
journalctl -k, you should see lines containingNFT SSH DENY. -
Generate a harmless “drop new” log by probing a closed port (example: 8443):
nc -vz YOUR_SERVER_IP 8443Expected output:
failedortimed out, and a kernel log containingNFT DROP NEW.
If you need retention and search, kernel logs on one box won’t cut it. Ship them somewhere you can query. The setup in VPS log shipping with Vector works well with nftables prefixes because they’re easy to filter and index.
Step 7: Tighten logging so it’s useful during incidents (not just “noise”)
Once you can see events, shape the logs around the decisions you’ll make. A few targeted rules beat a wall of text.
-
Log only what you plan to act on. Good candidates:
- Denied SSH NEW connections
- Denied access to internal admin ports (e.g., 9090, 3000)
- Invalid state drops (often indicate broken clients or garbage traffic)
-
Add a dedicated rule for an internal admin port. Example: you run a Grafana on
3000but it should be private. Add (above the final drop log):# Grafana: allow only from ADMIN_CIDR; log and drop everything else tcp dport 3000 ip saddr $ADMIN_CIDR ct state new accept tcp dport 3000 ct state new limit rate 1/second burst 5 log prefix "NFT GRAFANA DENY " level info tcp dport 3000 drop -
Keep log prefixes consistent. During an incident, you’ll grep by prefix:
sudo journalctl -k --since "1 hour ago" | grep "NFT SSH DENY" | tail -n 50
Step 8: Add basic DoS-friendly rate limits (without blocking normal users)
Rate limiting is always a trade-off. If you set it too low, you’ll miss bursts from NATed offices or CI runners. Too high, and a scan still turns into a log storm. These defaults are intentionally conservative for small-to-mid VPS deployments.
- SSH deny log:
2/second burst 10catches brute force attempts without flooding. - Final drop log:
10/second burst 40gives visibility into scans while remaining bounded. - Invalid state log:
5/second burst 20avoids spamming when something upstream misbehaves.
If your VPS takes sustained hostile traffic, you may get better results by removing the target. Put SSH behind a private network overlay instead of exposing it. The approach in Tailscale VPS VPN setup cuts down the deny events you’ll ever need to log.
Step 9: Operational checks (quick diagnostics you’ll actually run)
Keep a few commands handy for change windows and incident response. These cover 90% of what you’ll need.
-
Show rules with counters (helps confirm a rule is being hit):
sudo nft -a list chain inet hmc_filter input -
Reset counters before a test so you can measure only new traffic:
sudo nft reset counters -
See listening ports (ensure you’re not exposing something you forgot):
sudo ss -lntup -
Confirm SSH allowlist is correct (replace with your office IP):
curl -s https://ifconfig.me; echo
Common pitfalls (and how to avoid them)
-
Locking yourself out via CIDR mistakes. Test SSH from an allowed IP before you close your second session. Keep the timed rollback running until you’ve verified access.
-
Forgetting IPv6. Using
table inetcovers both stacks, but the allowlist examples useip saddr(IPv4). If you administer over IPv6, addip6 saddrequivalents. -
Logging established traffic. If you omit
ct state newon drop logs, a single connection can generate many entries. Keep “catch-all” logs limited to NEW connections. -
Assuming logs persist forever. journald has retention limits. If you need week-old evidence, ship logs to a central store or configure persistent journald storage and rotation. Also watch disk usage; VPS disk space troubleshooting helps when logs become part of the problem.
Rollback options (pick the safest one for your situation)
Rollback needs to be fast and boring. Pick the option that gets you back to a known state with the least thinking.
-
Immediate flush (restores “no firewall” behavior):
sudo nft flush rulesetThis is blunt, but it gets you back in if you broke access.
-
Restore the backed-up ruleset:
sudo ls -1 /root/nftables.ruleset.backup.* | tail -n 1 sudo nft -f /root/nftables.ruleset.backup.YYYY-MM-DD-HHMMSS -
Disable nftables service (and reboot if needed):
sudo systemctl disable --now nftables
After you’ve confirmed everything works, cancel the timed rollback job you started earlier. If you launched it with &, you can find it with:
jobs -l
If your shell doesn’t track it (common), don’t waste time chasing it. Wait three minutes after your final verification. If it flushes unexpectedly, re-apply the correct config.
Next steps: turn logs into action
- Alert on prefixes: notify on spikes of
NFT SSH DENYorNFT DROP NEWfrom a single ASN/IP block. - Correlate with auth logs: combine nftables deny logs with
sshdauth failures to spot credential stuffing. - Centralize and retain: ship kernel logs off-box so disk pressure doesn’t erase your evidence.
- Reduce exposure: move admin services behind a VPN overlay or a bastion.
If you’re building an audit-friendly Linux perimeter, start with a VPS you can snapshot and restore quickly. A HostMyCode VPS gives you full control over nftables, journald, and log shipping. If you want review help for firewall changes and a recovery plan you’ve actually tested, consider managed VPS hosting for production systems.
FAQ
Should I keep UFW installed if I’m using nftables directly?
You can keep the package installed, but don’t keep the service active. Two tools writing rules at the same time makes behavior hard to predict. Pick one manager for production.
Why do my nftables logs not show up in journalctl?
Start with journalctl -k. If nothing appears, check whether kernel logging is restricted, and whether rsyslog is intercepting logs. Also confirm your rules contain log statements and that rate limits aren’t set too low for your test.
How do I log the first packet only, not every packet?
Use connection tracking: add logging only on ct state new. That logs the start of a connection attempt, which is typically what you want for scanning and access attempts.
Is it safe to log IP addresses for security auditing?
For most security operations, yes, but treat them as potentially sensitive operational data. Set retention appropriately, restrict log access, and document why you collect it.
Can I use nftables logging to detect port scans automatically?
You can detect scan patterns by counting NFT DROP NEW events per source over a window, but don’t try to do it purely with raw logs on a single server. Centralize logs and use thresholds (and rate limits) so detection doesn’t become a DoS vector.
Summary
VPS firewall logging with nftables works best when you log selectively: denied management access, invalid states, and unexpected NEW inbound connections. Add rate limits, confirm logs appear in journald, and keep a rollback plan running until you’ve tested SSH and web access from real client networks. For production VPS deployments where predictable recovery matters, run this on a HostMyCode VPS and tie it into your monitoring and incident runbooks.