| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | policycoreutils-python-utils 3.10-1.el10policycoreutils 3.10-1.el10selinux-policy-targeted 42.1.18-4.el10_2.1audit 4.0.3-5.el10httpd 2.4.63-13.el10_2.5 (lab scenario) |
| Applies to | RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora |
| Privilege | Normal user for read-only checks with getsebool and semanage -l; sudo or root to label ports, change booleans, and restart services |
| Scope | Labelling TCP and UDP ports so a confined service can bind a non-default port, and switching optional service behaviour with policy booleans. Covers reading the AVC evidence first, local policy customizations, rollback, and how SELinux differs from the firewall. Does not cover file contexts or writing custom policy modules. |
| Related guides | SELinux modes and file contexts firewalld cheat sheet Disable SELinux ss command RHCSA tutorial |
Two SELinux problems account for most of the "it works with SELinux off" reports you will ever field. A service is moved to a non-default port and refuses to start, or a service needs to do something optional that policy keeps switched off by default. Neither one needs a custom policy module, and neither one is a reason to disable anything.
Both have a one-command fix once you know which command. This walkthrough runs both failures on a live RHEL 10.2 host, reads the denial that explains each one, applies the narrow fix, and then rolls it back.
Why SELinux Controls Ports and Optional Service Behaviour
SELinux decides what a process may do by comparing labels, not by trusting the process. Four moving parts matter for the two problems in this guide:
- Process domain — the label the service runs under, such as
httpd_tfor Apache - Port type — the label attached to a TCP or UDP port number, such as
http_port_t - Policy rule — the statement that a given domain may bind or connect to a given port type
- Boolean — a switch shipped with policy that turns a whole group of optional rules on or off
A service can bind a port only when a rule already allows its domain to use that port's type. Nothing about this involves network traffic yet, which is why the failure shows up as a startup error rather than a connection problem.
That distinction is worth holding onto, because three independent layers have to agree before a client on another machine gets a response:
- The application has to be configured to listen on the port and actually start
- SELinux has to permit that domain to bind that port type
- firewalld has to allow packets from outside to reach the port
Fixing the wrong layer is the most common way to waste an afternoon here. The rest of this guide keeps the three separate and shows how to tell which one is complaining.
SELinux Port and Boolean Quick Reference
Every command below is used somewhere in this guide. Listing is read-only and needs no elevation; anything that changes policy needs root.
Port labels with semanage port:
| Task | Command |
|---|---|
| List every port label | semanage port -l |
| Show one type's ports | semanage port -l | grep -w http_port_t |
| Show only local changes | semanage port -l -C |
| Add a port to a type | semanage port -a -t http_port_t -p tcp 8404 |
| Modify an already-defined port | semanage port -m -t http_port_t -p tcp 8080 |
| Delete a local mapping | semanage port -d -t http_port_t -p tcp 8404 |
| Export local changes as commands | semanage port -E |
Policy switches with getsebool and setsebool:
| Task | Command |
|---|---|
| List every boolean and value | getsebool -a |
| Check one boolean | getsebool httpd_can_network_connect |
| List booleans with descriptions | semanage boolean -l |
| Show only local changes | semanage boolean -l -C |
| Change until reboot | setsebool httpd_can_network_connect on |
| Change persistently | setsebool -P httpd_can_network_connect on |
| Find the denial to act on | ausearch -m AVC -ts recent |
Understand SELinux Port Types
Before touching policy, confirm SELinux is actually enforcing, because a denial you cannot reproduce is usually a host in permissive mode.
sestatus | head -6Sample output:
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcingEnforcing with the targeted policy is the RHEL default and the only configuration where the failures below happen. Next, look at the label the web server process runs under, since that domain is one half of every rule you are about to read.
ps -eZ | grep httpd | head -3Sample output:
system_u:system_r:httpd_t:s0 108534 ? 00:00:00 httpd
system_u:system_r:httpd_t:s0 108535 ? 00:00:00 httpd
system_u:system_r:httpd_t:s0 108537 ? 00:00:00 httpdThe third field, httpd_t, is the domain. Policy grants httpd_t the right to bind ports of type http_port_t, so the question for any new port becomes simple: does that port number carry the http_port_t label?
List SELinux Port Labels
The full table is long, so start with the header to see what the three columns mean.
semanage port -l | head -6Sample output:
SELinux Port Type Proto Port Number
afs3_callback_port_t tcp 7001
afs3_callback_port_t udp 7001
afs_bos_port_t udp 7007
afs_fs_port_t tcp 2040Each row is a port type, a protocol, and the port numbers carrying that label. Note that tcp and udp are separate rows even for the same type, so labelling a TCP port does nothing for UDP on the same number. Filtering by the type you care about is far more useful than reading all several hundred rows.
semanage port -l | grep -w http_port_tSample output:
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
http_port_t udp 80, 443Those are the TCP ports a web server can bind out of the box on this host. Anything outside that list needs either a policy that already labels it for web use or a mapping you add yourself, and that is exactly the failure worth reproducing.
Move a Service to a Non-Default Port
The scenario is deliberately ordinary: Apache should also answer on TCP 8404. Rather than editing the packaged configuration, drop a one-line file into conf.d so the only new variable is the extra port.
printf 'Listen 8404\n' > /etc/httpd/conf.d/golc-port.confprintf writes the file without any output. Give the site something unmistakable to serve as well, so a later test cannot be confused with Apache's default welcome page.
printf 'GoLinuxCloud SELinux port lab\n' > /var/www/html/index.htmlThat is also silent. Now ask httpd to pick up the new listener and watch what happens.
systemctl restart httpdSample output:
Job for httpd.service failed because the control process exited with error code.
See "systemctl status httpd.service" and "journalctl -xeu httpd.service" for details.The restart failed rather than starting with one port missing, because Apache treats a listener it cannot open as fatal. The service log says why, and the exact wording is what you should learn to recognise.
journalctl -u httpd --since "3 min ago" --no-pager | grep -E "make_sock|no listening sockets"Sample output:
Aug 08 19:01:26 vm1.lab.example httpd[103501]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:8404
Aug 08 19:01:26 vm1.lab.example httpd[103501]: (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:8404
Aug 08 19:01:26 vm1.lab.example httpd[103501]: no listening sockets available, shutting down(13)Permission denied on a bind is the signature of this problem. Apache is running as root at that moment, so ordinary Unix permissions cannot explain it, and there is no mention of the port being in use. That combination points at mandatory access control, and the audit log is where the proof lives. Filtering the journal like this is a standard move worth having in reach; more patterns live in journalctl log filtering.
ausearch -m AVC -ts recent | grep name_bind | tail -1Sample output:
type=AVC msg=audit(1786195886.425:6098): avc: denied { name_bind } for pid=103501 comm="httpd" src=8404 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0That single line names everything you need to fix it:
denied { name_bind }— the operation was binding a port, not reading a filecomm="httpd"andscontext=…httpd_t— the domain asking for permissionsrc=8404— the port number involvedtcontext=…unreserved_port_t— the label port 8404 currently carriestclass=tcp_socket— TCP, not UDPpermissive=0— the request was actually blocked, not merely logged
unreserved_port_t is the fallback label for high ports nobody has claimed, and httpd_t has no rule permitting it. The fix is to move 8404 into the type the web server is already allowed to bind.
semanage port -a -t http_port_t -p tcp 8404The command prints nothing on success and takes noticeably longer than a normal command, around sixteen seconds on this host, because it rebuilds and reloads the local policy store. Confirm the new number joined the type.
semanage port -l | grep -w http_port_tSample output:
http_port_t tcp 8404, 80, 81, 443, 488, 8008, 8009, 8443, 9000
http_port_t udp 80, 443Port 8404 now sits in the TCP row, listed first because local customizations are printed ahead of the ports that came with the policy. That ordering is a handy tell, and there is a cleaner way to ask the same question.
semanage port -l -CSample output:
SELinux Port Type Proto Port Number
http_port_t tcp 8404With -C you see only what this host changed, which is the list you will want when documenting a server or undoing your own work later. SELinux is satisfied now, but the firewall is a separate layer and has not been touched.
firewall-cmd --permanent --add-port=8404/tcpSample output:
successThe --permanent flag writes the rule to disk without applying it to the running firewall, so the change needs a reload before it means anything.
firewall-cmd --reloadSample output:
successNow both layers agree, so the restart that failed earlier should succeed.
systemctl restart httpdsystemctl is silent when a restart works, which is the first sign the port label did its job. Ask the kernel which sockets are actually open rather than trusting that silence; the ss command reports listeners with the owning process.
ss -tlnp | grep 8404Sample output:
LISTEN 0 511 *:8404 *:* users:(("httpd",pid=110737,fd=6),("httpd",pid=110734,fd=6),("httpd",pid=110733,fd=6),("httpd",pid=110731,fd=6))Apache is listening on 8404 with worker processes attached. One request confirms the whole path end to end.
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8404/Sample output:
HTTP 200A 200 means the listener, the port label, and the content all line up. Notice what fixed it: one port number added to one type, not a policy module and not a mode change.
Add a Port Mapping for Another Service with -a
The same one-liner works for any confined service, and the only thing that changes is the type name. SSH is the classic example, since moving sshd to 2222 fails in exactly the way Apache just did. Look at what the type holds first.
semanage port -l | grep -w ssh_port_tSample output:
ssh_port_t tcp 22Only port 22, which is why an sshd configured for 2222 cannot start. Adding the mapping is the same shape of command as before.
semanage port -a -t ssh_port_t -p tcp 2222Nothing is printed when the mapping is accepted, so verify it the same way.
semanage port -l | grep -w ssh_port_tSample output:
ssh_port_t tcp 2222, 22Both ports are now bindable by the SSH domain. Labelling the port is all SELinux needs; changing Port in sshd_config and opening the firewall are separate jobs covered in the ssh command guide. Do the label first, because an sshd that cannot bind its new port while you are connected over the old one is an unpleasant discovery. If you added 2222 only to see the command work, take it back out with semanage port -d -t ssh_port_t -p tcp 2222 using the rollback pattern shown below.
sshd on a remote host after changing its port until the SELinux label and the firewall rule are both in place, and keep your current session open while you test the new port from a second connection.
Modify an Already-Defined Port with -m
Some port numbers are already claimed by a different type, and that is where most people get stuck. Port 8080 is the usual example, because it looks like a web port but belongs to the caching proxy type. Try adding it to http_port_t the same way as before.
semanage port -a -t http_port_t -p tcp 8080Sample output:
Port tcp/8080 already defined, modifying insteadThis is where current behaviour differs from what most documentation describes. Older semanage refused the operation with Port tcp/8080 already defined and made you rerun it with -m; on policycoreutils 3.10 the message is a warning, the command exits successfully, and the modify happens for you. Look at who claims 8080 now.
semanage port -l | grep -w 8080Sample output:
http_cache_port_t tcp 8080, 8118, 8123, 10001-10010
http_port_t tcp 8080, 8404, 80, 81, 443, 488, 8008, 8009, 8443, 9000Both rows appear because semanage port -l displays the original policy definition as well as the local customization. They are not two simultaneously active labels: the local http_port_t mapping has higher priority and is the effective mapping for TCP 8080. Isolating it with semanage port -l -C shows only the override you created, which is the row that decides the port's context. Spelling the intent out with -m produces the same result without the warning.
semanage port -m -t http_port_t -p tcp 8080It exits silently because there is nothing new to report. The two options are still separate operations rather than aliases: -a adds a mapping and -m modifies one that exists, so -m is not a drop-in replacement for -a on a port number nothing has claimed yet. Reach for -m when you already know the port is defined elsewhere, since the next person reading your change log then sees a deliberate modification rather than an add that happened to be rescued.
Delete a Local Port Mapping with -d
Rolling back is the same command shape with -d, and it only ever touches your own customizations. Undo the 8080 experiment.
semanage port -d -t http_port_t -p tcp 8080The delete is silent as well, so confirm the port went back to the label the policy shipped with.
semanage port -l | grep -w 8080Sample output:
http_cache_port_t tcp 8080, 8118, 8123, 10001-10010Only the base policy row is left, which is exactly what rollback should look like. The limit of -d becomes obvious the moment you aim it at something you did not add.
semanage port -d -t http_cache_port_t -p tcp 8118Sample output:
ValueError: Port tcp/8118 is defined in policy, cannot be deletedPorts that came with the policy are read-only, so treat semanage port -l -C as the authoritative list of what is yours to remove. A mistyped type name fails in its own distinct way, which saves guessing when a command is rejected.
semanage port -a -t nosuch_port_t -p tcp 9911Sample output:
ValueError: Type nosuch_port_t is invalid, must be a port typeThe type has to exist in policy before you can put a port in it, and semanage port -l is where you find the real names. There is also a quick way to capture every local mapping in a form you can replay on another host.
semanage port -ESample output:
port -a -t http_port_t -r 's0' -p tcp 8404-E prints local customizations as the semanage commands that would recreate them, which beats hand-transcribing a table into a runbook.
SELinux Port Labels vs firewalld vs the Listener
When a port does not work, three layers can be responsible and each one answers a different question.
| Layer | Question it answers | How it fails | Where to look |
|---|---|---|---|
| Application | Is the service configured to listen and did it start? | Service inactive or no socket open | systemctl status, ss -tlnp |
| SELinux | May this domain bind or connect to that port type? | (13)Permission denied on bind |
ausearch -m AVC, semanage port -l |
| firewalld | May packets from outside reach the port? | Remote timeout or refusal with a healthy local listener | firewall-cmd --list-ports |
The trap is that a local test cannot see the third layer at all. Remove the firewall rule added earlier and watch what a localhost request reports.
firewall-cmd --permanent --remove-port=8404/tcp && firewall-cmd --reloadSample output:
success
successThe port is now closed to the outside world, with firewall-cmd --list-ports returning nothing at all. Repeat the request that succeeded before.
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8404/Sample output:
HTTP 200Still 200, because loopback traffic never traverses the zone rules that filter your network interfaces. Any curl localhost test proves the listener and the SELinux label are fine and tells you nothing whatsoever about reachability, so test from a second machine before declaring a port open. Zone and service syntax for that layer lives in the firewalld cheat sheet.
The application layer has its own lookalike failure, and telling it apart from an SELinux denial comes down to one number. To see both side by side I labelled a second port, started an unrelated process on it, and then asked Apache to listen there too. Reviewing the journal across both experiments shows the difference.
journalctl -u httpd --since "40 min ago" --no-pager | grep -E "make_sock|no listening sockets"Sample output:
Aug 08 19:01:26 vm1.lab.example httpd[103501]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:8404
Aug 08 19:01:26 vm1.lab.example httpd[103501]: (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:8404
Aug 08 19:01:26 vm1.lab.example httpd[103501]: no listening sockets available, shutting down
Aug 08 19:09:23 vm1.lab.example httpd[108381]: (98)Address already in use: AH00072: make_sock: could not bind to address [::]:8405
Aug 08 19:09:23 vm1.lab.example httpd[108381]: (98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:8405
Aug 08 19:09:23 vm1.lab.example httpd[108381]: no listening sockets available, shutting downSame AH00072: make_sock wording, same fatal outcome, entirely different cause. The 19:01 failure is errno 13 and produced an AVC, so it was SELinux refusing the bind. The 19:09 failure is errno 98 with a correctly labelled port and produced no AVC at all, because another process already held the socket and the kernel refused before policy was ever consulted. Read the number in the parentheses before you go anywhere near semanage.
What Is an SELinux Boolean?
A boolean is a switch that policy authors built in ahead of time. Behind each one sits a group of rules that are either active or dormant, so flipping the switch changes what a domain may do without anyone writing or compiling policy. The listing command shows the switches with the sentence that explains each one.
semanage boolean -l | head -6Sample output:
SELinux boolean State Default Description
abrt_anon_write (off , off) Allow abrt to anon write
abrt_handle_event (on , on) Allow abrt to handle event
abrt_upload_watch_anon_write (on , on) Allow abrt to upload watch anon write
auditadm_exec_content (on , on) Allow auditadm to exec contentThe two values in brackets are the part worth learning. The first is the value in the running kernel and the second is the value stored on disk, so (on , off) means somebody switched a boolean on without making it stick. That single detail explains most "it worked yesterday" reports after a reboot.
List and Search SELinux Booleans
For a plain name-and-value list without descriptions, getsebool is faster to read.
getsebool -a | head -8Sample output:
abrt_anon_write --> off
abrt_handle_event --> on
abrt_upload_watch_anon_write --> on
auditadm_exec_content --> on
authlogin_nsswitch_use_ldap --> off
authlogin_radius --> off
authlogin_yubikey --> off
cdrecord_read_content --> offThis host has 328 booleans, so scrolling is not a strategy. Grep for the service and the capability you have in mind instead, which also shows you how narrowly the switches are scoped.
getsebool -a | grep httpd_can_networkSample output:
httpd_can_network_connect --> off
httpd_can_network_connect_cobbler --> off
httpd_can_network_connect_db --> off
httpd_can_network_memcache --> off
httpd_can_network_redis --> off
httpd_can_network_relay --> offSix different switches cover outbound web server connections, from any network service down to just databases or just Redis. Picking the narrowest one that describes your actual need is the whole skill, and the next section works through it on a real denial.
Fix a Blocked Outbound Connection with a Boolean
The second classic problem is a service that needs to reach out rather than listen. Apache is going to proxy requests to a backend on port 9500, so give that backend a directory with one identifiable file in it.
mkdir -p /var/tmp/golc-backend && printf 'backend service reached\n' > /var/tmp/golc-backend/index.htmlBoth commands are silent when they succeed. Serve that directory with a one-line Python server so there is a real listener to proxy to.
cd /var/tmp/golc-backend && nohup python3 -m http.server 9500 --bind 127.0.0.1 > server.log 2>&1 &The job goes to the background and prints only a job number, so confirm the backend answers on its own before involving Apache at all.
curl -s http://127.0.0.1:9500/Sample output:
backend service reachedThe backend is healthy, which rules it out as a suspect later. Now point Apache at it with a proxy drop-in.
printf 'ProxyPass /api/ http://127.0.0.1:9500/\n' > /etc/httpd/conf.d/golc-proxy.confThe file is written silently, so reload Apache to activate the proxy route.
systemctl restart httpdApache starts cleanly this time, because nothing about the proxy configuration involves binding a new port. The failure only appears when a request actually travels through it.
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8404/api/Sample output:
HTTP 503A 503 means Apache could not reach its backend, and the error log is more specific about why.
tail -2 /var/log/httpd/error_logSample output:
[Sat Aug 08 19:21:22.963841 2026] [proxy:error] [pid 110733:tid 110821] (13)Permission denied: AH00957: http: attempt to connect to 127.0.0.1:9500 (127.0.0.1:9500) failed
[Sat Aug 08 19:21:22.969737 2026] [proxy_http:error] [pid 110733:tid 110821] [client ::1:59992] AH01114: HTTP: failed to make connection to backend: 127.0.0.1(13)Permission denied again, this time on an outbound connection to a service that is demonstrably listening. The audit log confirms which permission was refused.
ausearch -m AVC -ts recent | grep name_connect | tail -1Sample output:
type=AVC msg=audit(1786197082.954:6206): avc: denied { name_connect } for pid=110733 comm="httpd" dest=9500 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0Note name_connect rather than name_bind, and dest=9500 rather than src. The domain is the same httpd_t and the target port is unlabelled again, so both halves of the decision are visible: the domain doing the connecting and the type of the port it is reaching for. A name_connect denial can be about either one, and here the port belongs to a foreign backend rather than to Apache, so relabelling somebody else's port would be the wrong move; the real question is whether a web server may open outbound connections at all. Ask the policy which booleans would permit it.
ausearch -m AVC -ts recent | grep name_connect | tail -1 | audit2whySample output:
Was caused by:
One of the following booleans was set incorrectly.
Description:
Allow httpd to can network connect
Allow access by executing:
# setsebool -P httpd_can_network_connect 1
Description:
Allow nis to enabled
Allow access by executing:
# setsebool -P nis_enabled 1Two candidates, and they are not equivalent. httpd_can_network_connect describes exactly what this service is doing, while nis_enabled unlocks network access for a whole family of daemons and happens to include this case as a side effect. Check the current value of the narrow one before changing anything.
getsebool httpd_can_network_connectSample output:
httpd_can_network_connect --> offOff, which matches the denial. Switch it on without -P first, because a test that is instantly reversible is the right way to confirm a theory.
setsebool httpd_can_network_connect onThe command returns in a fraction of a second with no output, since it only writes to the running kernel policy. Repeat the request that returned 503.
curl -s http://localhost:8404/api/Sample output:
backend service reachedThe proxy now reaches its backend, which proves the boolean was the real constraint and no service restart was needed for policy to take effect.
Make the Boolean Change Persistent with setsebool -P
The fix works, but nothing has been written to disk yet, and the listing makes that visible.
semanage boolean -l | grep -w httpd_can_network_connectSample output:
httpd_can_network_connect (on , off) Allow httpd to can network connect(on , off) is the mismatch described earlier: on in the kernel, off in the store. Asking for local customizations only makes the gap even clearer.
semanage boolean -l -CSample output:
SELinux boolean State Default Description
virt_sandbox_use_all_caps (on , on) Allow virt to sandbox use all caps
virt_use_nfs (on , on) Allow virt to use nfsThe boolean you just changed is absent, because a runtime change is not a customization as far as the policy store is concerned. Adding -P is what commits it.
setsebool -P httpd_can_network_connect onThis one took about seven and a half seconds against a fraction of a second for the runtime form, which is the cost of rewriting the store rather than poking the kernel. Confirm the two values now agree.
semanage boolean -l -CSample output:
SELinux boolean State Default Description
httpd_can_network_connect (on , on) Allow httpd to can network connect
virt_sandbox_use_all_caps (on , on) Allow virt to sandbox use all caps
virt_use_nfs (on , on) Allow virt to use nfsThe boolean is listed as a local customization with both values on, so it will come back after a reboot. The file behind that list is worth seeing once so the mechanism stops feeling like magic.
cat /var/lib/selinux/targeted/active/booleans.localSample output:
# This file is auto-generated by libsemanage
# Do not edit directly.
virt_use_nfs=1
virt_sandbox_use_all_caps=1
httpd_can_network_connect=1Every persistent boolean on the host is one line in that file, which is why -P is slower and why the header warns you off editing it by hand. One caveat is worth stating precisely: a runtime-only value is more durable than most guides claim, and on this build it survived both load_policy and a full semodule -B policy rebuild. What clears it is a reboot, so "temporary" means "until the machine restarts" rather than "until policy reloads".
Roll a Boolean Back
Reverting is the same command with the opposite value, and it needs -P again so the store matches.
setsebool -P httpd_can_network_connect offIt exits silently after the same few seconds of store rewriting. Check what the local customization list looks like afterwards, because it is not quite what you might expect.
semanage boolean -l -CSample output:
SELinux boolean State Default Description
httpd_can_network_connect (off , off) Allow httpd to can network connect
virt_sandbox_use_all_caps (on , on) Allow virt to sandbox use all caps
virt_use_nfs (on , on) Allow virt to use nfsThe name stays in the list with both values off. semanage boolean has no per-boolean delete, so the entry remains as an inert record that matches the policy default, which is harmless. Resist the obvious-looking cleanup here.
semanage boolean -D deletes every local boolean customization on the host, not the one you were working on. On this lab that would also have wiped two unrelated virt_* settings. Set the value back with setsebool -P instead, and use semanage boolean -E first if you need a record of what was customised.
Exporting before any bulk change is cheap insurance, and the output doubles as documentation.
semanage boolean -ESample output:
boolean -m -0 httpd_can_network_connect
boolean -m -1 virt_sandbox_use_all_caps
boolean -m -1 virt_use_nfsEach line is a semanage invocation that recreates one setting, with -0 for off and -1 for on. Keep that output before you touch a host you did not build.
Diagnose Before Changing SELinux
Everything above started from a denial rather than a guess, and that order matters more than any individual command. The audit log is the entry point, and ausearch reads it. That command comes from the audit package rather than from the SELinux tooling, which is why it can be missing on trimmed images while semanage is present; audit2why and audit2allow ship with policycoreutils-python-utils alongside semanage.
ausearch -m AVC -ts today | grep -c "avc: denied"Sample output:
17Seventeen denials today on a lab host that was deliberately breaking things. On a production box a sudden jump in that count is your signal, and -ts recent narrows it to the last ten minutes when you are reproducing a fault on demand. The temptation at this point is to let a tool decide for you, which is where care is required.
ausearch -m AVC -ts today | grep name_bind | tail -1 | audit2whySample output:
Was caused by:
The boolean nis_enabled was set incorrectly.
Description:
Allow nis to enabled
Allow access by executing:
# setsebool -P nis_enabled 1That is the port denial from the start of this guide, and the fix offered here is a boolean that lets daemons bind ports generally. It is not wrong that the boolean would have worked; it is wrong as a choice, because labelling one port number grants far less than unlocking port binding for a family of services. audit2why analyses why the current policy denied the AVC and may identify booleans whose rules would permit it. It does not understand your administrative intent and does not prescribe semanage port as the fix for a non-standard port. That is why its boolean suggestion can be technically sufficient but broader than the correct port-label change.
The other tool people reach for is audit2allow, which is worth running mainly to see what it says about policy you have already fixed.
ausearch -m AVC -ts recent | audit2allowSample output:
#============= httpd_t ==============
#!!!! This avc is allowed in the current policy
allow httpd_t unreserved_port_t:tcp_socket name_connect;The !!!! comment is audit2allow noticing that the denial it was handed is no longer denied, because the boolean covers it now. That is the healthy outcome. Generating and installing a module from this output would have hard-coded a permission that a supported switch already provides.
A short discipline covers almost every case:
- Reproduce the failure while SELinux is enforcing, so the denial is real
- Read the AVC and identify the operation, the domain, and the target label
- Choose a port label for
name_bindandname_connecton a port your service owns - Choose the narrowest boolean when the capability itself is what policy withholds
- Test without
-P, confirm the fix, then commit it - Treat a custom policy module as the last resort, not the first suggestion
Permissive mode is a legitimate diagnostic step when denials are not appearing, but it is not a fix, and turning SELinux off entirely trades one service's inconvenience for the whole host's protection. If you have landed here from that direction, disable SELinux explains what that actually costs.
Troubleshoot SELinux Port and Boolean Problems
Most failures in this area produce one of these symptoms, and the fix follows from the evidence rather than from trial and error.
| Symptom | Likely cause | Fix |
|---|---|---|
semanage: command not found |
policycoreutils-python-utils is not installed |
Install it with dnf install policycoreutils-python-utils; the base policycoreutils package does not ship semanage |
ausearch: command not found |
The audit package is missing, which is common on minimal or container images |
Install it with dnf install audit; audit2why and audit2allow come from policycoreutils-python-utils instead |
(13)Permission denied on bind with a name_bind AVC |
Port number carries a type the service domain cannot bind | Add the port to the service's port type with semanage port -a |
(98)Address already in use on bind and no AVC |
Another process already holds the port | Find the owner with ss -tlnp and stop it or pick another port; SELinux is not involved |
Port tcp/NNNN already defined, modifying instead |
Base policy already labels that port under another type | Expected on current policycoreutils; use -m to state the intent explicitly |
ValueError: Port tcp/NNNN is defined in policy, cannot be deleted |
Trying to delete a base policy label rather than a local one | Only mappings shown by semanage port -l -C can be deleted |
ValueError: Type X is invalid, must be a port type |
Type name does not exist or is not a port type | Find the real name in semanage port -l output |
| Port labelled correctly but service still refuses to bind | Wrong protocol, or the service runs under a different domain than assumed | Check -p tcp versus -p udp, and confirm the domain with ps -eZ |
| Local client works, remote client times out | Firewall layer, which loopback traffic never touches | Open the port with firewall-cmd --permanent --add-port and reload, then retest from another host |
Error getting active value for X |
Boolean name is wrong or misspelled | Find the exact name with getsebool -a | grep keyword |
Could not change active booleans: Invalid boolean |
Same cause, from setsebool rather than getsebool |
Correct the name; boolean names are exact, with no partial matching |
| Boolean was on yesterday and is off after a reboot | Set without -P, so it was never written to the store |
Re-apply with setsebool -P and confirm with semanage boolean -l -C |
| Boolean enabled but the denial persists | The denial is a different operation, such as a file context or a port label | Re-read the AVC; name_bind and file getattr denials are not boolean problems |
References
- Red Hat Enterprise Linux 10: Using SELinux
- Manual page for semanage-port(8), covering the add, modify, delete, list and extract options
- Manual page for semanage-boolean(8), including the local customization and extract options
- Manual page for setsebool(8), documenting the persistent -P behaviour
- Manual page for httpd_selinux(8), listing the web server port types and booleans
- SELinux userspace source repository, including policycoreutils and libsemanage
Summary
Port labels and booleans are the two SELinux controls you will reach for most often, and both follow the same rhythm: reproduce the failure, read the denial, apply the narrowest change, verify. A service that cannot bind a non-default port produces an errno 13 bind failure and a name_bind denial naming the port and its current label, and the fix is one semanage port -a that moves that port number into the type the service is already allowed to use. A name_connect denial means the service was blocked while opening an outbound connection. In this Apache reverse-proxy lab the supported fix was the httpd_can_network_connect boolean, but do not assume every name_connect denial needs the same switch: that decision also involves the destination port type, so read the target label and check the policy options for the service before choosing.
Two details cause the most confusion afterwards. The first is that setsebool without -P changes only the running kernel policy, which semanage boolean -l exposes as a (on , off) pair and semanage boolean -l -C hides entirely; the value comes back at the next reboot, and on this build it survived even a full policy rebuild, so "temporary" is more durable than it sounds. The second is that a curl localhost test passes whether or not the firewall allows the port, because loopback traffic never meets the zone rules. Treat the listener, the SELinux label, and the firewall as three separate questions and test the third one from another machine.
The habit worth carrying away is distrusting tools that answer too quickly. audit2why explains why policy refused a request and can point at booleans that would permit it, but it has no idea what you were trying to achieve, so on a port denial it offered a broad boolean that would have worked while granting far more than the single port label the problem actually needed. Read every suggestion it offers, pick the one whose description matches what your service legitimately does, and check semanage port -l -C and semanage boolean -l -C before you change a host somebody else built, so you can put it back exactly as you found it.
Start with a harmless experiment on your own lab: move a test web server to a high port, watch it fail, and fix it with one port mapping. That single exercise teaches the bind denial, the type lookup, the firewall boundary, and the rollback path in about five minutes, and it is the same shape as every real version of this problem you will meet later.

