SELinux BasicsLesson 2 of 3
File contexts and port labels
Make a label stick with semanage and restorecon, relabel moved files, and teach SELinux about a service on a non-standard port.
chconsemanage fcontextrestoreconsemanage portls -Zsemanage fcontext -ltouch /.autorelabelpolicycoreutils-python-utils
After this lesson you can
- Explain why chcon is temporary and semanage fcontext plus restorecon is permanent.
- Set a persistent context for a custom web root using a pattern, and restore it after files are moved or created.
- List port labels, add a port to a type, and recognise a bind failure caused by SELinux.
The idea
Every file gets its label from rules, not from where you decided to put it. The policy contains a table that maps path patterns to types: /var/www(/.*)? is httpd_sys_content_t, /etc/shadow is shadow_t, and so on. Directories outside any pattern (like /web) get the generic default_t, which confined services may not touch.
Two ways to change a label, and only one lasts:
| Command | Effect | Survives restorecon / relabel? |
|---|---|---|
chcon -t TYPE PATH | Sets the label on the file now | No |
semanage fcontext -a -t TYPE 'PATTERN' + restorecon -R PATH | Adds a rule, then applies it | Yes |
The pattern is a regular expression. To cover a directory and everything below it, write '/web(/.*)?': the directory itself, or a slash followed by anything. Quote it so the shell does not touch the parentheses.
restorecon sets files to what the rules say. Useful switches: -R recursive, -v print each change, -n dry run. Run it on anything you moved, extracted from an archive, or restored from backup, because those carry old labels. A reboot with /.autorelabel present relabels the entire disk, which is the heavy hammer.
semanage fcontext -a -e /var/www /web is a shortcut worth knowing: it declares /web equivalent to /var/www, so everything under it gets the same labels as the standard web root, including different types for CGI or writable directories.
Edit, apply, verify
semanage fcontext -a -t httpd_sys_content_t '/web(/.*)?' # write the rule
restorecon -Rv /web # apply it
ls -Zd /web ; ls -Z /web # check
semanage fcontext -C -l # list local customisations
semanage fcontext -d deletes a rule, -m modifies one.
Ports
Network ports are labelled too. semanage port -l lists which port numbers each service type may use: http_port_t covers 80, 81, 443, 488, 8008, 8009, 8443, 9000. If a confined daemon tries to bind to a port outside its type, SELinux refuses, and the service fails to start with Permission denied. The fix is to add the port to the type:
semanage port -a -t http_port_t -p tcp 8888
Use -a to add a port that has no label yet, and -m to move one that is already assigned to another type. -d removes your addition. These are stored permanently. The firewall is separate: an open port that SELinux forbids, or a permitted port that the firewall blocks, both fail but with different symptoms (a service that won’t start, versus a connection that times out).
Try it
sudo mkdir -p /web && echo 'custom root' | sudo tee /web/index.html
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/
ls -Zd /web /web/index.html
custom root
403
unconfined_u:object_r:default_t:s0 /web
unconfined_u:object_r:default_t:s0 /web/index.html
default_t: httpd is not allowed there. The audit log confirms it:
sudo ausearch -m avc -ts recent | tail -1
type=AVC msg=audit(1762170312.114:312): avc: denied { read } for pid=3401 comm="httpd" name="index.html" dev="vda3" ino=134 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
Read it as a sentence: the process type httpd_t was denied read on a file of type default_t.
The fix that lasts
sudo semanage fcontext -a -t httpd_sys_content_t '/web(/.*)?'
sudo restorecon -Rv /web
curl -s http://localhost/
Relabeled /web from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Relabeled /web/index.html from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
custom root
Why chcon is a trap
sudo chcon -t default_t /web/index.html
sudo restorecon -Rv /web
Relabeled /web/index.html from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
restorecon returned the label to what the rule says. Had you fixed the original problem with chcon -t httpd_sys_content_t /web, the next relabel (or the exam’s automatic check that runs restorecon) would revert it and break the site.
Moved versus copied
echo m | sudo tee /tmp/moved.html /tmp/copied.html >/dev/null
sudo mv /tmp/moved.html /web/
sudo cp /tmp/copied.html /web/
ls -Z /web
sudo restorecon -nv /web
unconfined_u:object_r:httpd_sys_content_t:s0 copied.html
unconfined_u:object_r:httpd_sys_content_t:s0 index.html
unconfined_u:object_r:user_tmp_t:s0 moved.html
Would relabel /web/moved.html from unconfined_u:object_r:user_tmp_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
A port the policy does not know
echo 'Listen 8888' | sudo tee /etc/httpd/conf.d/port8888.conf
sudo systemctl restart httpd
sudo journalctl -u httpd -n 2 --no-pager
Nov 03 15:12:40 node1 httpd[4410]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:8888
Nov 03 15:12:40 node1 systemd[1]: httpd.service: Main process exited, code=exited, status=1/FAILURE
Permission denied for a root-started daemon is the SELinux signature. Fix and verify:
sudo semanage port -a -t http_port_t -p tcp 8888
sudo systemctl restart httpd
sudo firewall-cmd --permanent --add-port=8888/tcp
sudo firewall-cmd --reload
ss -tlnp | grep 8888
LISTEN 0 511 *:8888 *:* users:(("httpd",pid=4602,fd=4))
Going further
semanage fcontext -l -C and semanage port -l -C show only your own additions, which is exactly what to read when auditing a system someone else configured. semanage export > rules.txt and semanage import move custom rules between hosts. For a writable area for the web server use httpd_sys_rw_content_t; for scripts, httpd_sys_script_exec_t. matchpathcon -V PATH verifies an actual label against the policy. restorecon -Rv / is not a quick fix: relabel specific trees, or schedule a full relabel with touch /.autorelabel and a reboot.
Practice
0 of 5 done
Do these on your own system. They get harder. Check each result, then tick it off.
- Warm-up
Which labels does the policy know for the web server?
Install the SELinux management tools. List the file-context rules for
/var/www, and the ports labelledhttp_port_t.Check your result
semanage fcontext -l | grep '^/var/www'shows rules withhttpd_sys_content_t.semanage port -l | grep ^http_port_tshowstcp 80, 81, 443, 488, 8008, 8009, 8443, 9000.Hint
sudo dnf install -y policycoreutils-python-utilsprovidessemanage.Solution
sudo dnf install -y policycoreutils-python-utils semanage fcontext -l | grep '^/var/www' | head sudo semanage port -l | grep '^http_port_t' - Practice
A new document root is forbidden
Create
/web/index.htmlcontainingcustom root. Configure Apache to serve/web(DocumentRoot "/web"and a<Directory>block). Starthttpdand request the page. It returns403 Forbiddenalthough the file is world-readable. Find the cause in the context and in the audit log.Check your result
curl -s -o /dev/null -w '%{http_code}' http://localhost/prints403.ls -Zd /webshowsdefault_t.ausearch -m avc -ts recentshowsdenied { read } ... scontext=system_u:system_r:httpd_t ... tcontext=...:default_t.Hint
A top-level directory made by root gets
default_t.httpd_tmay not read that type.Solution
sudo mkdir -p /web && echo 'custom root' | sudo tee /web/index.html sudo tee /etc/httpd/conf.d/web.conf <<'EOT' DocumentRoot "/web" <Directory "/web"> Require all granted </Directory> EOT sudo dnf install -y httpd && sudo systemctl enable --now httpd curl -s -o /dev/null -w '%{http_code}\n' http://localhost/ ls -Zd /web /web/index.html sudo ausearch -m avc -ts recent | tail -3 - Practice
Fix it so it survives a relabel
Give
/weband everything under it the typehttpd_sys_content_tpermanently, apply it, and verify the page now loads. Then prove it is persistent by runningrestorecon -Ron the tree after deliberately changing a label withchcon.Check your result
curl http://localhost/returnscustom root.semanage fcontext -C -llists the/web(/.*)?rule. Afterchcon -t default_t /web/index.htmlandrestorecon -Rv /webthe label returns tohttpd_sys_content_t.Hint
semanage fcontext -a -t httpd_sys_content_t '/web(/.*)?'records the rule;restorecon -Rv /webapplies it.Solution
sudo semanage fcontext -a -t httpd_sys_content_t '/web(/.*)?' sudo restorecon -Rv /web curl -s http://localhost/ semanage fcontext -C -l sudo chcon -t default_t /web/index.html ls -Z /web/index.html sudo restorecon -Rv /web ls -Z /web/index.html - Practice
Moved versus copied
Create
/tmp/moved.htmland/tmp/copied.html. Move one into/weband copy the other. Compare their labels, explain the difference, and fix the moved one. Show how to askrestoreconwhat it would change without changing anything.Check your result
The moved file keeps
user_tmp_t; the copy getshttpd_sys_content_t.restorecon -nv /weblists the moved file withwould relabel.restorecon -Rv /webfixes it.Hint
mvpreserves the inode and the label;cpcreates a new file that inherits the directory's label.-nis a dry run,-vis verbose.Solution
echo m | sudo tee /tmp/moved.html /tmp/copied.html sudo mv /tmp/moved.html /web/ sudo cp /tmp/copied.html /web/ ls -Z /web sudo restorecon -nv /web/moved.html sudo restorecon -Rv /web - Challenge
Apache on port 8888
Make Apache listen on TCP 8888 (
Listen 8888in a drop-in), restart it and observe the failure. Find the SELinux cause, add the port to the right type permanently, restart, allow the port in the firewall permanently, and reach it fromnode2.Check your result
Without the label
systemctl restart httpdfails withPermission denied: AH00072: make_sock: could not bind to address [::]:8888and an AVCname_bind. Aftersemanage port -a -t http_port_t -p tcp 8888,ss -tlnp | grep 8888shows httpd andcurl http://node1:8888/works from node2.Hint
semanage port -a -t http_port_t -p tcp 8888. If the port is already assigned to another type, use-m(modify) instead of-a.Solution
echo 'Listen 8888' | sudo tee /etc/httpd/conf.d/port8888.conf sudo systemctl restart httpd; sudo journalctl -u httpd -n 3 --no-pager sudo ausearch -m avc -ts recent | tail -2 sudo semanage port -a -t http_port_t -p tcp 8888 sudo systemctl restart httpd sudo firewall-cmd --permanent --add-port=8888/tcp && sudo firewall-cmd --reload ss -tlnp | grep 8888 # from node2: curl -s http://node1:8888/