Network kernel hardening (sysctl)
No forwarding, no redirects, sane defaults.
Your Linux box ships with hundreds of network settings already turned to whatever value keeps the most hardware and the most networks working on the first day. Compatibility first, safety second. That trade made sense for an office network in 1998. It is the wrong default for a server with a public address, sitting in a data center, being scanned by strangers every minute.
Think of the kernel (the core of the operating system, the part that talks straight to the hardware and moves every packet in and out) as a wall of dials behind a panel. Each dial changes how the machine treats one kind of traffic. sysctl (short for system control) is the panel you use to turn them. A small set of those dials, set right, takes a real slice of attack surface off the table. This lesson is the network ones that matter: what each dial stops an attacker from doing, and how you prove it took effect.
How sysctl Actually Works
Every dial is a file. The kernel exposes them under /proc/sys, a virtual folder that holds no real files on disk. It is a live window into the running kernel. Read a file there and you read the current value. Write to it and you change the kernel's behavior that instant.
sysctl is a friendlier front door to the same files. It swaps the slashes for dots, so /proc/sys/net/ipv4/ip_forward becomes net.ipv4.ip_forward. There are two ways to change a dial, and the difference is the whole game. Running sudo sysctl -w net.ipv4.ip_forward=0 flips it right now and forgets it on the next reboot. A line in a file under /etc/sysctl.d/ is permanent, because the system reads that folder every time it boots. For hardening you always want the file. A setting that disappears on reboot is a setting you cannot trust.
Stop Forwarding Packets You Never Meant To
A mail room in a big office sorts letters for its own building. It does not take a bag of mail addressed to the building next door and quietly relay it across town. A router does exactly that relaying, for packets: it takes traffic that arrived on one network card and sends it out another, moving it between networks. That is a router's entire job. Your web server, your database, your build runner: none of them should do it. A packet that is not addressed to them should be dropped, not passed along.
The dial is net.ipv4.ip_forward, and here is why an attacker cares whether it is on. Say they get a foothold on a public-facing box in your DMZ (demilitarized zone, the exposed network segment that faces the internet). That box can reach your internal network. The internet cannot. If forwarding is on, the attacker turns the box into a router and rides it straight inward, using your own host as the bridge from the internet to the systems that were supposed to be unreachable. Forwarding off, and the box refuses to relay. The foothold stays a dead end.
On a plain server that should read 0. A 1 on a machine that is not a router means something switched it on, and you want to know what. The usual culprit is a container runtime like Docker or a VPN (virtual private network) daemon. Both enable forwarding so their own traffic works, and both leave it on for every other process on the box.
So before you force it to 0, find out what is using it. On a container host, slamming ip_forward to 0 breaks the networking your pods rely on. Track down the service that genuinely needs forwarding, scope the setting to that service if you can, and set it to 0 on every box where routing is not the host's job. The reason this is worth the care is the pivot. A non-router host with forwarding left on becomes a router the moment someone compromises it, and from there they reach the machines that host was never supposed to touch.
Redirects and Source Routes: Who Gets to Pick the Path
An ICMP redirect (Internet Control Message Protocol, the network's built-in messaging channel, the same one ping rides on) is a note from a router that says: there is a better next hop for that destination, send it this way instead. On a network you trust, it is a small routing optimization. On a hostile network it is a forged detour sign. Anyone sharing your subnet can send your host a redirect that means 'route everything through me,' and a host that obeys has handed that attacker the man-in-the-middle (MITM) seat, between you and everywhere your traffic was headed, free to read it and change it.
So you tell the host to ignore redirects (net.ipv4.conf.all.accept_redirects = 0) and, for good measure, never to send them either (send_redirects = 0), because only routers have any business emitting them. Source routing is the older, blunter cousin. A source-routed packet carries its own itinerary: the sender writes the exact path it wants through the network, and cooperating hosts follow the instructions. That lets an attacker steer packets around firewalls and around the very return-path checks you are about to turn on. A normal host has no reason to honor it, so you reject it outright.
Spoofed Sources: Reverse-Path Filtering and Martians
Every packet carries a source address (its IP address, short for Internet Protocol, the numbering scheme that gives every machine on a network its address), like the return address written on an envelope. Nothing forces that address to be true. Attackers forge it all day, to stay hidden, or to make the replies land on a victim who never asked for them. Reverse-path filtering is the kernel checking the return address against the map.
When a packet arrives on eth0 (the name Linux gives a network card) claiming to come from some address, the kernel asks one question: if I had to reply to that address, would the reply go back out eth0? If not, the claimed source could not really live out there, and the packet is a fake. rp_filter (reverse path filter) makes the kernel drop it. Value 1 is strict mode, where the reply must return on the same interface the packet came in. Value 2 is loose mode, where the address only has to be reachable out some interface. For an ordinary server with one network card, strict is what you want.
That drop is silent, so its partner is logging. A letter postmarked from a town no mail truck ever passes through carries an impossible return address, and you would flag it the second you saw it. A martian packet is the network's version. Its source address could never honestly appear where it did: a private 10.x.x.x address arriving from the public internet, or a packet claiming to come from your own IP address. net.ipv4.conf.all.log_martians = 1 writes a line to the kernel log every time one shows up. That is your tripwire. Forged-source traffic stops being invisible and starts landing in the journal, where your alerting can pick it up.
SYN Floods and Broadcast Amplifiers
Opening a TCP connection (Transmission Control Protocol, the reliable connection layer under almost everything you use: web, SSH which is Secure Shell for encrypted remote login, databases) is a three-step handshake. The client sends a SYN packet (synchronize, meaning 'I would like to talk'). The server replies SYN-ACK ('go ahead') and sets aside a small slot of memory to remember this half-finished connection. The client sends the final ACK (acknowledge), and the connection is live.
A SYN flood is a prank caller booking a thousand restaurant tables and showing up for none. The attacker fires SYN packets and never sends the closing ACK. Each one takes a slot in the backlog queue, the fixed-size waiting room for half-open connections. Fill the queue and there is no space left for real clients, so their connections get refused. The server is up, and unreachable.
net.ipv4.tcp_syncookies = 1 changes the rules under attack. A busy cloakroom that has run out of hooks does not have to turn people away. It can hand each arrival a numbered ticket that encodes everything about their coat, keep no rack space at all, and reunite coat and owner when the ticket comes back. That is the move. Instead of parking the half-open connection in the queue, the kernel packs everything it needs to know about that connection into the sequence number it sends back in the SYN-ACK, a value computed from the connection's details and a server secret. It stores nothing. When a real client returns the final ACK, that number comes back with it, the kernel checks the math, and rebuilds the connection from the cookie alone. A fake client never sends an ACK, so ignoring it cost the server nothing. The queue cannot be drained because, under pressure, the kernel stops depending on it.
One more dial, cheap and worth setting. net.ipv4.icmp_echo_ignore_broadcasts = 1 stops your host from answering a ping aimed at the whole subnet's broadcast address. Leave it answering, and an attacker sends a single ping to that broadcast address with a victim's address forged as the source. Every machine on the segment replies to the victim at once. That is a smurf attack, and your host is one of the amplifiers pounding a stranger on the attacker's behalf. Turn it off and your host sits the attack out.
Apply, Persist, and Verify
Here is the whole set in one file. The numeric prefix in the filename (90) decides when it is read, and setting both all and default covers a subtlety worth knowing: all combines with each existing interface, while default is the template every future interface inherits. Setting both is how you make sure a network card that appears later still comes up hardened.
# This host is not a router. Do not forward packets between interfaces.net.ipv4.ip_forward = 0net.ipv6.conf.all.forwarding = 0# Ignore ICMP redirects. A neighbor on your subnet can forge one and# silently reroute your traffic through their machine.net.ipv4.conf.all.accept_redirects = 0net.ipv4.conf.default.accept_redirects = 0net.ipv6.conf.all.accept_redirects = 0net.ipv6.conf.default.accept_redirects = 0# Only routers should send redirects. This host should not.net.ipv4.conf.all.send_redirects = 0net.ipv4.conf.default.send_redirects = 0# Reject source-routed packets. The sender does not get to pick the path.net.ipv4.conf.all.accept_source_route = 0net.ipv4.conf.default.accept_source_route = 0net.ipv6.conf.all.accept_source_route = 0net.ipv6.conf.default.accept_source_route = 0# Drop packets whose source address could not have arrived on this# interface (reverse-path filtering, strict mode).net.ipv4.conf.all.rp_filter = 1net.ipv4.conf.default.rp_filter = 1# Keep answering real clients during a SYN flood.net.ipv4.tcp_syncookies = 1# Log impossible-source packets so spoofing leaves a trail.net.ipv4.conf.all.log_martians = 1net.ipv4.conf.default.log_martians = 1# Do not answer pings sent to a broadcast address (smurf amplification).net.ipv4.icmp_echo_ignore_broadcasts = 1# Do not react to bogus ICMP error messages.net.ipv4.icmp_ignore_bogus_error_responses = 1
Load it without rebooting, then read the values back. sysctl --system walks every sysctl directory in order and applies what it finds.
Those files are read in filename order, and when two of them set the same key, the one that sorts last wins. That is why hardening lives in a high-numbered file like 90 or 99: a lower-numbered default somewhere else cannot quietly overrule it. These are the same checks a CIS benchmark (the Center for Internet Security's published hardening baselines) flags one by one, because their absence is common and their presence is close to free. Now confirm the ones you care about actually took.
Setting these once is the easy part. The dial you fixed today can flip back the day someone installs Docker on the box, or a VPN client, or a colleague copies a 'quick fix' off a forum. Read the values on a schedule (a nightly sysctl check piped into your monitoring, or a drift check in Ansible or whatever config management you run) so the box tells you the moment ip_forward goes back to 1. You want to hear about that the night it happens, not the morning after someone pivots through it.
Try this
Work through “Apply, Persist, and Verify” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: strict rp_filter can drop real traffic on multi-homed hosts. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.