SELinux and mandatory access control

Listen to this lesson

Episode 25 · 51:05

This episode is a study companion for CompTIA Linux+ XK0-006 and is not produced by or endorsed by CompTIA.

Objective 3.3 · Security · 18% of the exam

Why this matters

File permissions are discretionary: the owner decides who may read a file, and root may do anything. That has a hard limit — if a web server running as apache is compromised, the attacker can do everything apache may do, which includes reading every file that user can reach.

Mandatory access control adds a second, independent layer. A policy set by the administrator says what each program may do, regardless of who owns what. A compromised web server confined by SELinux cannot read /etc/shadow or connect outbound, even if it is running as root, because the policy does not permit that transition.

SELinux is also the single most common reason a service that "should work" does not — and the reflex to disable it is precisely what this lesson exists to replace.

The lesson

Contexts

Every process and every file carries a security context, four fields:

system_u:object_r:httpd_sys_content_t:s0
   user     role         TYPE         level

The type is what matters almost all the time. The others exist for role-based and multi-level policies you will rarely meet.

ls -Z /var/www/html/index.html      # a file's context
ps -eZ | grep httpd                 # a process's context
id -Z                               # your own
ls -Z /etc/shadow

The policy is a set of rules of the form a process of type X may perform operation Y on an object of type Z. httpd_t may read httpd_sys_content_t. It may not read shadow_t. That single pair of statements is SELinux in miniature.

Nearly every SELinux problem is a file with the wrong type. Move a file rather than copy it, or create one in an unexpected place, and it keeps a context the service is not allowed to read — so the service reports permission denied on a file whose ls -l permissions are perfect.

The three states

getenforce                  # Enforcing, Permissive, or Disabled
sestatus                    # fuller status
setenforce 1                # Enforcing, until reboot
setenforce 0                # Permissive, until reboot

Enforcing — the policy is applied and violations are blocked and logged.

Permissive — violations are logged but allowed. Nothing is blocked.

Disabled — the policy is not loaded at all, and no contexts are maintained.

Permissive is the diagnostic state, and using it properly is the technique worth learning: switch to permissive, reproduce the problem, and look at the log.

  • If it now works, SELinux was the cause and the log tells you exactly which rule was missing.
  • If it still fails, SELinux was never the problem and you have eliminated it in thirty seconds.

Permanent state lives in /etc/selinux/config:

SELINUX=enforcing
SELINUXTYPE=targeted

Going from Disabled back to Enforcing requires a full relabel, because contexts were not maintained while it was off:

touch /.autorelabel && reboot       # relabels the whole filesystem, then reboots

That relabel can take a long time on a large filesystem, which is a strong argument against ever setting SELINUX=disabled in the first place. If you must turn it off, use permissive — the contexts stay correct and you can switch back instantly.

Fixing contexts

restorecon -v /var/www/html/index.html      # reset to the policy default
restorecon -Rv /var/www/html                # recursively
chcon -t httpd_sys_content_t /srv/site/index.html   # set it manually
matchpathcon /srv/site                      # what SHOULD this path's context be?

restorecon and chcon differ in a way that matters. restorecon applies the context the policy says the path should have, and survives a relabel. chcon sets a context directly, and a future restorecon or an autorelabel will silently undo it.

So chcon is for testing a theory. To make it permanent, tell the policy about the path:

semanage fcontext -a -t httpd_sys_content_t "/srv/site(/.*)?"
restorecon -Rv /srv/site

That two-step — semanage fcontext to record the rule, restorecon to apply it — is the correct way to serve content from a non-standard directory, and it is a very common exam scenario.

The cp versus mv distinction explains most of the mystery cases: cp creates a new file, which inherits the destination directory's context. mv preserves the original context. So moving a file into /var/www/html carries its old, wrong type; copying it usually just works. When a file appears identical to its neighbours but is not served, compare ls -Z, not ls -l.

semanage: ports, booleans and more

semanage port -l | grep http                       # which ports httpd_t may bind
semanage port -a -t http_port_t -p tcp 8080        # allow a new one
semanage fcontext -l | grep www
semanage login -l

Running a service on a non-standard port fails under SELinux even with the firewall open, because the policy lists which ports each type may bind. Moving sshd to 2222 needs semanage port -a -t ssh_port_t -p tcp 2222, and forgetting it produces a daemon that will not start with a confusing error.

Booleans

Booleans are pre-built policy switches — supported variations that do not require writing policy.

getsebool -a                              # every boolean
getsebool httpd_can_network_connect
setsebool httpd_can_network_connect on    # until reboot
setsebool -P httpd_can_network_connect on # PERSISTENT
semanage boolean -l | grep httpd

The -P is the thing to remember. Without it the change is lost at the next reboot, and the service that has worked for weeks stops after a restart — one of the more baffling failures if you have forgotten which switch you flipped.

Booleans you will actually meet:

  • httpd_can_network_connect — let the web server make outbound connections. Off by default, and the reason a PHP application cannot reach an API or a database on another host.
  • httpd_can_sendmail
  • samba_enable_home_dirs
  • ftpd_full_access

Check for a boolean before writing policy. Most reasonable requirements already have one.

Diagnosing denials

Denials go to the audit log, tagged AVC — Access Vector Cache.

ausearch -m avc -ts recent
ausearch -m avc -ts today | audit2allow -a          # explain what would be needed
grep AVC /var/log/audit/audit.log | tail
sealert -a /var/log/audit/audit.log                 # human-readable analysis

sealert is the tool to reach for first. It reads the raw denial and produces a plain-English explanation with suggested fixes, usually naming the boolean or the semanage fcontext command you need. It comes from setroubleshoot-server, which is worth installing on any machine where you expect to debug this.

audit2allow generates a policy module permitting exactly what was denied:

ausearch -m avc -ts recent | audit2allow -M mypolicy
semodule -i mypolicy.pp

It is powerful and it is dangerous, because it will happily generate a rule permitting whatever it saw — including an attacker's activity, if that is what produced the denial. Read the generated .te file before installing it. Reaching for audit2allow as the first response to any denial is how a policy becomes meaningless one module at a time.

The correct order of investigation:

  1. sealert or ausearch — what exactly was denied?
  2. Is there a boolean? getsebool -a | grep <service>
  3. Is it a context problem? ls -Z, then semanage fcontext + restorecon
  4. Is it a port? semanage port -a
  5. Only then consider audit2allow, having read the output.

The temptation to disable it

SELINUX=disabled makes the immediate problem go away and removes the entire second layer of defence — the one that contains a compromised service. It is also awkward to reverse, because of the relabel.

Use permissive instead while you diagnose. It logs everything and blocks nothing, so the service works while you gather exactly the information needed to fix it properly. Then switch back.

AppArmor is the equivalent on Debian, Ubuntu and SUSE. It is path-based rather than label-based, which makes profiles easier to read and write but means it does not follow a file that is moved or hard-linked. Same purpose, different trade-off.

aa-status
aa-complain /etc/apparmor.d/usr.sbin.nginx     # permissive, per profile
aa-enforce /etc/apparmor.d/usr.sbin.nginx

On the exam

  • SELinux is mandatory access control, applied on top of — not instead of — ordinary permissions. Both must allow an operation.
  • The three states are Enforcing, Permissive and Disabled. Permissive logs and allows; use it to diagnose.
  • Returning from Disabled needs a relabel — touch /.autorelabel and reboot.
  • restorecon applies the policy's default context; chcon sets one that a relabel will undo. Make it permanent with semanage fcontext.
  • mv preserves the old context, cp inherits the destination's — the usual cause of "the file looks identical but is not served".
  • setsebool -P for a persistent boolean; without -P it is lost at reboot.
  • A service on a non-standard port needs semanage port -a.
  • sealert explains a denial; audit2allow generates a module — read it before installing.
  • AppArmor is the path-based equivalent on Debian and SUSE.

Practise what you just read

1. A file is owned by the web server's user with mode 644, yet Apache cannot read it. SELinux is Enforcing. What is the likely cause?

Select one

  1. The file is missing the execute bit needed to open it
  2. The file carries a security context the httpd process type may not read
  3. SELinux requires files to be mode 664 to be served
  4. Apache is running as a different user than the file's owner
Show answer

B. SELinux applies on top of ordinary permissions, and BOTH must allow the operation. A file whose context is user_home_t rather than httpd_sys_content_t is refused however correct its mode. ausearch -m avc shows the denial with the source and target contexts, and restorecon usually fixes it in one command.

2. A page moved into /var/www/html with mv returns 403, while an identical page copied there with cp works. Why?

Select one

  1. cp creates a new inode, which resets the immutable attribute
  2. mv preserves the original SELinux context; cp inherits the destination's
  3. mv leaves the file owned by the previous directory's group
  4. mv clears the file's permissions while cp preserves them
Show answer

B. Moving a file within a filesystem changes only the directory entry, so the inode keeps whatever context it had -- typically user_home_t from your home directory. Copying creates a new file that inherits the destination's default. restorecon -Rv on the directory resets everything to what the policy expects, which is the standard fix.

3. Setting SELinux to Permissive makes a problem disappear. What does this establish, and what should follow?

Select one

  1. It confirms the cause; fix the label, then re-enable
  2. It proves the permissions are wrong and SELinux is incidental
  3. It resolves the problem, and Permissive should be left in place
  4. It disables SELinux permanently and needs a relabel to undo
Show answer

A. Permissive logs denials without acting on them, which makes it an excellent diagnostic and a poor destination. Leaving a machine there removes the protection while everything appears fine, so nobody notices. Fix the context or the boolean and set Enforcing again. Note that Permissive is not Disabled -- returning from Disabled needs a full relabel.

5 more questions on this objective are part of the full course.

Practise the full question bank in the exam simulator

Hands-on labs

All hands-on labs