Configure SELinux Port Labels and Booleans

Tested on RHEL 10.2 (Coughlan)
Package policycoreutils-python-utils 3.10-1.el10
policycoreutils 3.10-1.el10
selinux-policy-targeted 42.1.18-4.el10_2.1
audit 4.0.3-5.el10
httpd 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.

IMPORTANT
This guide covers port labels and policy booleans only. If your problem is a service that cannot read a file or directory, that is a context problem rather than a port or boolean problem, and it belongs in SELinux modes and file contexts. Writing custom policy modules is out of scope here because a port label or a boolean solves the vast majority of these cases.

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_t for 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.

bash
sestatus | head -6

Sample output:

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:          enforcing

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

bash
ps -eZ | grep httpd | head -3

Sample output:

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 httpd

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

bash
semanage port -l | head -6

Sample output:

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      2040

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

bash
semanage port -l | grep -w http_port_t

Sample output:

output
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
http_port_t                    udp      80, 443

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

bash
printf 'Listen 8404\n' > /etc/httpd/conf.d/golc-port.conf

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

bash
printf 'GoLinuxCloud SELinux port lab\n' > /var/www/html/index.html

That is also silent. Now ask httpd to pick up the new listener and watch what happens.

bash
systemctl restart httpd

Sample output:

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.

bash
journalctl -u httpd --since "3 min ago" --no-pager | grep -E "make_sock|no listening sockets"

Sample output:

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.

bash
ausearch -m AVC -ts recent | grep name_bind | tail -1

Sample output:

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=0

That single line names everything you need to fix it:

  • denied { name_bind } — the operation was binding a port, not reading a file
  • comm="httpd" and scontext=…httpd_t — the domain asking for permission
  • src=8404 — the port number involved
  • tcontext=…unreserved_port_t — the label port 8404 currently carries
  • tclass=tcp_socket — TCP, not UDP
  • permissive=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.

bash
semanage port -a -t http_port_t -p tcp 8404

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

bash
semanage port -l | grep -w http_port_t

Sample output:

output
http_port_t                    tcp      8404, 80, 81, 443, 488, 8008, 8009, 8443, 9000
http_port_t                    udp      80, 443

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

bash
semanage port -l -C

Sample output:

output
SELinux Port Type              Proto    Port Number

http_port_t                    tcp      8404

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

bash
firewall-cmd --permanent --add-port=8404/tcp

Sample output:

output
success

The --permanent flag writes the rule to disk without applying it to the running firewall, so the change needs a reload before it means anything.

bash
firewall-cmd --reload

Sample output:

output
success

Now both layers agree, so the restart that failed earlier should succeed.

bash
systemctl restart httpd

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

bash
ss -tlnp | grep 8404

Sample output:

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.

bash
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8404/

Sample output:

output
HTTP 200

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

bash
semanage port -l | grep -w ssh_port_t

Sample output:

output
ssh_port_t                     tcp      22

Only port 22, which is why an sshd configured for 2222 cannot start. Adding the mapping is the same shape of command as before.

bash
semanage port -a -t ssh_port_t -p tcp 2222

Nothing is printed when the mapping is accepted, so verify it the same way.

bash
semanage port -l | grep -w ssh_port_t

Sample output:

output
ssh_port_t                     tcp      2222, 22

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

WARNING
Never restart 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.

bash
semanage port -a -t http_port_t -p tcp 8080

Sample output:

output
Port tcp/8080 already defined, modifying instead

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

bash
semanage port -l | grep -w 8080

Sample output:

output
http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010
http_port_t                    tcp      8080, 8404, 80, 81, 443, 488, 8008, 8009, 8443, 9000

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

bash
semanage port -m -t http_port_t -p tcp 8080

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

bash
semanage port -d -t http_port_t -p tcp 8080

The delete is silent as well, so confirm the port went back to the label the policy shipped with.

bash
semanage port -l | grep -w 8080

Sample output:

output
http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010

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

bash
semanage port -d -t http_cache_port_t -p tcp 8118

Sample output:

output
ValueError: Port tcp/8118 is defined in policy, cannot be deleted

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

bash
semanage port -a -t nosuch_port_t -p tcp 9911

Sample output:

output
ValueError: Type nosuch_port_t is invalid, must be a port type

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

bash
semanage port -E

Sample output:

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.

bash
firewall-cmd --permanent --remove-port=8404/tcp && firewall-cmd --reload

Sample output:

output
success
success

The port is now closed to the outside world, with firewall-cmd --list-ports returning nothing at all. Repeat the request that succeeded before.

bash
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8404/

Sample output:

output
HTTP 200

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

bash
journalctl -u httpd --since "40 min ago" --no-pager | grep -E "make_sock|no listening sockets"

Sample output:

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 down

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

bash
semanage boolean -l | head -6

Sample output:

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 content

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

bash
getsebool -a | head -8

Sample output:

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 --> off

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

bash
getsebool -a | grep httpd_can_network

Sample output:

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 --> off

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

bash
mkdir -p /var/tmp/golc-backend && printf 'backend service reached\n' > /var/tmp/golc-backend/index.html

Both commands are silent when they succeed. Serve that directory with a one-line Python server so there is a real listener to proxy to.

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

bash
curl -s http://127.0.0.1:9500/

Sample output:

output
backend service reached

The backend is healthy, which rules it out as a suspect later. Now point Apache at it with a proxy drop-in.

bash
printf 'ProxyPass /api/ http://127.0.0.1:9500/\n' > /etc/httpd/conf.d/golc-proxy.conf

The file is written silently, so reload Apache to activate the proxy route.

bash
systemctl restart httpd

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

bash
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8404/api/

Sample output:

output
HTTP 503

A 503 means Apache could not reach its backend, and the error log is more specific about why.

bash
tail -2 /var/log/httpd/error_log

Sample output:

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.

bash
ausearch -m AVC -ts recent | grep name_connect | tail -1

Sample output:

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=0

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

bash
ausearch -m AVC -ts recent | grep name_connect | tail -1 | audit2why

Sample output:

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 1

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

bash
getsebool httpd_can_network_connect

Sample output:

output
httpd_can_network_connect --> off

Off, 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.

bash
setsebool httpd_can_network_connect on

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

bash
curl -s http://localhost:8404/api/

Sample output:

output
backend service reached

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

bash
semanage boolean -l | grep -w httpd_can_network_connect

Sample output:

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.

bash
semanage boolean -l -C

Sample output:

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 nfs

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

bash
setsebool -P httpd_can_network_connect on

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

bash
semanage boolean -l -C

Sample output:

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 nfs

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

bash
cat /var/lib/selinux/targeted/active/booleans.local

Sample output:

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=1

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

bash
setsebool -P httpd_can_network_connect off

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

bash
semanage boolean -l -C

Sample output:

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 nfs

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

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

bash
semanage boolean -E

Sample output:

output
boolean -m -0 httpd_can_network_connect
boolean -m -1 virt_sandbox_use_all_caps
boolean -m -1 virt_use_nfs

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

bash
ausearch -m AVC -ts today | grep -c "avc:  denied"

Sample output:

output
17

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

bash
ausearch -m AVC -ts today | grep name_bind | tail -1 | audit2why

Sample output:

output
Was caused by:
	The boolean nis_enabled was set incorrectly. 
	Description:
	Allow nis to enabled

	Allow access by executing:
	# setsebool -P nis_enabled 1

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

bash
ausearch -m AVC -ts recent | audit2allow

Sample output:

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_bind and name_connect on 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


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.


Frequently Asked Questions

1. Why does my service fail to start on a custom port even though the firewall is open?

Because the firewall and SELinux answer different questions. firewalld decides whether packets from outside may reach a port, while SELinux decides whether the service process is allowed to bind that port number at all. A confined daemon such as httpd may only bind port numbers labelled with a type its domain is permitted to use, so a port nobody has labelled is refused before any traffic is involved. The clue is an errno 13 permission denied bind failure in the service log together with a name_bind denial in the audit log.

2. What is the difference between semanage port -a and semanage port -m?

The -a option adds a new mapping and -m modifies an existing one. On the policycoreutils 3.10 build tested here, using -a for a port already defined by base policy prints already defined, modifying instead and creates the local override automatically. They are still not interchangeable: use -m when you know the port already exists, and -a when you are labelling a port number nothing has claimed yet.

3. Does setsebool without -P survive a reboot?

No. Without -P the change is applied to the running kernel policy only and nothing is written to the on-disk boolean store, so the value returns to whatever the store holds when the machine boots. The -P option writes the value into the local policy store and takes several seconds instead of a fraction of one. Use the fast form to test a theory and the persistent form only once you have confirmed the boolean is the right fix.

4. Why does audit2why suggest a boolean that looks wrong for my problem?

Because audit2why can report booleans whose enabled rules would permit the denied access, but it cannot decide which permitted solution best matches your service configuration. A denial on binding an unlabelled high port can be silenced by a broad boolean that lets daemons bind any port, which is a much larger permission grant than labelling the single port you actually need. Read every suggestion, pick the narrowest one that matches what the service legitimately does, and prefer a port label over a broad boolean when the denial is about a port number.

5. How do I see only the SELinux port and boolean changes made on this host?

Add the -C option to the list commands. semanage port -l -C shows only local port mappings rather than all several hundred from the base policy, and semanage boolean -l -C shows persistent local boolean customizations. A boolean can remain in that list even after you set it back to the policy default value, because the local customization record still exists. The -E option goes further and prints local customizations as the semanage commands that would recreate them, which is useful for documenting a host or replaying the same settings elsewhere.

6. Can I delete an SELinux port label that came with the policy?

No. Only local customizations can be removed, so semanage port -d works on mappings you added and refuses anything shipped in the base policy with an error saying the port is defined in policy and cannot be deleted. Check semanage port -l -C first to see what is actually yours to delete. If a base policy label genuinely conflicts with your service, add your own mapping for that port instead of trying to remove the original.

7. Should I turn SELinux off when a service will not bind its port?

No, because the fix is usually a single command and disabling SELinux removes protection from every service on the host, not just the one you are debugging. Labelling one port number or switching one boolean is a far smaller change than turning off mandatory access control. If you need breathing room while you investigate, set permissive mode so denials are logged without being blocked, then make the targeted change and return to enforcing.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)