Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Link interfaces, ports, firewalls and exposure

By updated

Improve this page

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 -ltn and 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.

Terminal window
ss -ltn | grep -E 'State|:1810[34] '
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 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.

Terminal window
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"
done
127.0.0.1:18103 code 204
192.168.1.71:18103 code 000
127.0.0.1:18104 code 200
192.168.1.71:18104 code 200
127.0.0.1:18199 code 000

Each 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 FERME
127.0.0.1:18104 -> corps non vide : conclu OUVERT

This 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.

An external client reaches the entry point on port 443, an administration workstation reaches SSH, the entry point alone reaches the backend on loopback; the external client's direct accesses to the backend and to SSH must fail.Service hostmust failmust failExternal clientother networkAdmin workstationEntry pointlistens 0.0.0.0:443SSHlistens 0.0.0.0:22Backendlistens 127.0.0.1:8080
Dotted: the paths that the flow matrix must prove closed. Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

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:

ServiceIntended policyListener (ss -ltn)From the machineFrom another addressVerdict
Backend (18103)This machine only127.0.0.1204 (open)000Compliant
Entry point (18104)Open to everyone0.0.0.0200 (open)200 (open)Compliant
Port 18199Closedabsent000000Compliant

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:

DivergenceMost probable causeWhere to look
Backend meant to be closed, but 200 from another addressListening on 0.0.0.0 instead of 127.0.0.1Local Address in ss -ltn
Service meant to be open, 000 everywhere, absent from ss -ltnService stopped or badly startedThe process, its logs
Service meant to be open, present in ss -ltn, 000 from the outside onlyA 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. A 204 with 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

8 questions
8 min
70% to pass

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

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn