LearnRHCSA (EX200)Security

SELinux Port Labels

semanage - a hands-on Linux lab on a real virtual machine.

Let a service listen on a nonstandard port under Enforcing SELinux. SELinux keeps its own list of which TCP ports each service type may bind, so a web server told to use port 8404 gets a permission-denied on the bind even when the daemon and firewall are both correct. Read the current labels with semanage port -l, add your port with semanage port -a -t http_port_t -p tcp 8404, and verify by listing the type again. Use -a to add, -m to modify an existing entry, -d to delete one. Serves EX200 security: manage SELinux port labels.

You have been told to run a web dashboard on port 8404. You do it properly. You edit the config to listen on 8404. You open the port in the firewall. You start the service, and it dies on the spot. The log says permission denied on the bind. So you check again: the daemon config is right, the firewall is right, you are root. Everything a normal Linux course taught you is correct, and the service still refuses to listen.

There is a third gate you have not touched. SELinux keeps its own list of which TCP ports each kind of service is allowed to bind. Port 8404 is not on the web server's list, so the kernel blocks the bind before the daemon ever gets to run. This lesson is the one command that fixes it: teaching SELinux that a nonstandard port belongs to a service. It is a classic exam trap, because the daemon and the firewall are both innocent, and the fix is nowhere near either of them.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. The outputs are from a real RHEL 10 machine (AlmaLinux 10.2) running SELinux in Enforcing mode. The exact port list for a type, and the order ports appear in, are policy defaults that can shift with the policy version, so treat the specific numbers as illustrative. What is stable everywhere is the SHAPE: semanage port -l lists a type and its allowed ports, and -a adds a new port to that list.

SELinux labels everything. Files carry a label, processes carry a label, and so do network ports. A port label is a type, ending in _t, that says which service owns that port number. Port 80 and 443 carry http_port_t, the web server's label. Port 22 carries ssh_port_t. When a daemon tries to bind a port, SELinux checks whether the daemon's own type is allowed to bind a port carrying that label. No match, no bind.

The tool that reads and edits these labels is semanage, short for SELinux manage. It is the permanent, policy-level editor: changes made with semanage survive a reboot and a full filesystem relabel, unlike a one-off runtime tweak. Port labels are one of its subcommands: semanage port. Think of it as the master shelving list for the whole building, the authoritative record of which service is allowed on which port.

Here is the mental model. A firewall decides whether a packet is allowed to reach the machine. SELinux decides whether a process is allowed to perform an action, and binding a port is an action. These are two independent gates, and a service has to clear both.

So when you move a web server to port 8404, you have three things to get right, not two. The daemon must be configured for 8404, the firewall must allow 8404, and SELinux must label 8404 as a port the web server may bind. Miss the third and the symptom is a permission denied on the bind that looks exactly like a bug in the daemon. A working Linux engineer reaches for semanage port the moment a service refuses to listen on a nonstandard port with the daemon and firewall already correct.

Before you change anything, look at what the policy already allows. semanage port -l lists every port label on the system, one type per row, with the protocol and the ports that carry that label. It is a long list, so you filter it to the type you care about. Here you want the web server's type, http_port_t, so you pipe the list through grep for that line.

Before you run it, know what success looks like: one row naming http_port_t, the protocol tcp, and the ports the policy currently allows the web server to bind. List the web server's allowed ports:

semanage port -l | grep ^http_port_t

prompt: [root@servera ~]# answer: semanage port -l | grep ^http_port_t output: http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000 hint: The SELinux port lister is semanage port -l, filtered with grep for the type: semanage port -l | grep ^http_port_t

One row tells you the whole rule. The first field, http_port_t, is the label, the web server's port type. The second field, tcp, is the protocol. Everything after is the list of ports that carry that label: 80, 81, 443, 488, 8008, 8009, 8443, 9000. Any daemon running as the web server type may bind any port on that list and no others. Notice 8404 is not there. That is exactly why a web server told to listen on 8404 gets its bind refused: the port is not labeled for it.

The exact list of ports for http_port_t, and the order they print in, are policy defaults that depend on the selinux-policy package version, so your machine may show a slightly different set. What is stable is the shape of the row, the label then the protocol then the port list, and the fact that a port not on the list cannot be bound by that type. Read the row for whether YOUR target port is present, not for the specific numbers around it.

Now the fix. To let the web server bind port 8404, you add 8404 to the http_port_t label. The verb is -a, add. The full form names three things: the type with -t, the protocol with -p, and the port number last. Read it as: add, to type http_port_t, protocol tcp, port 8404.

The command prints nothing on success, so you prove it worked by listing the label again and watching 8404 appear. Add the port, then re-list:

semanage port -a -t http_port_t -p tcp 8404

prompt: [root@servera ~]# answer: semanage port -a -t http_port_t -p tcp 8404 output: hint: The add verb is -a, then -t for the type, -p for the protocol, and the port last: semanage port -a -t http_port_t -p tcp 8404

No output, which is success: semanage is silent when the change goes through. It has written a new entry into the local SELinux policy saying that TCP port 8404 now carries http_port_t. That change is permanent. It survives a reboot and even a full relabel, because it lives in the policy, not in memory. The web server may now bind 8404, assuming the daemon and firewall are also set. To see the proof, list the label one more time.

The verify step is the same list command you started with. Run semanage port -l | grep ^http_port_t again and read the row: 8404 should now be part of the port list for the type. Its position in the list does not matter; SELinux does not care about order, only membership.

Confirm the port is now labeled:

semanage port -l | grep ^http_port_t

prompt: [root@servera ~]# answer: semanage port -l | grep ^http_port_t output: http_port_t tcp 8404, 80, 81, 443, 488, 8008, 8009, 8443, 9000 hint: Re-run the same lister and look for your new port: semanage port -l | grep ^http_port_t

The row now reads 8404, 80, 81, 443, 488, 8008, 8009, 8443, 9000. Your added port appears at the front, but it could appear anywhere; the only thing that matters is that it is on the list. With 8404 now carrying http_port_t, SELinux will permit the web server to bind it, and the permission denied that started this whole lesson disappears. This before-and-after list is the standard way to prove a port label change on the exam: list, add, list again.

Use -a to add a brand-new port and -m to modify a port that already exists in the policy. They are not interchangeable. If you try to -a a port that is already defined, SELinux refuses with ValueError: Port tcp/PORT already defined, and if you try to -m a port that was never added, it errors that the port is not defined. When you need to change an existing custom entry, reach for -m, not -a. And -d deletes an entry you added, handing the port back to its old label.

The same rule governs every service, not just the web server. SSH has its own port type, ssh_port_t, and by default it carries exactly one port. List it the same way to see how tight a default can be:

semanage port -l | grep ^ssh_port_t

prompt: [root@servera ~]# answer: semanage port -l | grep ^ssh_port_t output: ssh_port_t tcp 22 hint: Same lister, different type. Filter for ssh_port_t: semanage port -l | grep ^ssh_port_t

ssh_port_t carries a single port, 22. This is why moving sshd to a nonstandard port, a common security-hardening move, breaks SSH until you tell SELinux about it. If you set sshd to listen on 2222, you must run semanage port -a -t ssh_port_t -p tcp 2222 before sshd can bind it, exactly as you did for the web server. The type name changes, the pattern does not: find the service's port type, add your port to it with -a, verify with -l.

Scaffolding off. No command is printed this time. You have every piece you need.

A task hands you a web server that must listen on TCP port 8404. You have already fixed the daemon config and opened 8404 in the firewall, but the service still refuses to bind with a permission denied. You know the reason: SELinux has not labeled 8404 for the web server type http_port_t. Which single command adds that port to that type over TCP, permanently, so the bind will be allowed? Recall the verb for add, the flag for the type, and the flag for the protocol.

prompt: [root@servera ~]# answer: semanage port -a -t http_port_t -p tcp 8404 output: hint: It is semanage port with the add verb -a, the type flag -t http_port_t, the protocol flag -p tcp, then the port 8404.

semanage port -a -t http_port_t -p tcp 8404 is the fix. It adds TCP port 8404 to the web server's port type, permanently, so SELinux will now permit the bind. Read it as its four parts: port is the subcommand, -a is add, -t http_port_t is the target type, -p tcp is the protocol, and 8404 is the port. Silence after you press Enter means it worked; confirm with semanage port -l | grep ^http_port_t and watch 8404 appear. This is the exact move that unblocks a daemon whose config and firewall are already correct.

You earned this cheat sheet. Every row is a form you just ran or built:

The one thing to burn in for the exam: when a service will not listen on a nonstandard port and the daemon and firewall are both correct, SELinux is the third gate. Find the service's port type, add your port with semanage port -a -t <type> -p tcp <port>, and verify with semanage port -l.

Remember the verbs as a set: -a add a new port, -m modify one already defined, -d delete one you added. Reach for -a first; only fall back to -m when SELinux tells you the port is already defined. Whenever a network service fails to start with the config and firewall correct, ask three questions in order. Is the daemon set for the port, is the firewall open, is the port SELinux-labeled for the service. The third is the one most people forget.

The practice terminal walked you through the whole loop. semanage port -l to read which ports a type may bind, -a to add a nonstandard port to a service's type, a second -l to prove it landed, and the -m and -d verbs for modifying and removing entries. You saw why the bind is blocked even when the daemon and firewall are fine, and you saw the same pattern hold for both the web server and SSH. Every command you typed yourself.

Operation Lockdown, the security capstone, is where you run these against a real RHEL 10 machine with SELinux in Enforcing mode. A full VM boots for you, with real services, a real firewall, and a real policy waiting to be edited. The mission hands you objectives that use exactly what you practiced here. Point a service at a nonstandard port, watch the bind fail, and fix it by labeling the port for the service's type. One difference from this lesson: the mission shows no commands. You read the objective, recall the semanage port -a -t <type> -p tcp <port> form, and type it. That recall is what makes it stick on exam day.

Finish the other security lessons, then take on Operation Lockdown and label a real port under Enforcing SELinux.

Practice SELinux Port Labels in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.