Recipes

Explained commands, based on the official documentation. All examples use documentation IP addresses (SIMULATED).

Drop INVALID packets (conntrack)

low ✓

Purpose: drop packets in the INVALID state (they match no known connection). Why this order: put the rule before the general ACCEPT rules. INVALID means packets that do not match any known connection (malformed or out of sequence); they also appear in some scans, but it is not a scan detector. Use -m conntrack (not -m state, which is deprecated). Verify: iptables -L INPUT -n -v --line-numbers. Undo: iptables -D INPUT -m conntrack --ctstate INVALID -j DROP.

iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

Temporary ban with timeout (self-expiring set)

low ✓

Purpose: block an address temporarily: add it to a set with a 3600 s timeout; entries expire on their own after 1 hour, with no manual intervention. Why this order: create the set first (hash:ip with timeout 3600), add the IP, then the DROP rule; the rule sits before the general DROP policy. A timeout set is ideal for short bans: it cleans itself up. Verify: ipset list temp-block (shows the IP with the remaining expiry counter) and iptables -L INPUT -n -v --line-numbers (the counter on the DROP rule). Undo: iptables -D INPUT -m set --match-set temp-block src -j DROP, then ipset destroy temp-block. Warning: 203.0.113.50 is a documentation IP; run it from a second session. Any other IP added to the set is blocked the same way, for 1 hour. The set default timeout (3600 s) applies to every new entry; a single entry can have a different timeout (ipset add temp-block 203.0.113.50 timeout 600).

ipset create temp-block hash:ip timeout 3600
ipset add temp-block 203.0.113.50
iptables -I INPUT -m set --match-set temp-block src -j DROP

Stateful base (set the DROP policy LAST)

medium ✓

The foundation for any host: allow loopback and the already established/related connections, then SSH. Set DROP only at the end, so you do not lock yourself out.

iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
iptables -P INPUT DROP

Block one IP on a single port

low ✓

Purpose: block a documentation IP on one port only (deny it HTTPS, but let it do the rest). Why this order: the rule is specific (one IP, one port), so it can sit anywhere before the general ACCEPT rules. Verify: iptables -L INPUT -n -v --line-numbers. Undo: iptables -D INPUT -s 203.0.113.50 -p tcp --dport 443 -j DROP (or by line number).

iptables -I INPUT -s 203.0.113.50 -p tcp --dport 443 -j DROP

Block an IP or a network

low ✓

Insert a DROP rule for an IP or a /24 network. Documentation IP.

iptables -I INPUT -s 203.0.113.50 -j DROP
iptables -I INPUT -s 203.0.113.0/24 -j DROP

Only one IP on a port, the rest blocked

medium ✓

Purpose: a port accepts traffic from a single admin IP only; the rest on that port is blocked. Why this order: the ACCEPT for the permitted IP comes first. The DROP with ! -s can never affect the permitted IP, so the recipe stays correct even if the order changes. Verify: iptables -L INPUT -n -v --line-numbers. Undo: delete both rules with -D (same specification or line numbers). Warning: rules in INPUT do not filter Docker's published ports; for containers use DOCKER-USER.

iptables -I INPUT -p tcp --dport 8080 -s 192.168.1.10 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 ! -s 192.168.1.10 -j DROP

Docker: containers cannot reach the LAN (a DOCKER-USER rule)

medium ✓

Purpose: containers (172.17.0.0/16) can no longer initiate NEW connections toward the LAN 192.168.1.0/24; responses to already-established connections (ESTABLISHED) pass. Why this order: the rule is in DOCKER-USER, before Docker own rules, so it stops container-to-LAN traffic before FORWARD routes it. -m conntrack --ctstate NEW is required: without it, the RESPONSE packets to connections initiated from the LAN toward a published port would also be dropped, so a LAN client could no longer reach the container. Verify: iptables -L DOCKER-USER -n -v --line-numbers (the counter on the DROP rule grows when a container initiates a new connection toward the LAN) and a curl test from inside a container to a LAN host (must fail). Mental test: a client from the LAN must still be able to reach the container published port (the connection is initiated from the LAN, the response is ESTABLISHED and passes). Undo: iptables -D DOCKER-USER -s 172.17.0.0/16 -d 192.168.1.0/24 -m conntrack --ctstate NEW -j DROP. Warning: 172.17.0.0/16 (the docker0 network) and 192.168.1.0/24 are examples — use your real subnets. If you have extra bridge networks, add a rule for each. --ctstate NEW means the rule blocks only initiation from the container, not the response.

iptables -I DOCKER-USER -s 172.17.0.0/16 -d 192.168.1.0/24 -m conntrack --ctstate NEW -j DROP

Docker: why INPUT does not filter published ports (a FORWARD diagnosis)

low ✓

Purpose: show concretely why rules in INPUT do NOT filter Docker published ports: traffic that reaches a published port is routed to the container (after DNAT the destination is the bridge IP), so it goes through FORWARD, not INPUT. Why this order: a LOG rule is placed in FORWARD on the outgoing interface toward the container (docker0); when you hit the published port, the packet appears here, while an equivalent INPUT rule would stay at 0 packets. LOG with a limit, so it does not flood the log. Verify: hit the published port, then iptables -L FORWARD -n -v --line-numbers (the counter on the LOG rule grows) and the kernel log (lines with the IPT-DOCKER-FWD: prefix). Undo: delete the FORWARD LOG rule (same specification as in commands). Warning: docker0 is the default bridge name; use your own bridge. If you want to filter the published port, do it in DOCKER-USER or FORWARD, not INPUT.

iptables -I FORWARD -o docker0 -m limit --limit 5/min -j LOG --log-prefix "IPT-DOCKER-FWD: " --log-level 4

Docker: a published port reachable only from the LAN (ctorigdstport)

medium ✓

Purpose: published port 80 (container) is reachable only from the LAN 192.168.1.0/24; anything that does not come from the LAN is dropped in DOCKER-USER. Why this order: in DOCKER-USER the packet has ALREADY gone through DNAT, so --dport would be the container port, not the published one; for the published port use -m conntrack --ctorigdstport (per the Docker docs, Packet filtering and firewalls). A single DROP rule with ! -s: it drops everything that does not come from the LAN, and the LAN traffic keeps going without an explicit ACCEPT. Verify: iptables -L DOCKER-USER -n -v --line-numbers (the counter on the DROP rule) and a curl test from the LAN (works) and from outside (does not). Undo: iptables -D DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 80 ! -s 192.168.1.0/24 -j DROP. Warning: eth0 (the incoming interface), 192.168.1.0/24 and 80 are examples — use your own interface, LAN and port. An explicit -j ACCEPT in DOCKER-USER bypasses Docker own rules; if you want the packet to keep going (to the Docker rules), use -j RETURN. The rule is in DOCKER-USER, not INPUT: rules in INPUT do NOT filter Docker published ports (the traffic goes through FORWARD to the container).

iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 80 ! -s 192.168.1.0/24 -j DROP

Docker: rules for published ports

medium ✓

Your rules for published ports go in the DOCKER-USER chain. Docker manages the rest of the rules itself.

iptables -I DOCKER-USER -s 203.0.113.0/24 -j DROP

Ignore ICMP broadcast + disable source routing (IPv4 and IPv6)

low ✓

Purpose: close two attack and routing paths: the host ignores responses to ICMP broadcast (Smurf protection) and no longer accepts source routing (packets with the source-route option no longer change the path). Why this order: the three keys are independent, order does not matter; but all are set at once, so they can be reviewed together. accept_source_route is separate for IPv4 and IPv6, so there are three keys in total. On many modern distributions these values are already the safe ones; the recipe fixes them explicitly, so you do not depend on the distribution defaults. Verify: sysctl net.ipv4.icmp_echo_ignore_broadcasts (value 1) and sysctl net.ipv4.conf.all.accept_source_route net.ipv6.conf.all.accept_source_route (both 0). Undo: record the current values first (sysctl net.ipv4.icmp_echo_ignore_broadcasts net.ipv4.conf.all.accept_source_route net.ipv6.conf.all.accept_source_route) and restore them with sysctl -w for each key (on many kernels icmp_echo_ignore_broadcasts is already 1 by default, so restoring the defaults is not always 0). Warning: sysctl -w does NOT persist across reboot — see the /etc/sysctl.d/ persistence recipe.

sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
sysctl -w net.ipv4.conf.all.accept_source_route=0
sysctl -w net.ipv6.conf.all.accept_source_route=0

ip_forward=1 for NAT, with persistence in /etc/sysctl.d/

medium ✓

Purpose: enable packet forwarding at the kernel level (required for NAT and port forwarding) and make it persistent in /etc/sysctl.d/, so it survives a reboot. Why this order: first you write the file in /etc/sysctl.d/ (via tee, so you see the content on screen), then you run sysctl --system, which applies all the files in /etc/sysctl.d/ in the correct order and validates the syntax. sysctl -w does NOT persist across reboot; only the file in /etc/sysctl.d/ survives. Verify: sysctl net.ipv4.ip_forward (value 1) and, after a test reboot, still 1 (not 0). Undo: sysctl -w net.ipv4.ip_forward=0 and delete or comment the line in /etc/sysctl.d/99-ipv4-forward.conf, then sysctl --system. Warning: you are enabling forwarding on a host that may not need to route — if you only want a plain server, leave it at 0. The filename 99-ipv4-forward.conf is chosen so it is read toward the end; name it as you prefer, as long as it is in /etc/sysctl.d/ with the .conf extension. sudo appears only here (so tee can write to /etc); the other commands in the site assume root.

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-ipv4-forward.conf
sudo sysctl --system

SSH limiting with connlimit (max 3 parallel connections)

medium ✓

Purpose: allow at most 3 parallel SSH connections from a single source; the rest of the new ones are dropped. Why this order: the DROP rule for above the limit sits before the ACCEPT, so it stops new connections past the ceiling. connlimit counts the connections already tracked by conntrack from the same source address (default mask /32) to port 22; the fourth simultaneous connection is dropped. Verify: iptables -L INPUT -n -v --line-numbers. Undo: iptables -D INPUT -p tcp --dport 22 --syn -m connlimit --connlimit-above 3 -m conntrack --ctstate NEW -j DROP. Warning: test it from a second SSH session.

iptables -A INPUT -p tcp --dport 22 --syn -m connlimit --connlimit-above 3 -m conntrack --ctstate NEW -j DROP
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

SSH limiting with recent (before the ACCEPT rule for SSH)

medium ✓

Block if more than 3 new SSH connections arrive in 60 s from the same source.

iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP

SYN rate limiting with a custom chain (anti-flood)

medium ✓

Purpose: limit the SYN rate (anti-flood) without canceling the DROP policy for ports. Why this order: SYNs within the limit return (RETURN) and are evaluated by the normal rules; what exceeds the limit is dropped by the DROP in the custom chain. The jump -A INPUT -p tcp --syn -j SYN_FLOOD must be before the ACCEPT rules for ports; otherwise the per-port ACCEPTs would let through any in-rate SYN, even on ports that should not be open. Verify: iptables -L INPUT -n -v --line-numbers and iptables -L SYN_FLOOD -n -v --line-numbers. Undo, in this order: iptables -D INPUT -p tcp --syn -j SYN_FLOOD, then iptables -F SYN_FLOOD, then iptables -X SYN_FLOOD. Warning: the limit applies to all SYNs at once, so set it above your real legitimate traffic peak.

iptables -N SYN_FLOOD
iptables -A SYN_FLOOD -m limit --limit 25/second --limit-burst 50 -j RETURN
iptables -A SYN_FLOOD -j DROP
iptables -A INPUT -p tcp --syn -j SYN_FLOOD

Large blocklist with ipset

medium ✓

For large lists of IPs, use ipset (hash:ip) instead of individual rules.

ipset create blocklist hash:ip
ipset add blocklist 203.0.113.50
iptables -I INPUT -m set --match-set blocklist src -j DROP

Logging + DROP on a closed port (e.g. 25, SMTP)

low ✓

Purpose: you close a port (25, SMTP), but you want to see who tries to connect. Why this order: the LOG rule sits before the port's DROP, so each dropped new connection is recorded first; LOG does not stop processing, so the packet still reaches DROP. The 5/min limit keeps the log from flooding. Verify: iptables -L INPUT -n -v --line-numbers (the counter on the LOG rule) and the kernel log, visible via dmesg or journalctl -k (lines with the IPT-SMTP: prefix). Undo: delete both rules with -D (same specification).

iptables -A INPUT -p tcp --dport 25 -m conntrack --ctstate NEW -m limit --limit 5/min -j LOG --log-prefix "IPT-SMTP: " --log-level 4
iptables -A INPUT -p tcp --dport 25 -m conntrack --ctstate NEW -j DROP

Logging new SSH connections, with a prefix

low ✓

Purpose: record who is trying SSH before the rate-limit rule (recent) drops them. Why this order: -I puts the LOG rule first (position 1), so every new attempt on port 22 is recorded, whether it is accepted or dropped by the rules below; with -A the rule would be added at the end, after the recent DROP, and the very dropped connections would not be seen. Verify: iptables -L INPUT -n -v --line-numbers (the counter on the LOG rule) and the kernel log, visible via dmesg or journalctl -k (lines with the IPT-SSH: prefix). Undo: iptables -D INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m limit --limit 3/min -j LOG --log-prefix "IPT-SSH: " --log-level 4. Warning: the 3/min limit means that during an attack only the first 3 lines per minute are visible.

iptables -I INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m limit --limit 3/min -j LOG --log-prefix "IPT-SSH: " --log-level 4

Rate-limited logging (before the DROP policy)

low ✓

Purpose: write a limited number of packets to the kernel log so you can see who is blocked by the default policy. This recipe assumes the INPUT DROP policy: the LOG rule must be the LAST rule in the chain, so it only sees the packets the policy drops. Why this order: LOG is appended with -A at the end, so it is the last rule in INPUT; packets that match no ACCEPT rule pass through LOG (which does not stop processing) and are then dropped by the DROP policy. The 5/min limit keeps the log from flooding. Verify: iptables -L INPUT -n -v --line-numbers (the counter on the LOG rule) and the kernel log, visible via dmesg or journalctl -k (lines with the IPT-DROP: prefix). Undo: iptables -D INPUT -m limit --limit 5/min -j LOG --log-prefix "IPT-DROP: " --log-level 4.

iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPT-DROP: " --log-level 4

Outbound NAT (masquerade)

medium ✓

The host acts as a router: masquerade on the outgoing interface + forwarding enabled.

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
sysctl -w net.ipv4.ip_forward=1

Do not answer ping (from any source)

low ✓

Purpose: the host does not answer ping (echo-request) from any source, including the local network. Why this order: insert the rule at the top with -I, so it drops echo-request before any general ICMP ACCEPT (for example the limited-ping rule); if it sat after an echo-request ACCEPT, it would have no effect. Verify: iptables -L INPUT -n -v --line-numbers. Undo: iptables -D INPUT -p icmp --icmp-type echo-request -j DROP (or by line number). Warning: it also blocks ping from the local network; for LAN-only ping see the next recipe.

iptables -I INPUT -p icmp --icmp-type echo-request -j DROP

Allow SSH only from an ipset (the management network)

medium ✓

Purpose: port 22 accepts new connections only from the management network (192.168.1.0/24), kept in an ipset (hash:net) instead of a -s rule. Why this order: create the set first (hash:net stores CIDR networks), add the network, then the -m set --match-set ... src -j ACCEPT rule; the rule sits before the DROP policy (the recipe assumes the INPUT DROP policy). Verify: ipset list admin-subs (shows 192.168.1.0/24) and iptables -L INPUT -n -v --line-numbers (the counter on the ACCEPT rule). Undo: iptables -D INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m set --match-set admin-subs src -j ACCEPT, then ipset destroy admin-subs. Warning: 192.168.1.0/24 is an example — use your own management network. The undo order matters: delete the iptables rule first, then ipset destroy; otherwise ipset refuses (error 'Set cannot be destroyed: it is in use by a kernel component') because the set is bound to a rule.

ipset create admin-subs hash:net
ipset add admin-subs 192.168.1.0/24
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m set --match-set admin-subs src -j ACCEPT

Limited ping

low ✓

Allow echo-request with a limit (1/s, burst 5) — the rest is not explicitly accepted.

iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/second --limit-burst 5 -j ACCEPT

Ping allowed only from the LAN

low ✓

Purpose: ping is accepted only from the local network (192.168.1.0/24); from outside it is dropped. Why this order: the first rule (position 1) lets LAN echo-request through; the second rule (position 2) drops every other echo-request. The first rule must sit before the second, otherwise local ping is dropped too. Verify: iptables -L INPUT -n -v --line-numbers. Undo: delete both rules with -D (same specification), in the same order as they were added.

iptables -I INPUT -p icmp --icmp-type echo-request -s 192.168.1.0/24 -j ACCEPT
iptables -I INPUT 2 -p icmp --icmp-type echo-request -j DROP

Port forward (DNAT + FORWARD)

medium ✓

Purpose: redirect a port on the external interface (8080) to an internal host (192.168.1.10:80) and let the FORWARD traffic pass in both directions. Why this order: the ESTABLISHED,RELATED rule is first in FORWARD, so the server→client responses pass (they have the source 192.168.1.10 and the destination the client, so they do not match the client→server rule). After DNAT, the destination port in FORWARD is 80, not 8080, so the client→server ACCEPT rule uses --dport 80. Verify: iptables -t nat -L PREROUTING -n -v --line-numbers and iptables -L FORWARD -n -v --line-numbers; a curl http://server:8080 test from outside. Undo, in reverse order: delete the client→server ACCEPT rule, then the ESTABLISHED,RELATED rule, then the DNAT rule (each with -D, same specification). Warning: net.ipv4.ip_forward=1 is required, otherwise the traffic is not routed. If the internal server does not have this machine as its default gateway, the responses do not come back through it; in that case you also need SNAT/MASQUERADE on the internal interface. eth0, 8080 and 192.168.1.10 are examples — use your own values.

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.10:80
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i eth0 -d 192.168.1.10 -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT

REDIRECT 80 to 8080 (local)

medium ✓

Purpose: traffic that reaches this host's port 80 is moved locally to port 8080 (an app on a non-standard port). Why this order: REDIRECT is a form of DNAT to the same host; it goes in PREROUTING (for incoming traffic). It is local traffic, so it needs no ip_forward and does not affect the FORWARD rules. Verify: iptables -t nat -L PREROUTING -n -v --line-numbers and a connection test on port 80. Undo: iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080. Warning: REDIRECT happens in PREROUTING, before INPUT, so by the time the packet reaches INPUT its destination port is already 8080: with the INPUT policy DROP you must accept 8080, not 80 (iptables -A INPUT -p tcp --dport 8080 -m conntrack --ctstate NEW -j ACCEPT). Warning: PREROUTING does not apply to connections generated by this host itself (they go through OUTPUT); a local curl localhost:80 test does not exercise the rule.

iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080

rp_filter loose (does not break asymmetric routing)

low ✓

Purpose: filter packets whose source IP cannot be reached on the interface they arrived on (rp_filter, the reverse path). Value 2 (loose) accepts if the source is reachable via ANY interface; 1 (strict) requires exactly the interface the packet came in on. Why this order: value 2 is recommended for hosts with asymmetric routing (e.g. multiple routes or load balancing): with 1 strict, legitimate packets can be dropped precisely because the source is reached through a different interface, so 1 strict can break asymmetric routing. Here you apply 2 so it does not break your traffic. Verify: sysctl net.ipv4.conf.all.rp_filter (value 2) and, on a concrete interface, sysctl net.ipv4.conf.eth0.rp_filter. Undo: record the current value first (sysctl net.ipv4.conf.all.rp_filter) and restore it with sysctl -w net.ipv4.conf.all.rp_filter=<value> (0 only if you disable the filter entirely). Warning: with a simple symmetric setup, 1 strict is tighter; but on a host with several interfaces or routes, 1 may drop legitimate traffic. The kernel uses the LARGEST value between net.ipv4.conf.all.rp_filter and the per-interface one. sysctl -w does NOT persist across reboot — see the /etc/sysctl.d/ persistence recipe.

sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.default.rp_filter=2

Save and restore ipset sets (persistence)

low ✓

Purpose: ipset sets are lost on reboot, and the iptables rules that use them fail if the set does not exist; save and restore them manually as a file. Why this order: save the sets to a file (ipset save > file) and restore them with ipset restore < file; the RESTORE happens BEFORE any iptables rule that references the set, otherwise the rule does not apply (the kernel refuses to bind a rule to a missing set). Verify: ipset list (all restored sets, with their entries) and iptables -S | grep match-set (rules bound to sets, no errors). Undo: delete the file /etc/ipset.save (or your path) and, if you want, ipset flush (empties the entries, keeps the sets). Warning: for automatic persistence on boot, run ipset restore at startup (a systemd unit or a script in /etc/network/if-up.d) and iptables-restore after it; the order is: ipset restore first, then iptables-restore. ipset restore fails if the set already exists; for that case use ipset restore -exist < /etc/ipset.save. The > and < redirections need root (the file is in /etc).

ipset save > /etc/ipset.save
ipset restore < /etc/ipset.save

Odd scans (unusual TCP flags)

low ✓

Block packets with TCP flag combinations that are not used in normal traffic.

iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP
iptables -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP

Web server on the stateful base (HTTP/HTTPS)

high ✓

Purpose: extend the stateful base with the web ports for an internet server. Why this order: iptables evaluates rules top-down, first match wins. Loopback, ESTABLISHED/RELATED, SSH and ports 80/443 must pass before the DROP policy; set -P INPUT DROP earlier and you lock yourself out. If you already have the base rules, add only the lines for ports 80 and 443 before the DROP policy; the policy is set once, at the end. Verify: iptables -L -n -v --line-numbers. Undo: first iptables -P INPUT ACCEPT, then delete the rules with -D (same specification or line number). Warning: run it from an already-open second SSH session, or you will be locked out.

iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
iptables -P INPUT DROP

SNAT with a fixed IP (LAN to the internet)

medium ✓

Purpose: packets from the local network go out to the internet with a fixed source address (the router's), not their internal ones. Why this order: the rule is in POSTROUTING (after routing), where the source can safely be rewritten; it applies only to traffic from 192.168.1.0/24 leaving via eth1. Fixed-IP SNAT is used when you have a static address (masquerade is for dynamic addresses). Verify: iptables -t nat -L POSTROUTING -n -v --line-numbers. Undo: iptables -t nat -D POSTROUTING -s 192.168.1.0/24 -o eth1 -j SNAT --to-source 203.0.113.1. Warning: also run sysctl -w net.ipv4.ip_forward=1 (forwarding enabled), otherwise the traffic is not routed at all. Warning: eth1 and 203.0.113.1 are examples — use your own interface and public address. Warning: if the FORWARD policy is DROP, you also need rules in FORWARD (LAN-to-outbound traffic and a general ESTABLISHED,RELATED rule so the responses pass); see the ESTABLISHED,RELATED rule in the port-forward recipe.

iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth1 -j SNAT --to-source 203.0.113.1

SSH only from the management network

high ✓

Purpose: new SSH connections are accepted only from the management network (192.168.1.0/24); the rest falls to the policy. Why this order: the SSH ACCEPT is limited to the management source and sits before the DROP policy; a new connection from outside matches no ACCEPT rule and is dropped by DROP (response packets pass via ESTABLISHED). Verify: iptables -L -n -v --line-numbers. Undo: delete the rule with -D (iptables -D INPUT -p tcp --dport 22 -s 192.168.1.0/24 -m conntrack --ctstate NEW -j ACCEPT) and, if needed, re-add the broad SSH rule. Warning: 192.168.1.0/24 is an example — use your own management network and run it from a second SSH session.

iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -m conntrack --ctstate NEW -j ACCEPT
iptables -P INPUT DROP

SSH on another port (new rule FIRST, then remove the old one)

high ✓

Purpose: move SSH from port 22 to 2222 without losing access. Steps, in order: 1. in sshd_config add Port 2222 IN ADDITION to Port 22, check it with sshd -t and restart the service; 2. run the command above (the ACCEPT rule on 2222); 3. from a second session test ssh -p 2222 user@server; 4. only once it works, delete the old rule (iptables -D INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT) and remove Port 22 from sshd_config. Why this order: the ACCEPT on 2222 is active and verified before you delete port 22; if you reverse it, you are locked out the moment the new rule does not work. Verify: iptables -L INPUT -n -v --line-numbers (2222 ACCEPT; 22 is gone only at the end). Undo: re-add the rule on 22 (iptables -I INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT) and, if needed, delete the one on 2222. Warning: run it all from an SSH session opened beforehand, on the old port.

iptables -I INPUT -p tcp --dport 2222 -m conntrack --ctstate NEW -j ACCEPT

SYN cookies

low ✓

Enable SYN cookies as protection against SYN floods (kernel level).

sysctl -w net.ipv4.tcp_syncookies=1