SDB:SELinux
SELinux is flexible Mandatory Access Control (MAC) for Linux. This differs from the standard Discrestionary Access Control (DAC) system of unix octal permissions and acls as the owner of a file in a DAC system is also free to change it's DAC permission. In a MAC system, even owning a file is not sufficient for you to remove or alter it's MAC labels.
Introduction
SELinux rules are based on type enforcement. This takes the label of the current process, the label of the target, and the label of the type of action, to determine if this should be allowed to proceed.
For example, if a process with the label `unconfined_t` attempted to perform the action of `write` to a file with a label of `user_home_t` this is allowed because this action is permitted in the set of rules for that triplet.
However, if a process with a label of `chronyd_t` attempted to `write` to a file with `user_home_t` this would be denied as the chronyd process should never need to write to a user home directory.
This is how a system like SELinux provides security boundaries to processes to limit what they can do.
An important concept in SELinux is a transition - the act of changing a running processes label from a source to a destination.
For example when you launch a process in your shell since the shell runs as unconfined_t then no transition occurs and the new process inherits this type. If something like a webserver were compromised by an attacker, then any new processes from the webserver context httpd_t would also remain in httpd_t. This is how SELinux confines what a compromised process can achieve by preventing the process from changing it's own context.
However, there are cases where we don't want to remain in the context of the source process. We need to change our context. This requires a transition. Given a running process with source type S, and a binary with type B, if a transition rule exists for S + B then the process will be created in a target context T.
For example, systemd runs as init_t and when it starts the webserver service from it's binary which is labeled as httpd_exec_t, then a transition rule exists between init_t + httpd_exec_t and the resulting process runs in the target context of httpd_t. These transitions are critical to how an SELinux enforcing system operates.
SELinux setup
Tumbleweed
- Install the package selinux-policy-targeted. That will also pull in a number of tools
- Modify the kernel boot parameters. In /etc/default/grub add security=selinux selinux=1 to the value of GRUB_CMDLINE_LINUX_DEFAULT and run update-bootloader.
- In /etc/selinux/config make sure SELINUXTYPE is set to targeted
- In /etc/selinux/config make sure SELINUX is set to permissive
- run touch /etc/selinux/.autorelabel
- reboot the system
- check for potential error messages to avoid getting locked out. ausearch -ts boot | grep -e DEN to check for obvious denials.
- once completed, to enforce the policy set SELINUX=enforcing in /etc/selinux/config
- reboot once more
On first boot the system will label all files in the file system. So the first boot after enabling SELinux will take a while.
After that verify SELinux is on
# sestatus SELinux status: enabled SELinuxfs mount: /sys/fs/selinux SELinux root directory: /etc/selinux Loaded policy name: targeted Current mode: permissive Mode from config file: permissive Policy MLS status: enabled Policy deny_unknown status: allowed Memory protection checking: requested (insecure) Max kernel policy version: 33
MicroOS
Same as with Tumbleweed, alternatively use transactional-update setup-selinux to perform all steps automatically and switch to enforcing mode.
SELinux Tools
Many standard tools on openSUSE support selinux labels and displaying them. The convention is that an extra argument of `-Z` will show you selinux label/type information.
Common examples are:
# ls -alZ total 20 drwx------. 1 admin admin system_u:object_r:user_home_dir_t:s0 128 Jun 22 18:32 . dr-xr-xr-x. 1 root root system_u:object_r:home_root_t:s0 10 Jun 22 18:32 .. -rw-------. 1 admin admin system_u:object_r:user_home_t:s0 8 Jun 22 19:16 .bash_history -rw-r--r--. 1 admin admin system_u:object_r:user_home_t:s0 604 Jun 15 04:50 .bashrc
# ps auxZ LABEL USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 root 1232 200 0.1 8360 3584 pts/0 R+ 16:43 0:00 ps auxZ
In addition SELinux has a number of dedicated tools to view denials (when a type is denied access to another type), create policy and to enable or disable policy selectively.
You can ensure these tools are installed with `zypper in policycoreutils setools-console`
ausearch
ausearch is the audit search tool. This allows you to query audit logs to display denials that match certain conditions. This may hint to actions you can take to resolve the issue.
To show all denials since boot
ausearch -ts boot -m avc
To show denials from a timestamp. If the current time was 16:20:00 then we could show the list 5 minutes with:
ausearch -ts 16:15:00 -m avc
semanage
`semanage` allows changing some elements of the current policy on a running system. Some common uses are:
List all available policy boolean tunables. Turning some of these "on" may resolve some issues you have without needing to resort to disabling selinux
semanage boolean -l
List all types / processes that have been put into permissive mode. Rather than disable policy for the whole system, individual types/processes can be made permissive temporarily for analysis.
semanage permissive -l # put a single type into permissive mode semanage permissive -a type semanage permissive -a httpd_t # remove that type from permissive mode to enforcing semanage permissive -d type semanage permissive -d httpd_t
semodule
semodule allows dynamic changes and loading of selinux policy modules. This also has a useful feature to allow auditing of some types that otherwise are invisible. By default a number of rules that "may be hit" commonly are marked as dontaudit to prevent them cluttering logs. However, sometimes these don't audit rules are important to understand an issue.
To disable dont audit rules and have them emit messages to the audit log.
semodule -DB
To re-enable don't audit rules so that they enforce rules silently
semodule -B
Troubleshooting
Moving Files
SELinux types are applied to a file when it is created. When a file is moved, it's type remains the same. A common example of this is if you copy a file to a server with scp to your home directory it will be labeled `user_home_t`. If you wanted to then move that file to `/var/www/html` to be served by a webserver, the file would *not* change it's type to `httpd_file_t`.
This is a feature, to prevent accidentally disclosing information that shouldn't be disclosed!
You can see these types with `ls -lZ`.
To avoid this you can use "mv -Z src dst" to have the files type updated during the move. Alternately after you do the move you can use `restorecon -v dst` to reset the types.
Relabeling your system
Sometimes if things are inconsistent this can lead to a lot of denials in selinux. You should relabel your filesystem in these cases.
You can trigger this with `systemctl start selinux-autorelabel`. This WILL reboot your system.
Investigating Denials
If you can't fix it yourself please open a bug
List SELinux related audit events since boot
# ausearch -ts boot -m avc
Intead of "boot" other useful options are "today" or "recent".
Analyze service failure
# ausearch -ts recent -m avc -c sshd ---- time->Tue May 18 14:47:47 2021 type=AVC msg=audit(1621342067.432:82): avc: denied { read } for pid=839 comm="sshd" name="example.com.3" dev="vda2" ino=199155 scontext=system_u:system_r:sshd_t:s0-s0:c0.c1023 tcontext=system_u:object_r:var_yp_t:s0 tclass=file permissive=1 ---- time->Tue May 18 14:47:47 2021 type=AVC msg=audit(1621342067.432:83): avc: denied { open } for pid=839 comm="sshd" path="/var/yp/binding/example.com.3" dev="vda2" ino=199155 scontext=system_u:system_r:sshd_t:s0-s0:c0.c1023 tcontext=system_u:object_r:var_yp_t:s0 tclass=file permissive=1 [...]
The output of that can be piped through audit2why (from policycoreutils-python-utils package) which potentially gives some explanation. In this case the tool recommends turning on a boolean:
Was caused by: The boolean nis_enabled was set incorrectly. Description: Allow nis to enabled Allow access by executing: # setsebool -P nis_enabled 1
Following the instruction to use setsebool would resolve the issue in this case. To get the description of a boolean run semanage (from policycoreutils-python-utils package) and to see the changes a boolean does run sesearch (from setools-console):
semanage boolean -l | grep nis_enabled
sesearch -A -b nis_enabled
Another example:
type=AVC msg=audit(1621342040.556:15): avc: denied { watch } for pid=1 comm="systemd" path="/var/cache/cups" dev="vda2" ino=22stem_r:init_t:s0 tcontext=system_u:object_r:cupsd_rw_etc_t:s0 tclass=dir permissive=1
That one recommends to use audit2allow (from policycoreutils-python-utils package) to produce a new rule.
Using audit2allow
The audit2allow tool uses audit messages to produce rules that can be loaded into SELinux after conversion into the correct format.
#============= init_t ============== allow init_t cupsd_rw_etc_t:dir watch;
Writing Policy
SELinux policy can be written by anyone - even you! There are many tools to assist with this process.
To ensure you are ready to write selinux policy for your application, install `policycoreutils-devel`.
Setup
Let's assume we have a daemon we want to write policy for called "mydaemon".
First change to your work directory where you are happy to create files, and create template policy files.
cd ~/policy-devel sepolicy generate --init /path/to/binary/mydaemon
This will create multiple files for you. The suffixes are
- fc - filecontexts, which lists the types that should be applied to named file paths
- if - interfaces, exportable selinux policy interfaces for other modules to use.
- te - type enforcement, rules that define rules of type interactions.
Examining the fc file that was generated you should see context like:
/usr/sbin/mydaemon -- gen_context(system_u:object_r:mydaemon_exec_t,s0)
By convention, the file type for a daemon named X is to name it X_exec_t.
The other important file here is the te file. It's contents will appear like
policy_module(mydaemon, 1.0.0) ######################################## # # Declarations # type mydaemon_t; type mydaemon_exec_t; init_daemon_domain(mydaemon_t, mydaemon_exec_t) permissive mydaemon_t; ######################################## # # mydaemon local policy # allow mydaemon_t self:fifo_file rw_fifo_file_perms; allow mydaemon_t self:unix_stream_socket create_stream_socket_perms; domain_use_interactive_fds(mydaemon_t) files_read_etc_files(mydaemon_t) miscfiles_read_localization(mydaemon_t)
This defines:
- Type names for new labels.
- A rule to allow the init daemon to transition a new process from type mydaemon_exec_t to mydaemon_t
- permissive rule for mydaemon_t to allow the daemon to work as you develop the policy
- a number of default rules to broadly allow your process to continue to operate.
You can rebuild and install this policy with:
make -f /usr/share/selinux/devel/Makefile mydaemon.pp /usr/sbin/semodule -i mydaemon.pp
This builds and loads the policy module. Now you can relabel your files that were change from your filecontexts with:
restorecon -v /path/to/binary/mydaemon
If you wish to reset files in a directory
restorecon -v -r /path/to/dir
It's important you relabel files to correctly enable the interactions for the next phase! If in doubt run:
restorecon -v -r /
At this point you can now restart your service, and you will be able to see it running in it's new selinux type.
systemctl restart mydaemon # ps auxZ | grep mydaemon ...
If the process is not running as mydaemon_t, then you can investigate this with:
# Can init_t transition to mydaemon_t? sepolicy transition -s init_t -t mydaemon_t # Is there a denial during the transition? ausearch -ts recent -m avc -u system_u:system_r:init_t:s0
Investigating Denials
If the process is running as mydaemon_t, or is not, we need to start to investigate and add rules to authorise our daemon to perform the limited actions it requires. Look at recent denials with `ausearch -ts recent -m avc`. An example is:
type=AVC msg=audit(1687768006.183:92): avc: denied { nnp_transition } for pid=1206 comm="(dm_unixd)" scontext=system_u:system_r:init_t:s0 tcontext=system_u:system_r:kanidm_unixd_t:s0 tclass=process2 permissive=0
This shows a failure for init_t to transition to kanidm_unix_t due to a denial from nnp_transition. This resulted in kanidm_unixd running as init_t instead of kanidm_unix_t.
There are two approaches to resolve this.
First we can attempt to auto-generate rules with audit2allow.
# ausearch -ts recent -m avc | audit2allow allow init_t kanidm_unixd_t:process2 nnp_transition;
While this works, sometimes audit2allow can "allow too much" and the rules it generates are quite coarse. However, it will create a working policy. You can add the rules you are happy with to your .te file and rebuild and install the policy.
The preferred way to write policy however is to consume interfaces from existing policy. These interfaces exist in `/usr/share/selinux/devel` and can be searched with grep or similar. So we can use our keyword `nnp_transition` to locate related policy.
# grep -i -r -n -e 'nnp_transition' /usr/share/selinux/devel /usr/share/selinux/devel/include/contrib/kpatch.if:38: allow $1 kpatch_t:process2 { nnp_transition nosuid_transition }; /usr/share/selinux/devel/include/contrib/mozilla.if:289: allow $1 mozilla_plugin_t:process2 nnp_transition; /usr/share/selinux/devel/include/system/init.if:128: allow init_t $1:process2 { nnp_transition nosuid_transition }; /usr/share/selinux/devel/include/system/init.if:155: allow init_t $1:process2 { nnp_transition nosuid_transition };
We can see that policy for kpatch and mozilla here both reference nnp_transition, however they appear to be related to other types transitioning into kpatch and mozilla since allow rules are `allow source dest`. However in init.if we see some possible rules that match our keywords and appear to allow init_t to nnp_transition to a variable. Viewing these files we see an interface called `init_daemon` and another called `init_nnp_daemon_domain`
Ultimately you need to review these interfaces and determine which you want to use in your rules. In this example I chose to use `init_nnp_daemon_domain` which updates our policy to:
policy_module(mydaemon, 1.0.0) ######################################## # # Declarations # type mydaemon_t; type mydaemon_exec_t; init_daemon_domain(mydaemon_t, mydaemon_exec_t) init_nnp_daemon_domain(mydaemon_t, mydaemon_exec_t) permissive mydaemon_t; ######################################## # # mydaemon local policy # allow mydaemon_t self:fifo_file rw_fifo_file_perms; allow mydaemon_t self:unix_stream_socket create_stream_socket_perms; domain_use_interactive_fds(mydaemon_t) files_read_etc_files(mydaemon_t) miscfiles_read_localization(mydaemon_t)
Refining The Policy
While audit2allow will work, and the policy above will give us a minimal working confined binary, we want to have stricter rules and types so that our daemon can only access it's resources - and so that other processes are unable to access our daemons resources.
To achieve this we need to label our filesystem resources so that the denials we see are specific to those resources.
First, add the new types we will need to our .te file. There are many "conventions" for how these type names should exist. If in doubt, ls -lZ other files and see what others have done!
type mydaemon_conf_t; files_config_file(mydaemon_conf_t) type mydaemon_var_run_t; files_type(mydaemon_var_run_t) files_pid_filetrans(mydaemon_t, mydaemon_var_run_t, { dir file sock_file }) type mydaemon_var_cache_t; files_type(mydaemon_var_cache_t)
Once these types exist, we need to add file contexts to our fc files. These rules are regexes so be careful!
/etc/mydaemon(/.*)? gen_context(system_u:object_r:mydaemon_conf_t,s0) /var/run/mydaemon(/.*)? gen_context(system_u:object_r:mydaemon_var_run_t,s0) /var/cache/mydaemon(/.*)? gen_context(system_u:object_r:mydaemon_var_cache_t,s0) /usr/sbin/mydaemon -- gen_context(system_u:object_r:mydaemon_exec_t,s0)
HINT notice in the rules that apply to directories we remove the --? This allows the context to change other file types such as directories.
Now, we rebuild and reinstall our policy. We also need to restorecon on the file paths that we updated.
restorecon -r -v /etc/mydaemon /var/run/mydaemon /var/cache/mydaemon
You can now restart your daemon and refine your policy with these new types.
From here you iteratively add rules with audit2allow or manually adding interfaces from searching the policy headers.
You also need to exercise your daemon and it's features in order to find all your areas where denials may occur and how your daemon interacts.
It can be useful to limit your ausearch queries to your daemon:
ausearch -ts timestamp -su system_u:system_r:mydaemon_t:s0
Investigating Non-Denial Behaviours
Sometimes your policy appears correct but behaviours may not be occuring as you expect. For example a file may not be relabeled correctly, or a type not being used in a process transition.
There are sometimes many subtle edge cases in SELinux that aren't always obvious.
It can be a useful reference to look at other existing policy to see how they handle the same situations. This is especially true of files in locations like `/var/run` or `/etc`.
Finalising your policy
Once you are happy with the policy and have no denials for a period of time, you should remove the line:
permissive mydaemon_t
Then rebuild and reinstall your policy. If you encounter denials you can temporarily add permissive status to the domain with
semanage permissive -a mydaemon_t
And you can reset the type to enforcing with
semanage permissive -d mydaemon_t