Before you expose a service, you need to know precisely from which network its socket is really reachable, and to have written the list of expected flows rather than assuming a firewall already blocks them. A socket, here, is a process's listening point: an IP address and a port. This lesson concludes the "Publish a web service" foundation: it builds on Prepare DNS names and their cutover and prepares the next lesson on the complete path of a request. The detailed mechanics of the firewall, NAT and ports are already covered by dedicated guides, cited throughout the text: here, the goal is to know how to build and check a flow matrix, not to relearn every command.
Two words will recur throughout the lesson. The entry point is the service that receives clients' requests (the reverse proxy of the previous lessons). The backend is the application it calls behind itself, and which must not be reached directly by an external client.
What you will learn
- Identify from which network a socket is really reachable, depending on its listening address
- Read listeners with
ss -ltnand probe by the HTTP code (never by the body) to fill in a flow matrix - Build a flow matrix that never exposes a backend directly to the external client
- Explain why testing from the machine itself, or stacking containers on one host, proves neither of the things you think
Prerequisites
This lesson assumes
Prepare DNS names and their cutover
and the networking basics detailed in TCP vs UDP (in French) (ports, testing with
netcat), Firewall (in French) (ingress,
egress, rules) and NAT (in French) (port
forwarding). It does not re-explain them, and focuses on a single gesture:
knowing, for a given socket, who can really reach it.
From which network is a socket really reachable?
The address a process listens on determines, before any firewall rule, who
can even try to connect to it. Three cases come up constantly: a listener on
127.0.0.1 (loopback) is reachable only from the machine itself, no
firewall is needed to protect it from a remote network; a listener on a
specific private address (10.10.110.11 for example) is reachable only
from a network that routes to that address; a listener on 0.0.0.0 (all
interfaces) answers on every IP address the machine carries, including a
public address if it has one.
Confusing these three cases produces an accidental exposure: a service
meant to stay internal, started by mistake on 0.0.0.0 instead of the planned
private address, becomes reachable from any network that reaches the machine,
including the Internet if a port redirection rule exists elsewhere.
Checking the real listening address (not the one you think you configured)
is therefore the very first gesture, before even reading a single firewall
rule.
How do you read listeners with ss -ltn?
ss -ltn lists the TCP sockets in listening state (-l) in numeric form
(-n), and the Local Address column gives the real listening address of each
process. It is the source of truth for the first gesture, more reliable than
the configuration you think you wrote. Real reading, with two test services on
the test machine: the first listens on loopback (port 18103), the second on all
interfaces (port 18104). The ports and addresses in this lesson are those of
this capture, you do not have to recreate them: on your side, the same command
lists your own services.
ss -ltn | grep -E 'State|:1810[34] 'State Recv-Q Send-Q Local Address:Port Peer Address:PortProcessLISTEN 0 5 0.0.0.0:18104 0.0.0.0:*LISTEN 0 5 127.0.0.1:18103 0.0.0.0:*The difference reads in the Local Address column: 127.0.0.1:18103 is
reachable only from the machine, 0.0.0.0:18104 is reachable on each of
its addresses. The same service started with a different option therefore
changes its exposure surface, without any firewall rule having moved.
Which probe tells you a port is open: the code or the body?
To test an HTTP listener, read the response code: 000 means no response
arrived (port closed, filtered or unreachable), any other HTTP code means the
port is open and answers. Reading the response body is a trap: a service
can answer correctly with no body at all. The first test service returns
204 No Content, the HTTP code meant for "I succeeded, I have nothing to tell
you". You probe it from the machine (127.0.0.1) then from another of its
addresses (192.168.1.71, the local network address of the test machine: on
your side, hostname -I lists yours), with port 18199 as a closed witness.
This "other address" belongs to the same machine: it is not yet a test from
the outside, the limit is detailed below.
for t in 127.0.0.1:18103 192.168.1.71:18103 127.0.0.1:18104 \ 192.168.1.71:18104 127.0.0.1:18199; do c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "http://$t/") printf '%-22s code %s\n' "$t" "$c"done127.0.0.1:18103 code 204192.168.1.71:18103 code 000127.0.0.1:18104 code 200192.168.1.71:18104 code 200127.0.0.1:18199 code 000Each line is a curl probe on a different address, and
-w '%{http_code}' prints only the code. The loopback service answers
204 locally and 000 via the network address: reachable here, unreachable
there. The second answers 200 on both sides. And port 18199, where nothing
listens, also gives 000.
Now let us look at the faulty probe, the one that downloads the response and tests whether the body is non-empty, on the same two services:
127.0.0.1:18103 -> corps vide : conclu FERME127.0.0.1:18104 -> corps non vide : conclu OUVERTThis probe concludes "closed" for the service that answers 204: it is a
false conclusion of closure, while the port is open and answers correctly.
The body measures only what the service decided to write, not what the network
lets through. Conversely, the 000 code does not say why: curl returns
Failed to connect ... Could not connect to server (exit code 7) for a port
where nothing listens. For its part, nc -z (which opens the connection
without sending anything) decides by its exit code: 0 for an accepted
connection, 1 for a refused one, without reading a single byte of the response.
A "closed" port and a "filtered" port give the same 000 but do not have the
same cause. A closed port is one where the machine explicitly answers that
no process listens: the refusal is immediate. A filtered port is one where
a firewall drops the packet without answering anything: the client waits until
its maximum delay (with --max-time 2, curl then stops with exit code 28).
To tell a refusal from a silent filtering in detail, see
TCP vs UDP (in French).
Why doesn't a cloud provider's security replace the host firewall?
A cloud security group and the firewall installed on the machine (iptables, nftables) are two distinct controls, applied at different moments of the path, and neither exempts you from the other. The security group filters traffic before it reaches the machine's network interface, at the provider's infrastructure level; the host firewall then filters, once the packet has arrived on that interface. A rule opened in one without an equivalent closed in the other exposes exactly what the other control thought it was protecting.
A container engine adds yet another level: Docker, for example, manipulates its own iptables rules to route published traffic to a container, sometimes bypassing rules placed manually on the host if they are not written in the right chain. The guide Docker networking (in French) details this interaction. Building a flow matrix therefore requires listing, for each service, the three levels that can let a flow through or block it: the cloud security group, the host firewall, and the rules specific to the container engine.
How do you build a flow matrix that does not expose the backends?
A flow matrix lists, for each service, who is allowed to talk to it: the external client, a possible administration network, and the backend network, which must be reachable only by the planned entry point. Three categories of origin are enough for most publications: the external client, which must reach only the entry point's public port; the administration network, reserved for the people who operate the service (SSH, monitoring console) and which must never be open to the external client; the backend network, where the applications themselves live, which must accept connections only from the entry point, never directly from the outside.
Writing this flow matrix before placing any firewall rule changes the question asked: instead of wondering "which rule should I add?", you wonder "which flow have I decided to allow, and why?". The guides Firewall (in French) and NAT (in French) show how to translate this matrix into concrete rules, in IPv4 as in IPv6: the absence of NAT, common in IPv6 where every machine keeps a routable address, does not mean the absence of filtering, as detailed in IPv6 in practice (in French).
What does the topology to check look like?
The topology says who must be able to reach what: the external client may reach only the entry point, administration only SSH, and the backend only the entry point. On the diagram, solid arrows are the intended flows; dotted ones are the paths that must fail and that your probes will look for. Note that the backend listens on loopback: even without a firewall, it is not reachable from the outside.
How do you fill in the matrix with what you observe?
You fill in two columns per service: what the policy allows, and what
observation shows from each test point; a divergence between the two columns is
a defect to handle. The "policy" column comes from your architecture
decision; the "observation" column comes from ss -ltn and the probes above.
With the three test services (the first plays a backend, the second an entry
point, the last a service that should be closed), the filled matrix gives:
| Service | Intended policy | Listener (ss -ltn) | From the machine | From another address | Verdict |
|---|---|---|---|---|---|
| Backend (18103) | This machine only | 127.0.0.1 | 204 (open) | 000 | Compliant |
| Entry point (18104) | Open to everyone | 0.0.0.0 | 200 (open) | 200 (open) | Compliant |
| Port 18199 | Closed | absent | 000 | 000 | Compliant |
This matrix has a limit you must know: the "other address" is an address of the same machine. It proves that the loopback listener is indeed invisible from the local network, but it crosses no perimeter firewall (the one that guards the network boundary, between the Internet and your machines): this matrix therefore proves nothing about it. Only a probe launched from another network does, and that is what the lab asks for.
What should you conclude when the two columns diverge?
When policy and observation diverge, look first in the listener (ss -ltn),
then in the filters. Three cases occur:
| Divergence | Most probable cause | Where to look |
|---|---|---|
Backend meant to be closed, but 200 from another address | Listening on 0.0.0.0 instead of 127.0.0.1 | Local Address in ss -ltn |
Service meant to be open, 000 everywhere, absent from ss -ltn | Service stopped or badly started | The process, its logs |
Service meant to be open, present in ss -ltn, 000 from the outside only | A filter in between (host firewall, security group, container engine rule) | The three levels described above (security group, host firewall, container engine) |
The third case is where the firewall becomes suspect: this is why you do not start with it.
Why does a test from the machine itself prove nothing about external reachability?
A request that succeeds from the machine hosting the service often takes a
completely different network path from that of an external client, and crosses
neither the perimeter firewall nor, sometimes, any rule at all. This is the
first mistake to rule out before declaring a service correctly exposed:
testing from the VM does not prove unreachability from the outside, nor
indeed its reachability. A process that answers locally via 127.0.0.1 or the
machine's internal address can remain totally unreachable from a remote
network, or conversely, a service you believe protected can already be
publicly exposed without a local test ever revealing it.
Checking an exposure therefore requires a test made from a real external observation point, on a network distinct from that of the tested machine, with the same ports and the same protocol a real client would use. This is exactly what the flow matrix verifies once in place: not "does the service answer?" but "from which network precisely does it answer, and from which does it refuse?".
Why aren't two containers on the same host high availability?
Two containers, two replicas or two instances of an application running on the same physical machine share the same point of failure: the host. This is the second mistake to rule out before publishing a service you believe resilient: multiplying instances protects against the failure of one process, never against the failure of the machine hosting them all. A well-built flow matrix identifies not only the authorised networks, but also the shared points of failure behind each flow it describes: a redundancy that stops at the same host, the same power supply or the same network rack is not one.
This distinction is not resolved by a firewall rule: it is resolved by an architecture decision, spreading instances across physically distinct machines, which goes beyond the scope of this lesson but must be made before calling a service highly available.
The lab applies the lesson's approach: a publication answers, but exposes too many ports. You establish the flow matrix, then close the paths that bypass the entry point, checking from a client on another network.
Key points
- A listener on loopback, on a private address or on all interfaces
(
0.0.0.0) does not determine the same reachability surface. - The cloud security group, the host firewall and the container engine rules are three distinct controls, and none exempts you from the other two.
- A flow matrix lists the external client, the administration network and the backend network, and exposes the latter only to the planned entry point.
- An HTTP probe reads the code:
000= no response (port closed or filtered), any other code = port open. A204with no body is an open port, not a closed one. - Testing from the machine itself proves neither external reachability nor unreachability: only a test from a distinct network does. Another address of the same machine crosses no perimeter firewall.
- Two containers on the same host share the same point of failure: that is not high availability.
Knowledge check
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Reverse proxy: definition, role and which tool to choose
A new full quiz with random questions
Work again only on the questions you missed