Understanding port labeling with semanage port for services outside default ports, mapping services to port types, and packet and connection labeling through SECMARK and CONNSECMARK in netfilter.

In episode 12 we wrote a custom policy and saw the rule allow myapp_t http_port_t:tcp_socket name_connect. There's a question we deliberately postponed: where does http_port_t come from, and how does SELinux know that port 8080 isn't part of http_port_t unless we tell it? The answer is port labeling — one of the most frequently tweaked data by admins, and the most common source of "mysterious" denials when a service is moved to a non-default port.
In this episode we dissect the entire network layer: port labeling (the object bound by a socket), interface and node labeling (for MLS networks), and packet and connection labeling through interaction with netfilter/iptables. By the end of this episode, you'll stop guessing when a daemon fails to listen on a custom port.
In SELinux, a "port can't be bound" denial is not the network kernel's decision — it's a policy decision. When a process calls bind(), the kernel doesn't just check whether the port is free; it also asks the LSM for a decision: is this domain allowed to name_bind a socket with that port's label? The rule allow httpd_t http_port_t:tcp_socket name_bind only applies if the bound port is labeled http_port_t.
Here's the analogy: a port type is an "office room" with a marked door. The policy gives each domain permission to enter certain rooms (httpd_t may enter http_port_t). When you move a web server to port 8080, you're not adding new permissions — you're just changing the door sign of 8080 to http_port_t. If the sign is wrong (still generic port_t), the door stays closed even though the policy is correct.
The port labeling directory is managed in userspace (libsemanage), not inside the binary policy — that's why it can be changed without recompiling the policy. The command:
semanage port -lssh_port_t tcp 22
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
mysql_port_t tcp 1186, 3306, 63132-63164
postgresql_port_t tcp 5432
redis_port_t tcp 6379Notice a very important pattern: one port type can hold many ports, and one port only has one label. http_port_t isn't "port 80" — it's a category covering a range of ports. That's why moving nginx to port 8080 (already in the list) requires nothing; what needs action is a port that isn't registered at all.
When your service must listen on a port not yet labeled for its domain, add it with semanage port -a:
semanage port -a -t http_port_t -p tcp 8080The general form is always -a (add), -t (type), -p (protocol), then the port number. Without this step, a name_bind denial appears exactly like this:
avc: denied { name_bind } for pid=4321 comm="nginx"
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:port_t:s0 tclass=tcp_socketNotice the tcontext: its label is port_t — the default type for all ports not explicitly labeled. This is the telltale sign of an "unlabeled port": the target is always port_t, not a service port type. One semanage port -a command resolves it, and it takes effect immediately without restarting the service — the port label is read at bind() time.
This mapping isn't arbitrary — it's a convention built by refpolicy so each domain only deals with its own ports:
| Service | Common port | Port type |
|---|---|---|
| nginx / httpd | 80, 443 | http_port_t |
| sshd | 22 | ssh_port_t |
| MySQL | 3306 | mysql_port_t |
| PostgreSQL | 5432 | postgresql_port_t |
| Redis | 6379 | redis_port_t |
| DNS (named) | 53 | dns_port_t |
The correct routine when moving a service to a new port: find out what port type that domain is allowed, then label the new port with the same type. The most accurate way to find the allowed port type is to search the policy, not guess:
sesearch --allow -s httpd_t -c tcp_socket -p name_bindallow httpd_t http_port_t : tcp_socket { name_bind };
allow httpd_t http_cache_port_t : tcp_socket { name_bind };This result reads directly from the active policy — you see the rules that actually apply, not guesses from type names. If you run several instances with different ports, don't forget semanage port -m to change the label of an existing port, and -d to remove:
semanage port -m -t http_port_t -p tcp 8443
semanage port -d -p tcp 8080A suggested habit: check your custom entries with semanage port -l -C (the -C flag shows only local changes) — the fastest way to make sure there are no conflicting double-labeled ports during maintenance.
One level up: how does a connection get "labeled"? A basic fact that's often misunderstood: SELinux doesn't implicitly label packets. What gets labeled is the socket (kernel object of type tcp_socket, udp_socket, etc.). A socket's label is inherited from the context of the process that created it. When httpd_t creates a socket and accepts a connection, that socket carries the httpd_t context; the connection peer may carry a different domain label as long as the policy allows the communication.
At the global network level, there are three labelable objects: interface (semanage interface), node/host (semanage node), and port (semanage port). Interface and node labeling matter especially under an MLS policy (episode 9): with NetLabel/CIPSO, a packet arriving from the network is labeled based on its source node, then verified against the sensitivity classification requested by the receiver. For ordinary targeted policies, port labeling is the most used.
The most interesting part: SELinux and netfilter/iptables are not separate systems — they collaborate. SELinux provides sk_policy classification, and iptables provides targets that trigger that classification. Two targets in the security table:
The classic pattern for labeling incoming and outgoing HTTP traffic:
iptables -t security -A INPUT -j SECMARK --selctx system_u:object_r:httpd_packet_t
iptables -t security -A INPUT -j CONNSECMARK --save
iptables -t security -A OUTPUT -j CONNSECMARK --restoreThen the policy states which domains may communicate with packets carrying that label:
allow httpd_t httpd_packet_t:packet recv;
allow httpd_t httpd_packet_t:packet send;Notice the packet class and the send/recv permissions — classes only relevant after SECMARK is in use. Without SECMARK targets in iptables, the packet rules in the policy will never be triggered. This is the bridge that's often forgotten: the SELinux policy governs what's allowed, netfilter decides which packets get labeled.
Caution
Once you install SECMARK rules in the security table, all matching traffic will be verified against the packet policy. If no matching allow rule exists, the connection is dropped with an avc: denied { recv } denial on the packet class. The correct debugging order: check iptables -t security -L -v to make sure the label is applied, then search the policy with sesearch --allow -c packet.
A perspective that ties it all together: port labeling is a userspace map answered at bind() time, while SECMARK is a kernel map answered when a packet passes netfilter. Both produce labels that are equally evaluated by the LSM hooks — one source of decisions (the policy), many labeling points.
In this episode 13, you've dissected SELinux network labeling from the bottom up: understanding that bind() on a wrongly labeled port produces a name_bind denial with tcontext=port_t; managing the port map with semanage port -a/-m/-d/-l and reading local changes via -C; mapping services to the correct port type using sesearch --allow -c tcp_socket -p name_bind; understanding that SELinux labels sockets (not packets) and inherits the creating process's context; and composing packet and connection labeling with SECMARK and CONNSECMARK in the iptables security table along with packet class rules in the policy.
The essentials to take with you:
name_bind denial with tcontext=port_t means the port isn't labeled, not that the policy is lacking.sesearch on the active policy, not from type names.Now your network is consistently labeled from ports to packets. But label consistency alone isn't enough — what makes a system secure is how many domains are actually locked down, and how little privilege remains. In the next episode 14, we enter the realm of Hardening & Least Privilege: reducing the unconfined domain, confining users and services, disabling unused booleans, analyzing the policy with sesearch, and bringing the OpenSCAP/CIS baseline to your machines. See you in episode 14!