Contents
- 1 Introduction
- 2. Why are traditional iptables tutorials so difficult to understand?
- 3. The "path" of data packets in the kernel“
- 4. The so-called "4 tables and 5 chains" are actually functional areas on the roadmap.
- 5. How iptables works with other systems
- 6. Based on real-world needs, write an iptables script that won't cause problems.
- 7. Performance and Scalability Principles of iptables
- 8 Commonly Used iptables Commands: These are the ones you'll actually use before writing rules.
- 9. Postscript: A letter to my future self (and to you who may not understand iptables yet).
1 Introduction
Actually, I've wanted to write an article about iptables for a long time. Although I don't use it often, every time I do need to use it, I find that I've completely forgotten the relevant knowledge points and have to read them all over again. This is the tragic consequence of learning a certain knowledge point without reinforcing my memory through writing.
But why do I keep wanting to write about iptables, yet always hesitate to put pen to paper? The reason is simple: articles about iptables are really difficult to write. You can easily end up with one of those "user manual" tutorials you find online, piling up a bunch of chains, tables, and rules, leaving you completely confused while reading and your head spinning afterward—that's what happened to me before, getting lost in all sorts of explanations.
So the tragedy happened—when I first bought the Racknerd Chicago VPS, I had a very clear security strategy: only two high-level ports were open (SSH and Transmission for key-based login), all inbound requests were dropped, and all internal services were managed via tailscale IPs, making it seem impregnable. Therefore, I used the simplest username and password for my MariaDB database (wordpress/wordpress), since the VPS didn't expose port 3306, so anything would be safe. But the very next day, my MariaDB WordPress database was deleted! I was truly slapped in the face!
After investigation, the culprit turned out to be an inconspicuous Docker creation parameter: `-p 3306:3306`. Just that casually typed it, and the database was exposed to the whole world. At that moment, I was completely dumbfounded: I had clearly locked down the system firewall tightly, so how could Docker easily bypass it?
It wasn't until then that I truly realized that what I thought I understood about iptables was actually just knowing some "commands," but I had no idea how the traffic actually moved, why it was like this, or what the system and Docker were doing behind the scenes.
The biggest problem with traditional iptables tutorials is this: they tell you what tables exist, what chains exist, and what various rules mean, but they don't tell you the order in which things happen, nor do they tell you that what truly affects actual behavior is the "traffic path".
For example: Where does the packet come from? Which chain does it pass through first? Which rules are built into the system? Which ones are secretly inserted by Docker? Why does the painstakingly written DROP function never have a chance to take effect?
Traditional tutorials assume you'll "understand it on your own," but the reality is most people simply don't, just like I was back then—Docker automatically added a DNAT rule to the host machine, forwarding external 3306 traffic, and then added an ACCEPT rule, effectively bypassing my input firewall. My initially confident "flawless" security solution didn't even meet the requirements of the rules Docker had installed.
This incident made me realize that iptables is not a "command-line tool" at all, but a "traffic routing map." What truly matters is not which rules you write, but the order in which packets pass through these chains. If you don't understand this path, you can't truly control the firewall.
Therefore, for this article, I want to adopt a writing style that is more suitable for ordinary people and more in line with my own logic: starting from real incidents and real needs, I want to clearly explain the traffic path of iptables so that people can treat it as a map in their minds, rather than a manual piled on the table.
I hope that after reading this, you'll have an "Ah, that explains why I always felt like I only had a vague understanding" feeling. For me personally, this article is also a way to "completely embed knowledge into long-term memory," avoiding another incident where I relied on luck to bypass the firewall.
2. Why are traditional iptables tutorials so difficult to understand?
If you've ever searched for iptables tutorials online, you've probably had the same experience: the articles are usually very detailed, explaining the four tables, five chains, and every parameter clearly, making them seem quite comprehensive, but after reading them, your mind is still a mess. You might remember the names INPUT, FORWARD, and OUTPUT, know what PREROUTING is, and vaguely understand what NAT, Filter, and Mangle are responsible for, but when it comes to actually configuring the firewall, you'll still start getting confused from the very first line.
You might ask yourself: Why didn't the package get dropped even though I wrote DROP? Why did Docker seem to be able to just open the door from behind when I thought INPUT was the first line of defense? Why did the commands I typed according to the tutorial behave completely differently from what the tutorial explained? I was just as confused back then.
The problem isn't that you're not smart enough, or that you're not working hard enough. It's that most iptables tutorials start off by "teaching things backwards." They begin by telling you "there are four tables in the world," then "what chains are in each table," and then launch into a lecture on parameters and syntax, making you feel like you've mastered most of it. But in reality, this is just a fixed set of terms; it doesn't explain why the system does something, nor does it help you understand how real-world network traffic actually works.
It's like someone patiently teaching you how to drive, showing you what a steering wheel is, what the accelerator is, and what the brakes are, but completely ignoring how roads are planned, how traffic flows, which intersections have right-of-way, and which sections are one-way streets. You know all the parts of a vehicle, but you still can't drive because you don't understand the true behavioral logic of a car on the road.
The biggest problem with iptables is precisely this: it's not a "collection of commands," but rather a "traffic routing system." It doesn't handle commands, but rather data packets. Data packets are like cars: they enter from an inlet, pass through several "main roads," are diverted to a "side road," may be inspected, may be allowed to pass, or may have their destination changed. Your rules are simply signs placed at these intersections. If you don't know the order of the intersections, you'll never know if your sign is placed in a place no one will pass by.
Traditional tutorials often avoid discussing this point, telling you that INPUT is where inbound packets are intercepted, so you naturally treat INPUT as the "first line of defense." However, in real systems, packets have already passed through other places, such as PREROUTING, before reaching INPUT. Docker quietly inserts its own redirection rules in these "places you don't know about." By the time you see INPUT, everything has already been completed.
Even worse, most tutorials don't tell you that the system itself dynamically generates some rules. This means that you might think your DROP is the "top-level" rule, but in reality, the system has already inserted several ACCEPT rules in front of you, leaving your painstakingly written rules in an untouched corner. You think you made a mistake, but in fact, you started from the wrong position from the very first step.
This is why even if you know what a table is, what a chain is, what -A and -I are, you will still find yourself completely unable to predict the actual behavior of iptables—because you are never told how the whole world works.
Therefore, rather than saying traditional tutorials are difficult to learn, it's more accurate to say they don't enter this knowledge system from the "correct entry point." The world of iptables must be understood starting with "traffic paths," with "how data packets traverse the system," not with a jumble of terms. Terms are meaningless; paths are what matter. Without understanding the order, rules will forever remain empty talk.
In this article, I hope to steer the discussion back on the right track. It's not about explaining concepts or memorizing commands, but about first building that essential "traffic map" in your mind. Only by understanding that map can you truly understand iptables, why Docker can bypass INPUT, why your rules might fail, and why incidents occur.
Starting with this chapter, what we're going to do is clear away all the "fog" and let you see the full picture of iptables for the first time, instead of a bunch of parts scattered on your desktop.
3. The "path" of data packets in the kernel“
Before we begin this chapter, there is one thing that must be emphasized, because if you don't understand this, all the subsequent discussions of chains, tables, and hook points will be completely confusing:iptables is not actually responsible for "processing" any data packets.
It's more like an "administrator's remote control": you type a few commands, and it writes the rules into the kernel; the actual packet interception, packet release, NAT, and connection tracking are all done within the Linux kernel. netfilter Working.
Therefore, whether you use iptables, nftables, or firewalld, the essence is the same:User space (you) → issues commands → Kernel space (netfilter) → Kernel runs the actual traffic processing logic.
The INPUT, FORWARD, PREROUTING, and POSTROUTING strings you see are not actually iptables' "processes," but rather several key nodes of netfilter in the kernel. iptables simply inserts rules into these nodes.
The real problem with iptables isn't remembering the commands, but rather not having a clear roadmap in your mind of how data packets move within the kernel. You don't know when it will pause, which hook will handle it, when it will bypass your rules, or when it will be intercepted by mechanisms like Docker, NAT, and FORWARD. So you may understand the commands, but you don't understand what Linux is actually doing with your traffic.
I initially encountered problems with Docker's DNAT on my VPS because of this: I assumed an external request would always be "INPUT first" before being mapped by Docker; however, Linux actually goes to INPUT first. PREROUTING (nat table)And Docker quietly inserted DNAT here. In other words, Docker's changes happened before all my INPUT rules—which is completely different from the process I had in mind.
This is why "understanding the path" is far more important than "understanding the command." Because commands can be forgotten, but the route remains unchanged; once the route is thoroughly understood, all chains, tables, and rules become logical and clear.
If you could only choose one diagram to give you a quick overview of how Linux processes a data packet within the kernel, and where the links for iptables, netfilter, NAT, and Docker are located, it would be this one—it shows the entire "Linux kernel network stack" process, but we'll use the iptables chain as a reference point for our explanation:

Next, we'll start with this diagram and guide you through tracing data packets—from the moment they enter the network interface card, to when they are received by the local machine, forwarded by containers, or sent back to the network. You'll find that once you understand its path, iptables, netfilter, NAT, Docker, and even nftables later on, are no longer mysterious.
From NIC to PREROUTING: The first stop, the gateway to everything.
All incoming traffic, whether it's from web pages you visit or attackers scanning your ports, goes straight to the network card and then to the left side of the diagram. PREROUTING.
The firewall will proceed in the following order: raw, mangle, and nat (this is the most crucial). You can think of PREROUTING as the "real first gate." Do you think INPUT is the first point of attack for the firewall? That's a common misconception.
The real first stop is PREROUTING. A lot of cybersecurity tricks happen here—especially DNAT (port mapping). My accident that day was because Docker added something here:
DNAT --dport 3306 → Internal port of the container
Therefore, when attackers scan 3306, they never even reached the INPUT chain I carefully wrote; they were already being pulled into the container by Docker in PREROUTING.
The meaning of this passage is:If you don't understand PREROUTING, you'll never understand why no matter how nicely the INPUT chain is set up, it won't work.
Is the destination the local machine? Or should it be forwarded? This small section in the diagram represents the major fork in the iptables system.
The diagram shows a red diamond that reads "Destination is local machine," which is the dividing line for all routing actions: If the IP address is your VPS → Y → proceed to INPUT; if the IP address is not your local machine → N → proceed to FORWARD.
This seemingly simple statement leads many to remain perpetually confused: Why doesn't Docker traffic go into the INPUT? Why does Kubernetes Service NAT go through FORWARD? Why can port forwarding on a VPS bypass your INPUT? Why do packets from virtual bridges br0 and docker0 both end up in FORWARD?
If you only have a literal understanding of terms like "INPUT = inbound" and "OUTPUT = outbound" in your mind, you won't be able to truly understand these issues.
But once you see this diagram, you'll immediately realize that the Docker network isn't "coming to access my host," it's accessing the internal network forwarded by the docker0 bridge, naturally going through FORWARD.
Taking the Y branch: Entering the actual "local firewall"—INPUT
If the packet is indeed destined for your VPS IP, it will go to the INPUT section at the top of the image.
In INPUT, it goes through the following in order: mangle (generally ignored) and filter (most people write DROP here). This section is the traditional "VPS firewall rule area".
If you want to block a specific port, restrict a particular IP address, or prohibit a certain protocol, that's where you'll find the solution. But what's clear now is:INPUT is the second gate, not the first.
Therefore, the things it can filter out are subject to the following conditions:The destination address must be changed before PREROUTING.That's why I made a mistake before.
After INPUT is completed, the packet enters the kernel application layer, and then the process itself, such as sshd, nginx, or docker-proxy, goes up next.
Branching to N: Not targeting me? Then forward it—Forward Chain debuts.
If the destination address is not your local IP, the packet will enter the FORWARD on the right side of the diagram.
This also applies to: mangle, filter
Docker, container networking, Kubernetes, router forwarding, side routing... all of these go through this process.
This is why Docker inserts DOCKER and DOCKER-USER into FORWARD, and it inserts them at the very beginning of FORWARD, thus having mandatory priority.
Once you understand this diagram, you'll immediately understand:Any container traffic (including '-p 3306:3306') will go through Docker's PREROUTING and FORWARD in advance, and will not go through INPUT.
This is the fundamental reason why "it's impossible to protect the container with INPUT".
Packets leaving the local machine: OUTPUT → POSTROUTING
If your program sends a request to the public network, it will follow the output path shown at the top of the diagram. The process is: output (raw → mangle → NAT → filter) → POSTROUTING (mangle → NAT) → NIC → exit point
This section is usually quiet because most people don't set rules in OUTPUT. But if you want to understand SNAT/MASQUERADE, this is where it happens.
The short phrase mnemonic is:DNAT comes first (PREROUTING), SNAT comes second (POSTROUTING).
When Docker's bridged network forwards data, it executes MASQUERADE at the POSTROUTING stage.
After you've walked through the diagram, you'll truly understand iptables.
This diagram is essentially a "human anatomy diagram" of iptables. No matter how complex a command is written, as long as you can answer one question: In which part of the diagram is it executed? Then you will never get lost.
Now you know:
- INPUT is indeed not the first stop.
- PREROUTING is the critical path affecting Docker, port mapping, and reverse proxy.
- FORWARD is the main battleground for all "bridge/container/router forwarding" technologies.
- Postrouting is an essential step in SNAT.
- OUTPUT is only used for packets sent by the local program.
- The "binary search" between local and non-local processes directly determines the flow distribution between INPUT and FORWARD.
To summarize this chapter in one sentence:Don't memorize iptables; just walk through this diagram, and you'll understand.
4. The so-called "4 tables and 5 chains" are actually functional areas on the roadmap.
The most common obstacle for many people learning iptables isn't the commands, but rather the overwhelming array of terms: filter tables, nat tables, mangle tables, raw tables, and chains like INPUT, OUTPUT, FORWARD, PREROUTING, and POSTROUTING. Most tutorials like to dump all these terms on you right away, saying, "iptables is made up of these." But the problem is, without a mental roadmap of how data packets move within the kernel, these names are like chapter titles in an unfamiliar reference book—completely lacking intuitive meaning and only leading to more confusion.
To truly understand them, you must first realize a very crucial but often overlooked fact:Chains do not exist independently; they always belong to tables. What you see is not an abstract "PREROUTING chain", but rather PREROUTING in the nat table, PREROUTING in the mangle table, and PREROUTING in the raw table.
It is worth emphasizing that,The so-called "4 tables and 5 chains" is just the default starting point of iptables, not the complete form. These are more like a pre-drawn roadmap skeleton, telling you roughly which nodes a data packet will pass through in the kernel, but that doesn't mean these nodes only contain the rules you've written. In fact, iptables tables and chains are themselves extensible structures, allowing systems and applications to further refine logic, insert jumps, and build their own processing flows below these default locations.
Therefore, when you actually examine a running server, what you often see is not a "clean 4 tables and 5 chains," but a real traffic path diagram that has been continuously supplemented, split, and extended. The significance of understanding this lies not in memorizing which chains have been added, but in developing a correct intuition:iptables does not describe static rule tables, but rather a traffic processing framework that can be continuously edited.
Understanding this is crucial because it helps you develop the right intuition from the start: iptables is not a static form or a rigid conceptual framework, but rather a "traffic routing map" that is jointly edited by the system and applications. What you need to understand is not the terminology, but the actual path that data packets take; not memorizing table chains, but understanding who should handle the traffic and what operations should be performed at different nodes.
If you imagine the Linux kernel handling traffic as a highly specialized government building, it becomes much easier to understand. The building has four main halls (four tables): raw handles the initial registration, mangle handles metadata modification, nat handles address changes, and filter handles final approval. Each hall contains counters (chains) to handle "where the traffic came from and which counter it should go to." The rules are the specific actions you post on the counters: "If X occurs, execute Y."
And one point that is more easily overlooked is:The chains in each table are not the same; they are not symmetrical.
For example, the raw table only has PREROUTING and OUTPUT because the most basic registration only occurs when traffic enters the building and when traffic originates from within the local machine; the nat table naturally needs to set up counters in three places: PREROUTING (DNAT), OUTPUT (DNAT for traffic initiated by the local machine), and POSTROUTING (SNAT); the filter table only focuses on INPUT, FORWARD, and OUTPUT because the decision of "whether to allow or not" only needs to be made in these three scenarios; while the mangle table, because it modifies metadata, is used in a very wide range of scenarios and exists in almost all five chains:

If you view each table as a different business unit and each chain as a counter in a lobby, this distribution is no longer abstract, but rather logical. More importantly:These four tables are ordered and their priorities are hardcoded. The sequence of Linux processing a data packet is raw → mangle → nat → filter: first register (raw), then modify the metadata (mangle), then process the address (nat), and finally decide whether to allow it (filter).
You want to do DNAT in a filter? You can't. You want to skip connection tracking in NAT? That's the job of the raw code. You want to do the final clearance decision in a mangle? That belongs to the filter.
The order of processes determines where rules can be placed and whether they will take effect. This is why understanding the relationship between tables and chains is more important than memorizing commands.
Once you understand these prerequisite concepts, you'll look back at the traffic flow diagram and realize that the "4 tables and 5 chains" don't need to be memorized at all. They are naturally distributed according to the routes data packets take in the system, not terms invented by textbooks. As long as the routes are clear, you'll naturally develop an intuition for what each chain is and what each table is responsible for, just like you know where the entrance, exit, and tollbooth are even if you're not on a highway.
Once you understand it this way, iptables is no longer an "unmemorable jargon system," but a traffic system that fits the logic of reality: traffic will pass through different tables and different chains according to the route, and you just put the rules you want at each intersection.
5. How iptables works with other systems
In Chapter 4, we deliberately shifted our focus away from the "4-table, 5-chain glossary" and only created one...A roadmap skeleton showing how data packets travel within the kernel.But this route map isn't just for you—Any system running on this server that needs to handle network traffic will leave its mark on this graph..
This chapter will discuss where these "markers" come from and how they change the actual path of traffic without your knowledge.
Once you actually get your servers running, you'll find that iptables isn't a "standalone tool" at all—it's more like a public transportation hub. Any system that needs to handle network traffic—Docker, Podman, Kubernetes, Tailscale, even HAProxy, FRP, and Clash—will jump the queue, insert rules, preempt, and modify paths on the Linux kernel network stack. They won't consult you; they'll directly write their rules into various critical nodes of netfilter. If you're not careful, you might encounter incidents like services that worked fine yesterday becoming inaccessible today, or databases being inexplicably exposed.
The key point of this chapter is: you must view iptables as a "public hub," not your personal firewall. All systems use it simultaneously; you are just one of them.
The most typical example is Docker. Whenever you use the `-p` option to map a port, Docker writes a DNAT rule in `PREROUTING` to map your public port to the container; then it adds an `ACCEPT` rule in the `FORWARD` chain to allow packets to continue forwarding; finally, it adds a `MASQUERADE` rule in `POSTROUTING` to ensure that the container's return packets can be correctly sent out. Throughout this process, you don't write a single line of iptables, but Docker silently performs all the work of a router for you. So what you actually see is: your `INPUT` chain is empty, yet Docker's container is exposed to the public internet. This isn't a bug, but a natural result of Docker and you "sharing the same security system."
Tailscale operates on a similar logic. It takes over the routing table in the kernel and rewrites iptables rules, directing packets passing through its virtual network interface (tailscale0) to take specific paths. It even automatically handles NAT and reverse traffic forwarding. If you use Tailscale Funnel or Tailscale Serve, it will also create ephemeral ports on the local machine and redirect traffic from the user-space proxy back to the kernel. These steps are transparent to you, but they mean that Tailscale inserts its own rules into the routing and forwarding process, affecting the overall traffic path of your machine.
Now let's look at Kubernetes. In a K8s environment, iptables is the most problematic and best illustrates the concept of "collaborative work." Kubernetes automatically generates numerous chains on each node, such as KUBE-SERVICES, KUBE-PORTALS, and KUBE-MARK-DROP, using iptables to implement service clusterIP mapping, Pod SNAT, and service load balancing paths. If you have even a few dozen services, the iptables chains will automatically become extremely long, and every packet must pass through these chains. In such a system, your custom rules cannot be written directly in the main chain; they must "bypass" the entire K8s process. Otherwise, they will either have no effect or completely cut off service traffic.
Furthermore, regarding HAProxy and FRP, these two tools don't write iptables rules themselves, but they are often strongly bound to your firewall. For example, if you want HAProxy to only listen to internal IPs and direct external traffic through a certain proxy, then you must configure DNAT in PREROUTING to ensure that incoming external packets are forwarded to HAProxy by your local machine. If you don't understand this detail, you might think that "starting the proxy is enough," when in reality, the traffic doesn't even reach the destination you expect. Many people encounter situations where HAProxy can't forward traffic because they are tripped up by this pitfall of "lack of coordination between iptables and the service."
You'll also see some more "invisible" collaborators, such as OpenVPN, WireGuard, Clash, and Surge. These tools, in order to perform traffic hijacking or transparent proxying, write to the mangle table, mark entries, modify routes, and redirect traffic to a user-space proxy before sending it back to the kernel. They don't modify filter rules; they change routing decisions. If you don't understand what they're inserting into the mangle or routing table, you might think your input is correct, only to find that the traffic has been "intercepted" by the user-space proxy, circled back, and returned—completely different from what you expected.
All these systems together constitute one reality: any slightly complex server environment is using iptables simultaneously. You are not the only "firewall administrator." You are just one of all the users.
Once you understand this, you can truly take control of your system. Because you'll know: your rules must be written in the correct places; you'll know Docker will always populate the PREROUTING; you'll know Tailscale will compete for routes; you'll know Kubernetes will make FORWARD extremely complex; you'll know proxies and VPNs will affect mangles and routing tables; and you'll know that some systems are jumping the queue higher than you, and simply using INPUT won't stop them.
From this perspective, iptables itself isn't complex. What's complex is "who is using it with you." When you know which systems are modifying which nodes of netfilter, you can design truly reliable and controllable security policies, no longer getting overwhelmed by inexplicable traffic behavior.
The focus of this chapter is not to list which chains each system has implemented, but rather to help you understand that iptables in the modern Linux ecosystem is not a "standalone tool," but a shared platform. What you really need to manage are the modifications made to the kernel network stack by all systems, not looking at a single chain in isolation.
6. Based on real-world needs, write an iptables script that won't cause problems.
6.1 Using a real VPS scenario, we'll connect "tables, chains, and rules" into a truly usable solution.
Before actually starting to write iptables rules, there's a problem that's often overlooked: most people get stuck not on how to write the commands, but on not knowing where to begin. This is because many tutorials jump right in with tables, chains, and parameters, rarely taking a more practical approach—on a real server, what traffic do you actually want to let through, and what traffic do you want to block? So in this section, we won't touch on commands yet, but will start with a very common VPS use case.
Let's say you have a regular Linux VPS with a simple purpose: it provides web services to the public internet, requiring ports 80 and 443 to be open; you log in via SSH using a non-default port (e.g., 54331); and this machine runs Docker, with one container listening on port 3306 for database services (this port 3306 isn't for public access; it's just for other applications on the local machine, or containers on the same machine, to access the database through the host network. In other words, it "exists," but that doesn't mean you want it "exposed"). Your security goal is clear: only allow necessary inbound access, and reject all other external TCP, UDP, and ICMP requests. In short, it's very simple: let those who should come in, and keep those who shouldn't.
The security requirements are summarized in the table below:
| Demand type | Scene Description | Is it allowed? | Notes |
|---|---|---|---|
| Web services | Public network access via HTTP (80) | allow | Provide website services to external parties |
| Web services | Public network access via HTTPS (443) | allow | Provide website services to external parties |
| Management Access | SSH connection (port 54331) | allow | Log in to the server using a non-default port. |
| Database service | Access host machine 3306 (Docker container) | Allow (local machine/internal network only) | This is not a public service; it is only accessible to local applications or containers. |
| Other TCP inbound | Any source, any port | reject | TCP requests not explicitly allowed |
| Other UDP inbound | Any source, any port | reject | Including various detection and abnormal flow |
| ICMP Inbound | ping / traceroute, etc. | reject | Do not expose ICMP to the public network |
| Forwarding traffic | VPS as a router or gateway | Not involved | This machine does not act as a forwarding machine. |
| Outbound traffic | This machine actively accesses external | allow | Outbound connections are not restricted by default. |
When you look at the problem from this perspective, you'll find that the thought process behind iptables isn't actually abstract. Because what you're currently concerned with is almost entirely traffic entering this machine from the outside.
When an external data packet arrives at a VPS, it typically has only a few possibilities: it's either sent to a service on the local machine; it's forwarded to another host; or it's dropped at some point. In this scenario, the VPS doesn't act as a router or gateway, nor does it need to forward traffic. This step is crucial; it immediately helps you rule out the FORWARD chain branch.
Looking back at the "5 default chains" now, their roles have begun to differentiate. PREROUTING occurs as soon as the data packet enters the kernel, before its destination is decided, and is more related to address rewriting and early processing; POSTROUTING occurs when traffic is about to leave the local machine, mainly for source address translation; OUTPUT deals with connections initiated by the local machine itself; FORWARD is used to forward traffic, but in this scenario, it can be explicitly excluded. Therefore, the only thing that truly needs attention is the gateway that external traffic must pass through before entering the local machine.
Returning to the intuition we've already established: the filter table handles access control; the nat table handles address translation; and mangle and raw are used for lower-level or more specialized processing. Essentially, you only need to do one thing—determine which traffic can enter this machine and which cannot. Therefore, the core of this entire set of rules is actually very clear: it lies in the INPUT chain of the filter table.
Note that this conclusion was not reached by rote memorization of the "4 tables and 5 chains," but rather by a natural deduction from the needs step by step.
Breaking it down further, you'll find that all inbound traffic can be clearly divided into two categories: one is what you explicitly want to allow, such as web services and SSH; the other is all other requests that you have no reason to allow. This corresponds to a very classic and reliable firewall approach: first clearly define "what is allowed," and then use a fallback rule to drop everything else. Docker is not an exception to this model. The reason you don't need to manually write complex rules for 3306 is not because Docker bypasses iptables, but because Docker actively inserts its own chains and redirection rules into the existing rule system, handling "access between containers and within the host machine" without affecting your overall control over public network inbound traffic.
Up to this point, we still haven't written a single command. But you've completed the most crucial step: transforming an abstract security requirement into a clear "traffic decision roadmap." If you now vaguely sense that iptables is transforming from a bunch of terms into a system that can be reasoned about and planned, then this section has achieved its intended purpose.
It's important to note that this article doesn't aim to exhaustively list all iptables configuration methods in every scenario. Instead, it uses a common and easily implemented VPS use case to clearly explain the process. “How should we think about and plan the rules?” .
Therefore, the example primarily focuses on inbound access control, with rules naturally concentrated on the INPUT chain of the filter table. However, in other use cases, such as when Linux is used as a standalone firewall, gateway, or forwarding router, chains like FORWARD, PREROUTING, and POSTROUTING will inevitably be involved, and the dependency on the nat and mangle tables will become even more pronounced.
However, regardless of the role, the underlying way of thinking is completely the same: first, clarify where the traffic comes from and where it goes, then find the checkpoints it must pass through on the core "roadmap", and finally decide at that point to allow it to pass, modify it, or discard it.
6.2 Starting with Rules: Truly Implementing the Idea in iptables
Before actually writing the rules, we had already figured out one thing in section 5.1: the core of this VPS's security policy is...Control who can actively connect.Therefore, all key rules should revolve around the INPUT chain of the filter table.
When writing the INPUT chain, there is a consensus that is almost undisputed:Established or associated connections must be allowed unconditionally.
If even this is not done well, firewall configuration itself becomes a form of "self-harm".
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
The meaning of this rule is very simple: as long as the communication has been approved by the current server, whether it is SSH, HTTP or the return traffic of the container's external request, it will not be reviewed again.
Based on this, the next steps become much clearer:Clearly state the service ports you wish to make public.
For a typical Web VPS, this list is often very short.
iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT
The existence of 80 and 443 needs no further explanation; they are the primary reasons this server is "accessed." Whenever you're running a website, these two rules should be the most prominent parts of the INPUT chain.
Next is the management channel, where we assume port 54331 is used as the SSH login port:
iptables -A INPUT -p tcp --dport 54331 -j ACCEPT
Note: Changing SSH from port 22 to a non-standard port doesn't bring real security benefits, but it can significantly reduce unnecessary scans and log noise. At the rule level, its importance remains clear:This is an entry point where you understand its purpose and are willing to take the risks.
At this point, it's necessary to clarify the issue of Docker-related ports. For example, the database container listens on port 3306. This port does exist, but it's not a "public service." It only needs to be reachable within the container network or the host machine, and Docker itself handles this traffic through NAT and the FORWARD chain. Therefore,You do not need to, and should not, include ports like 3306 in the INPUT chain's allow rules.
Once all the "entry points you explicitly want" have been clearly defined, the remaining traffic in the INPUT chain falls into only one category: requests you have no reason to allow. At this point, the strategy becomes extremely simple:
iptables -A INPUT -j DROP
This rule is not "cold-blooded," but rather a natural summary of all the previous rules:All inbound connections not on the allowed list should be rejected.
Finally, by putting the above rules together in order, you will get a very clean and highly readable INPUT chain strategy:
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT iptables -A INPUT -p tcp --dport 54331 -j ACCEPT iptables -A INPUT -j DROP
If you can explain clearlyThe reason why each rule existsIf a rule is in good condition, then the firewall configuration is healthy; if you can't clearly explain what a rule "protects," then it probably shouldn't exist.
6.3 Docker, Port Exposure, and iptables: The Boundaries of Responsibility Behind a Real-World Incident
In the previous example, we deliberately treated the database service as a component that "exists but is not exposed to the public internet." This is easy to understand on paper, but in a real environment, it is precisely where misoperation and accidents are most likely to occur. The problem doesn't lie in the iptables commands themselves, but often occurs in an earlier step:How are container ports exposed?.
Many Docker tutorials, when demonstrating database containers, directly use commands like these:
docker run -p 3306:3306
This command is syntactically correct and works correctly, but it implies a very crucial yet often overlooked fact:It will make the database port listen on all network interfaces of the host machine. In other words, this 3306 error is visible not only to the local machine but also to the external network. If the host machine is on a local network, it's not a big deal, but if the host machine has a public IP address (or port forwarding for a public IP address), then it's in trouble!
If you then "naturally" think, "It's okay, I have iptables, I'll control it in the INPUT chain," then the risk has already quietly emerged.
In the real world, Docker doesn't simply hand over ports to the INPUT chain for processing. It inserts its own chains and redirection rules into the nat and filter tables, altering the path of traffic entering the container. If you don't understand how these rules are inserted, a consequence can easily arise:The rules may seem to be written, but they don't actually block what you thought they were blocking.
The real-life incident mentioned earlier in this article—the WordPress database being directly accessed and deleted—essentially happened like this: the database container port was unintentionally exposed to the public internet, and the firewall rules did not cover the path that was actually in effect.
However, when you change the port mapping method to the following, the nature of the problem changes fundamentally (here, 100.xyz is not a public IP address, but a virtual internal IP address assigned by Tailscale):
docker run -p 100.xyz:3306:3306 ...
The true meaning of this command is—Listen only on the Tailscale private network interface 3306Taking my Chicago VPS as an example, the actual effect is to map only port 3306 on the tailscale IP 100.xyz to port 3306 on the container IP 172.19.0.4:

In other words, before the traffic even enters the iptables decision-making system, you have already completely removed the database from the "public network world." For public network scanners, this port is even "non-existent."
Understanding this is crucial because it clearly delineates the boundaries of responsibility at several different levels: iptables' INPUT chain is responsible for "which traffic can enter this host machine"; Docker's port bindings determine "whether this service is exposed on a public network interface"; and Docker's automatically created DOCKER-USER chain provides an optional safety net when you really need fine-grained control over container traffic.
When you limit the attack surface to the internal network during the port binding phase, iptables rules are more about defense in depth; however, once you expose the port to 0.0.0.0 from the very beginning, the firewall rules will be forced to assume the responsibility of risk mitigation that is not originally theirs.
Returning to the core intuition emphasized throughout this article: iptables is not a "one-size-fits-all" solution. It's more like a traffic management system than a security patch to fix front-end design flaws.
If you can truly realize this in this section:“Whether "3306" is open to the public is often earlier and more crucial than "what rules are written in the INPUT chain".Then your understanding of iptables has far exceeded the level of "being able to write a few commands".
7. Performance and Scalability Principles of iptables
When someone actually starts using iptables to manage servers, they often go through this stage: at first, they think that rules are "as many as you write, that's how many will be executed"; later, they discover that Docker, NAT, and conntrack cause a lot of performance overhead that they are unaware of; and only later do they realize that the performance bottleneck of iptables is not the number of rules, but which "heavy jobs" in the kernel are actually triggered by the rules you write.
What many people don't know is that if you use NAT and write most of your filter rules, the Linux kernel will almost always include conntrack. This "connection tracking system" is essentially a hash table that remembers the five-tuple and state of each connection, allowing NAT to perform packet translation correctly and enabling your "ESTABLISHED, RELATED" rules to work. The benefits are obvious, but the downside is that in high-concurrency scenarios, conntrack itself can become a huge performance bottleneck: with many connections, the kernel constantly looks up the table; with many NATs, the table gets even more state; hash collisions lead to slowdowns; and when the table is full, it can even cause packet loss. This is why some VPSs experience sudden NAT failures under high traffic—not because iptables is broken, but because conntrack is overloaded.
The performance overhead of NAT is actually much greater than that of most filter rules. This is because NAT is not simply about changing IPs or ports; it requires creating mappings for each connection, synchronizing state, maintaining timers, and managing return packet paths, resulting in a significant overhead. If you run a large number of containers, and Docker/Kubernetes uses it to manage a large number of port mappings and traffic forwarding, the growth rate of conntrack will be more dramatic than you imagine. This is why many production environments either disable conntrack, minimize NAT, or switch to a bypass method like ipvs.
When discussing performance overhead, many people's first reaction is "more rules mean slower performance." This statement isn't entirely accurate, at least not for all chains. For example, the INPUT chain has the biggest impact on local services, but generally doesn't handle much traffic; PREROUTING and POSTROUTING are the busiest chains during peak traffic periods because all inbound and outbound packets pass through them. If these locations are filled with complex rules, server latency will escalate from TTFB all the way to the overall response time. Therefore, the number of rules isn't as important as where you write them and whether they affect the kernel's "busiest nodes."
In the Linux kernel's packet processing logic, there are actually "fast paths" and "slow paths." When conntrack has already established mappings, or the route cache is valid, packets can often directly take the fast path without needing to redo NAT, verification, and route determination. However, if you make changes to the mangle or raw tables, or write rules that would break the fast path, the kernel will force packets into the slow path. The slow path means more checks, more computation, and more state synchronization. As a result, performance naturally drops. Some people carelessly write mangle rules, or dozens of strange TEE/mark rules, which can unknowingly drag a large number of packets into the slow path, resulting in server lag, even when the CPU usage isn't even at full capacity.
Why do iptables rules look messier and perform worse in a Docker environment? The reason is actually quite simple: Docker automatically creates a large number of DNAT mappings, automatically opens FORWARD rules, automatically enables MASQUERADE for SNAT, and inserts various forwarding logic in POSTROUTING. This might be manageable with dozens of containers, but after hundreds of containers, these NAT and FORWARD chains become incredibly long. Combined with the explosive increase in conntrack entries, increased latency is normal. It's not that "your configuration isn't optimized enough," but rather that Docker's default design prevents it from being as lightweight as a traditional router.
Of course, iptables performance can be optimized. The most common approach is to place frequently hit rules at the beginning, merge a bunch of ports into a multiport, merge a large number of IPs into an ipset, and ensure that filtering logic is performed in the INPUT block rather than the PREROUTING block as much as possible. This minimizes unnecessary NAT operations and avoids writing unnecessary entries to the mangle table in the PREROUTING block, which is the most performance-sensitive area. The ipset block, in particular, allows you to reduce what would normally be 500 rules to a single one. This optimization technique can often save you from many performance pitfalls.
To summarize this chapter, one sentence suffices: the performance bottleneck of iptables isn't the number of rules, but rather the amount of extra work the rules require from the kernel. Behind every rule you write lies a piece of logic running in the kernel. Some logic is as light as a feather, others as heavy as a mountain. The difference in how you write the rules determines whether your server can survive under high traffic.
8 Commonly Used iptables Commands: These are the ones you'll actually use before writing rules.
In previous chapters, we have already "touched" iptables rules in the INPUT chain configuration based on a real VPS scenario. I deliberately put the commands in specific contexts rather than talking about the parameters themselves, so that you can first establish the correct judgment path.
But when you actually start maintaining a server long-term, you'll discover another real-world problem:You don't spend every day "designing rules," but more often you're "viewing, adjusting, rolling back, and validating" existing rules.At this point, what you need is not a complete set of strategies, but a thorough grasp of a few of the most commonly used commands—how to check the current rule's effectiveness, how to safely insert or delete a rule, and how to confirm whether a data packet has been hit.
Therefore, this chapter will no longer discuss "how to design rules," but instead extracts the most frequently used and error-prone iptables operations in actual operations and maintenance, presenting them as a command toolbox that can be easily referenced. You can treat it as an "operational-level supplement" to all the previous chapters, rather than a new learning main line.
However, the first principle in all operations is always:Observe first, then modify.—You must first understand what the current rules look like, what their order is, whether Docker is adding anything to PREROUTING, whether NAT is working as expected… all of this starts with “looking,” for example:
If you suspect Docker is secretly performing DNAT for you, simply check the PREROUTING chain in the nat table. iptables will list the rules by chain, in the order of kernel execution, allowing you to quickly determine what's actually happening instead of guessing.
When you want to check if a certain port has been hit, you can add a log rule at the end of INPUT or FORWARD. This way, you can observe the path without taking up the priority of the previous key rules.
When you want to delete a rule, the safest way is to delete it by line number. This is because the overall order of the chain changes every time you add or delete a rule—operating by line number can prevent accidental or incorrect deletion.
When troubleshooting, always refer to the "kernel roadmap". The entry point is always PREROUTING, and the exit point is POSTROUTING; for local access, look at INPUT; for containers, bypassing, and forwarding, look at FORWARD.
You need to consider the commands and "paths" together, rather than just focusing on the commands themselves. This will allow you to locate problems much faster than someone who only memorizes commands.
The following commands are some of the actions I actually use in my daily work, and they are also the actions you will rely on most when troubleshooting problems in the future:Check the rules, add rules, delete rules, and check if the rules have been hit.As for those other commands you've never used, you don't actually need to memorize them.
1. View the rules of a specific table or chain.
iptables -t nat -L PREROUTING -n -v
What is it doing: List nat table middle PREROUTING chain All rules are displayed in the actual execution order, along with the hit count for each rule. Note that PREROUTING rules are frequently inserted by system components (such as Docker, 1Panel, CNI plugins, etc.), so you will often see redirect rules that you did not write.
When to use: This command is the first choice when you want to confirm whether a program is inserting rules into the NAT table (especially Docker), or when you want to check whether DNAT, transparent proxy, and port forwarding are working as expected.
The output of this command on my Chicago VPS is as follows:

2. View all tables and all chains (complete overview)
iptables -L -n -v
What is it doing: List all the rules in all chains of the filter table so you can see the entire filtering logic at once.
When to use: When you're completely clueless about "who's intercepting the packet," this command is the fastest way to get you started.
The output (partial) of the command I ran on my Chicago VPS is as follows:

3. Add a rule to the end of a chain.
iptables -A INPUT -s 1.2.3.4 -j DROP
What is it doing: Adding a rule at the end of the INPUT chain will prevent priority contention.
When to use: When temporarily blocking an IP address, debugging a path, or verifying a certain condition, this "tail appending" method is the safest.
4. Insert a rule at the beginning of a chain.
iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT
What is it doing: Insert a rule at the very beginning of the INPUT chain to make it the highest priority rule.
When to use: When you're preparing to test logic that's easily overridden by previous rules, or when you're afraid of "accidentally shutting yourself out," use insertion to ensure that the rules are executed first.
5. View the line number of a specific rule in a chain and delete the rule corresponding to that line number (using the Docker chain as an example).
`iptables -t nat -L DOCKER --line-numbers -n #` displays all rules and line numbers in the DOCKER chain of the nat table. `iptables -D DOCKER 5 #` deletes the rule with line number 3.
What is it doing: Delete rule number 3 in the Docker chain.
When to use: This method is always recommended when deleting rules because the rule order will change, and "deleting by line number" is the least likely to result in accidental deletion.
For example, if I run this command on a Chicago VPS and view the rule row numbers in the DOCKER chain of the nat table, the output will be as follows:

Run at this timeiptables -D DOCKER 5You can then directly delete the first DNAT rule in row 5.
6. Check the "hit count" of the INPUT rule (very important)
iptables -L INPUT -n -v
What is it doing: Displays traffic statistics for each rule in the INPUT chain.
When to use: This is the most valuable information when troubleshooting. You can tell at a glance whether a rule has actually been enforced.
The output of this command on my Chicago VPS is as follows:

7. Clear a specific chain (using INPUT as an example)
iptables -F INPUT
What is it doing: Clear all rules in the INPUT chain.
When to use: This tool is used for debugging the environment, initializing scripts before writing them, and preventing historical rules from interfering with current behavior. It is not recommended for indiscriminate use in production environments.
8. Clear the entire iptables (use with caution!)
iptables -F iptables -t nat -F
What is it doing: Clear all rules in the filter and nat tables.
When to use: This can be used when rebuilding the rule system or debugging the isolation environment. However, it's best to avoid doing this on a real server.
9. Save the current rules (to prevent them from disappearing after a machine restart).
Debian/Ubuntu:
iptables-save > /etc/iptables/rules.v4
CentOS/RHEL (using the service tool):
service iptables save
What is it doing: Persist the rules currently in memory to disk.
When to use: When you have already set up a set of rules and are ready to use it long-term.
If installediptables-persistentFor components, the corresponding command is:
netfilter-persistent save
10. Recovery rules (loaded from file)
iptables-restore < /etc/iptables/rules.v4
What is it doing: Load the saved rules into the system all at once.
When to use: After the system restarts, or when deploying several machines in batches.
If installediptables-persistentFor components, the corresponding command is:
netfilter-persistent reload
In conclusion: There aren't many commands; the key is to "understand them."“
You'll find there aren't many commands, and there aren't any mysterious techniques. What really gets you stuck isn't the commands themselves, but rather...I don't know which part of the path the command corresponds to..
Just remember this: entry points always start with PREROUTING; local activities go through INPUT; forwarding and Docker go through FORWARD; and exit points always go through POSTROUTING. Then, combine this with the "good enough" commands mentioned above, and you can find a direction for most tricky problems within minutes.
9. Postscript: A letter to my future self (and to you who may not understand iptables yet).
After writing this entire set of content, I'm even more certain of one thing: iptables, this old-school tool, while "old," is not obsolete. Its simple logic, the causal relationships within rule chains, and the clarity of matching and redirection are actually very much in line with an engineer's way of thinking. But precisely because it's so flexible and capable of being tweaked so much, many people easily get lost when they first encounter it—the rules are incomprehensible, the relationships between chains are confusing, and fixing a single mistake can destroy the network.
My purpose in writing this article is not to pile up all the grammar rules, but to...The easiest to use, the easiest to make mistakes with, and the most worthwhile to understandWe need to explain those parts clearly. After all, most of the time, we don't need to become "walking encyclopedias of iptables," but rather be able to solve problems reliably in real-world scenarios: be able to determine the intent of a rule when we see it, be able to roughly deduce which chains a traffic flow will pass through when we see the direction of the traffic, and know where to start and where to avoid messing around.
That's why I've broken down some commonly used commands, troubleshooting approaches, and the actual function of chains into separate sections. This isn't to create a tutorial, but rather so that in the future, when I encounter strange NAT phenomena, Docker traffic deviations, ineffective policy routing, or inexplicable port mapping failures, I'll have a place to go back and check the logic.
In fact, for someone like me who is used to configuring standalone firewalls, configuring iptables is where I'm most prone to making habitual mistakes. In the world of standalone firewalls, rules are laid out in layers of GUI or policy trees, and you change one rule at a time; but on Linux, iptables is not a "firewall configuration file," but a tool for "real-time editing of kernel traffic processing."
At first, you might subconsciously think of it as a "rule-once-you-add-it" model like Palo Alto, FortiGate, or Hillstone. But the reality is quite the opposite—iptables rules are scattered across different tables and chains with different behaviors, and system components will also add rules to these chains uninvited. If you don't check the tables, verify the chains, or observe the actual hit count, it's easy to fall into the misconception of "I clearly added a rule, so why isn't it working?"
Ultimately, this isn't a technical issue, but rather a matter of experience transfer. Those accustomed to building standalone firewalls often need to abandon their old "firewall is configuration file" mindset when first encountering iptables and re-examine all rules from the perspective of "real-time kernel traffic processing." Once this mindset is adjusted, iptables suddenly becomes "understandable"—it's no longer a screen full of commands, but a flow control system that you can break down and verify step by step.
iptables has a unique quality: once you truly understand it, the network world suddenly becomes "explainable." Things that were incomprehensible before can now be deduced by tracing the chain; things that could only be solved by trial and error can now be deduced by rules; things that were once considered mystical can now be identified by capturing packets, checking chains, and looking at counters, step by step finding out who caused the problem.
Having written this far, I've summarized the pitfalls I've encountered, the patterns I've discovered, and my systematic understanding of it over the past few years. If I encounter more typical problems in the future, I may continue to add to this, but there's no rush, and I don't need to set a specific level of detail. iptables is inherently a tool that you learn as you go—the more you understand, the more at ease you'll be; maintaining a certain distance won't diminish its value.
And so, let's leave a brief summary, and also leave a guiding light for our future selves.
I studied it carefully too. Before, I just skimmed through it. Thank you for the explanation, expert!
You're welcome. I'm glad it's helpful. I only wrote this article because I felt I didn't understand it deeply enough before, so I wanted to urge myself to think it through more clearly.
Thank you so much for the detailed explanation of iptables. I read it word for word, and now I have a new understanding of iptables; many things that I couldn't understand before are now clear. Thank you.
I'm just doing this out of habit because I tend to forget things easily, so I try to write it in as much detail as possible. But I didn't expect you to actually read it word for word! These days, most people don't have the patience to finish long articles.