“Is it open?” is the question everyone reaches for first when a request doesn’t get through, and it’s the wrong question: not because it’s badly phrased, but because it presupposes there’s one gate. On a segmented personal infrastructure platform, there isn’t one gate. There are at least six, stacked, each answering a narrow question that has nothing to do with the others, and every single one of them has to come back “yes” before a request actually lands on the thing it’s trying to reach.
I’ve come to think of this as a fixed checklist rather than a vague sense of “network stuff.” Not because the specific six are sacred — a different topology might have five, or eight — but because naming them separately is the only thing that has ever reliably gotten me out of the debugging loop where you fix one thing, the symptom doesn’t change, and you start doubting the fix instead of looking for the next layer.
The six questions
1. Does a tunnel or VPN peer exist at all? Before anything else, is there even a relationship between the caller and this network: a WireGuard peer configured, a tunnel endpoint that knows about the other side? If the answer is no, every layer above this is irrelevant, because the packet has no path in. This is the layer people forget exists because it’s usually set up once, works, and then sits untouched for months. When it silently breaks (a peer config drifts, a key rotates on one side and not the other), everything above it looks broken too, and it’s tempting to go straight to “the firewall must be blocking it” without checking whether there’s a tunnel at all. On this platform that peer relationship isn’t one undifferentiated VPN either — privileged and general access run over separate tunnels, so “does a peer exist” is really “does a peer exist on the specific tunnel this request needs.”
2. Does routing carry the packet there?
A peer relationship existing doesn’t mean the network knows how to get a specific destination address through it. Routing tables, whether static or dynamic, decide which interface and which next hop a packet takes. It’s entirely possible to have a working tunnel and a route to the wrong subnet, or no route at all to a newly-added segment: the tunnel comes up clean, ping to the peer itself works, and the actual service is still unreachable because nothing told the router that its subnet lives on the other end of that tunnel.
3. Does a firewall rule allow it? This is the layer most people mean when they say “is it open,” and it’s genuinely important, but it’s only one of six. A firewall rule is a narrow allow/deny decision on a specific combination of source, destination, port, and protocol. Getting the rule wrong (right host, wrong port; right port, wrong direction; a rule that allows the segment but not the specific host) produces exactly the same symptom as a routing problem or a missing tunnel: the connection just doesn’t happen. From the outside, “no route” and “no rule” and “no tunnel” are indistinguishable without checking each one. Every segment here defaults to deny at exactly this layer: nothing crosses without an explicit rule naming it, which is a real trade-off in its own right, detailed separately.
4. Does DNS resolve the name at all? This layer is almost embarrassingly separate from the first three, and that’s exactly why it causes confusion. A network path can be entirely correct (tunnel up, route present, firewall rule allowing the traffic) and the request still fails because the client never got an IP address to send the packet to in the first place. DNS resolution failures produce a different-looking error than a blocked connection (usually a fast, clean failure rather than a timeout), but under pressure, “it’s not working” doesn’t distinguish between the two, and it’s easy to burn time checking firewall rules for a host whose name never resolved.
5. Does the application’s own auth layer accept the caller? Now the packet has actually arrived, cleanly, at the service. This is where a lot of people’s mental model of “reachability” quietly stops, but it’s not the end: the application still gets an independent say. A valid, unblocked, correctly-routed connection can still get a 401 or 403, and that failure has nothing to do with any of the four layers below it. This is also the layer that most resembles “working as intended” from a security standpoint: a request reaching the app and getting rejected by its own auth is often the system doing exactly what it should, not a bug to route around.
6. Does the host’s own local firewall allow it?
Last, and easy to forget precisely because everything else already looks fine by this point: the destination host can have its own local firewall (iptables, nftables, a host-level ufw, whatever the platform on that specific box runs) that is entirely independent of whatever perimeter or segment-level firewall rule handled layer three. A rule that permits a segment to reach a host doesn’t guarantee the host itself is configured to accept the connection on its own loopback-adjacent rules. This layer answers “does this specific machine, on its own terms, let this in,” and no upstream layer speaks for it.
Walking the checklist top to bottom
The practical value of the model only shows up once you actually use it as an ordered checklist rather than a list of things you know about in the abstract. Say a service that was reachable yesterday isn’t reachable today, and nothing about the service itself changed. The instinct is to start wherever feels most likely — usually the firewall, because firewall rules are the layer people are most primed to blame. But starting in the middle means that if the actual problem is one layer below (routing) or one layer above (DNS), you’ll spend real time confirming a layer that was never broken, and worse, you’ll finish that confirmation with false confidence: “I checked the firewall, it’s fine,” which subtly rules out the wrong thing while doing nothing to rule in the right one.
Walking the six layers in order, bottom to top, turns that same investigation into a sequence of narrow, falsifiable checks: confirm the tunnel peer is up and its handshake is recent, not just configured, but actually connected right now. Confirm the routing table has an entry that covers the destination, and that it points at the right interface. Confirm the firewall rule set still contains the rule it’s supposed to, matching the right source, destination, and port, and that a broader rule higher in the rule order isn’t shadowing it. Confirm the hostname being used actually resolves, from the same vantage point the failing request originates from, not from a machine on a different segment where DNS behaves differently. Confirm the application’s own logs show the request arriving and being rejected, versus never arriving at all: that distinction alone tells you whether the problem is in the first four layers or the last two. And only if all of that clears do you look at the host’s own local firewall, because by that point you’ve already confirmed the request should be arriving and something local to the box is still eating it.
Each of those six checks is genuinely quick on its own. The discipline isn’t in any individual check being hard — it’s in resisting the urge to stop at the first layer that looks plausible and declare it “probably the fix” before actually confirming the symptom changed.
Why collapsing this into one question breaks debugging
Each of these six layers is independently sufficient to produce the exact same user-visible symptom: the request doesn’t complete. A timeout at layer 1 looks like a timeout at layer 2 looks like a timeout at layer 3. A DNS failure at layer 4 usually looks different (fast failure vs. a hang), but under time pressure that distinction gets lost, and “it’s not working” becomes the entire diagnostic vocabulary.
The actual danger isn’t that any one of these layers is hard to check — each one, in isolation, is usually a two-minute check. The danger is the implicit assumption that fixing one of them should fix the symptom, and when it doesn’t, concluding the fix was wrong rather than concluding there’s another, unrelated, still-failing layer underneath or above it. I’ve watched this play out as: firewall rule gets corrected, confirmed correct with a rule-tracing tool, request still fails. The instinct is to second-guess the firewall fix again, add another rule, broaden it, when the actual problem was that DNS never resolved the hostname being used, a layer that was never touched.
Treating “is it open” as six separate, ordered questions instead of one vague one has a specific, mechanical benefit: it turns debugging into a checklist you walk top to bottom (or bottom to top) instead of a single hypothesis you either believe or don’t. Does the peer exist? Yes. Does routing carry it? Yes. Does the firewall rule allow it? Yes. Does DNS resolve? And there it is: the fourth question, sitting right where a false assumption (“the network path is fine, so it must be DNS or the app”) would have skipped straight past it if the process had stayed as one collapsed question rather than six discrete ones.
None of the six substitutes for any other, in either direction. A generous firewall rule doesn’t fix broken routing. A working DNS record doesn’t imply the app will accept the caller. A permissive perimeter rule doesn’t override a host’s own local firewall dropping the packet at the last hop. Each layer only ever answers its own question, and the request only ever gets through when all six, independently, say yes.
The same model, read as a security property instead of a debugging aid
Flip the frame and the six layers stop being a debugging checklist and become something closer to defense in depth, almost by accident. Because each layer is genuinely independent (a firewall rule really can’t compensate for a bad routing decision, and application auth really can’t be bypassed just because the network path is open), a mistake at any single layer doesn’t automatically become a breach. Misconfigure a firewall rule too broadly, and the request still has to clear DNS, still has to clear application auth, still has to clear the host’s own local firewall, before it reaches anything. That’s not a designed-in redundancy so much as a side effect of the layers genuinely not trusting each other to have done their job, which is exactly the property defense-in-depth is trying to achieve on purpose elsewhere.
The corollary is that it also cuts the other way for legitimate access: adding a new tunnel peer for a new device doesn’t imply that device can suddenly reach everything on the network. It clears layer one. It still needs a route, a firewall rule, a resolvable name if it’s connecting by hostname, and it still has to pass whatever the destination application’s own auth requires. Each new layer of friction is also a new layer of containment, in both directions: for the intruder trying to get further than one misconfiguration should allow, and for the well-intentioned change that shouldn’t grant more access than it means to. Reasoning about the six layers explicitly, instead of treating “reachability” as one fuzzy property a system either has or doesn’t, is what makes it possible to add or remove access at exactly one layer with confidence about what that change does and doesn’t affect everywhere else.
The management network is the sharpest version of this model: see Isolating the Management Plane for what happens when one of these six layers is a boundary that’s meant to never be crossed, by design, rather than one that’s merely narrow.