2 of 3

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.

  • chcon
  • semanage fcontext
  • restorecon
  • semanage port
  • ls -Z
  • semanage fcontext -l
  • touch /.autorelabel
  • policycoreutils-python-utils

50 min5 exercises

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:

CommandEffectSurvives restorecon / relabel?
chcon -t TYPE PATHSets the label on the file nowNo
semanage fcontext -a -t TYPE 'PATTERN' + restorecon -R PATHAdds a rule, then applies itYes

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.

  1. 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 labelled http_port_t.

    Check your result

    semanage fcontext -l | grep '^/var/www' shows rules with httpd_sys_content_t. semanage port -l | grep ^http_port_t shows tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000.

    Hint

    sudo dnf install -y policycoreutils-python-utils provides semanage.

    Solution
    sudo dnf install -y policycoreutils-python-utils
    semanage fcontext -l | grep '^/var/www' | head
    sudo semanage port -l | grep '^http_port_t'
  2. Practice

    A new document root is forbidden

    Create /web/index.html containing custom root. Configure Apache to serve /web (DocumentRoot "/web" and a <Directory> block). Start httpd and request the page. It returns 403 Forbidden although 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/ prints 403. ls -Zd /web shows default_t. ausearch -m avc -ts recent shows denied { read } ... scontext=system_u:system_r:httpd_t ... tcontext=...:default_t.

    Hint

    A top-level directory made by root gets default_t. httpd_t may 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
  3. Practice

    Fix it so it survives a relabel

    Give /web and everything under it the type httpd_sys_content_t permanently, apply it, and verify the page now loads. Then prove it is persistent by running restorecon -R on the tree after deliberately changing a label with chcon.

    Check your result

    curl http://localhost/ returns custom root. semanage fcontext -C -l lists the /web(/.*)? rule. After chcon -t default_t /web/index.html and restorecon -Rv /web the label returns to httpd_sys_content_t.

    Hint

    semanage fcontext -a -t httpd_sys_content_t '/web(/.*)?' records the rule; restorecon -Rv /web applies 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
  4. Practice

    Moved versus copied

    Create /tmp/moved.html and /tmp/copied.html. Move one into /web and copy the other. Compare their labels, explain the difference, and fix the moved one. Show how to ask restorecon what it would change without changing anything.

    Check your result

    The moved file keeps user_tmp_t; the copy gets httpd_sys_content_t. restorecon -nv /web lists the moved file with would relabel. restorecon -Rv /web fixes it.

    Hint

    mv preserves the inode and the label; cp creates a new file that inherits the directory's label. -n is a dry run, -v is 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
  5. Challenge

    Apache on port 8888

    Make Apache listen on TCP 8888 (Listen 8888 in 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 from node2.

    Check your result

    Without the label systemctl restart httpd fails with Permission denied: AH00072: make_sock: could not bind to address [::]:8888 and an AVC name_bind. After semanage port -a -t http_port_t -p tcp 8888, ss -tlnp | grep 8888 shows httpd and curl 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/