Quick start to write a custom SELinux policy
Table of Contents
To run your new application confined by SELinux on the system, prepare custom policy files that supplement rules enforced by the distribution SELinux policy.
Generating a policy template
The sepolicy generate
tool provided by the policycoreutils-devel
package helps you to perform the initial step. The tool generates a basic policy module, a Makefile, and a setup script. The script builds and installs the policy module, and relabels paths defined in the .fc file. You can use the command as a regular user, for example:
$ sepolicy generate --init <path_to_my_app_binary>
See the Creating and enforcing an SELinux policy for a custom application section in the RHEL Using SELinux document for more details.
If you prefer a graphical interface and wizards, you can use the selinux-polgengui
tool provided by the policycoreutils-gui
package.
The generated policy template contains the following policy files:
- <mypolicy>.te - defines policy rules as well as new types and domains used by your application
- <mypolicy>.fc - contains file context definitions, in other words, instructions for labeling files related to the application
- <mypolicy>.if - contains interfaces, which are policy macros used by other policy modules to interact with this policy
Allowing access based on application behavior
After you install the initial policy on the system, you can improve it based on the access required by the application.
To include all accessed paths in logs, enable full auditing:
# audictl -d never,task
# auditctl -w /etc/ -p w
Alternatively, open the /etc/audit/rules.d/audit.rules
file`, remove "-a task,never" if present, add "-w /etc/ -p w", and restart the Audit daemon.
To log all violations of the SELinux policy, switch SELinux to permissive mode:
# setenforce 0
The setenforce 0
command sets SELinux to permissive mode on the whole system until the next restart. You can switch only the new domain to permissive mode and keep the rest of the SELinux policy in enforcing mode:
# semanage permissive -a <myapp_t>
Note that the sepolicy generate
command adds the permissive <myapp_t>
rule to the policy module. Therefore, you can skip this step if you preserved the rule.
After you enable full auditing and permissive mode, run the application in its new SELinux domain. To see all access required by the application, test as many use cases as possible. The system logs all access requests that are not allowed in the current policy in the form of AVCs (SELinux access denial logs).
Processing AVCs
When SELinux denies an action, the system adds an Access Vector Cache (AVC) message to the /var/log/audit/audit.log
and /var/log/messages
files or the Journal daemon logs the denial.
You can display recent AVCs by using the ausearch
command, for example:
# ausearch -m AVC,USER_AVC -ts recent
The -ts recent
parameter limits the search to last 10 minutes, but you can use a timestamp instead.
The audit2allow
tool can process AVCs and generate corresponding allow rules for our policy.
Whenever your custom rules interact with resources defined in other policy packages, use interfaces instead simple allow rules if possible. The audit2allow -R
command attempts to find an interface covering the use case instead. You must check each result of this command to ensure that the resulting policy is not too loose.
Policy macros
Policy macros, which are also called patterns or in more complex cases, interfaces, generate multiple rules that work together to allow certain use cases.
Examples:
- interfaces:
mysql_read_config(<domain>)
- gives the specified domain (httpd_t in this example) read access to MySQL config filesinit_daemon_domain(<domain>, <file_type>)
- promotes the type given as the first argument to a domain, whileis used as an entry-point executable by the init daemon
- patterns:
rw_files_pattern(<domain>, <dir_type>, <file_type>)
- givesread and write access to files labeled located in directories labeled domtrans_pattern(<source>, <entrypoint>, <target>)
- sets up an automatic domain transition: when a process running in domain
Interfaces available in Fedora, RHEL, and CentOS are defined in the modules section of the selinux-policy Github repository. Each policy module contains a set of interfaces as means for other modules to gain access to resources defined in the module.
Each macro has a fixed number of arguments you must specify. If you are not sure about the corresponding arguments, check the body of the macro for argument references in the form of the $
character followed by an index (for example, $1
). Alternatively, check the interface index in the /usr/share/doc/selinux-policy/html/interfaces.html
file provided by the selinux-policy-doc
package.
Optional policy
Whenever you use an interface defined in one of the contribution modules, you must enclose it in an optional_policy block.
For example, Apache modules use kerberos_read_keytab interface defined in kerberos module:
optional_policy(`
kerberos_read_keytab(httpd_t)
')
The optional_policy block ensures you can use the policy module without the optional module. In this case, the apache module could be used on a system where the kerberos module was removed. If you omit the optional_policy block, installing your new policy module causes an error when any of the modules whose resources you granted access to are missing.
Expanding macros
The macro-expander
tool provided by the selinux-policy-devel
package displays what each macro contains if the name is not self-explanatory. Use macro-expander
with the full macro name including all arguments to see all allow rules the macro generates, for example:
# macro-expander "mysql_read_config(httpd_t)"
allow httpd_t mysqld_etc_t:dir { getattr search open read lock ioctl };
allow httpd_t mysqld_etc_t:file { open { getattr read ioctl lock } };
allow httpd_t mysqld_etc_t:lnk_file { getattr read };
Note that macro-expander
lists only allow rules by default. To see the full content of a macro, such as type_transition rules or attribute assignments, use macro-expander -M <>
and ignore the module header and all require blocks:
$ macro-expander -M "domtrans_pattern(<source>, <entrypoint>, <target>)"
module expander 1.0.0;
require {
role system_r;
...
}
allow <source> <entrypoint>:file { getattr open map read execute ioctl execute_no_trans };
allow <source> <target>:process transition;
type_transition <source> <entrypoint>:process <target>;
allow <target> <source>:fd use;
allow <target> <source>:fifo_file { getattr read write append ioctl lock };
allow <target> <source>:process sigchld;
Permission sets
Unlike other policy macros, permission sets do not use any arguments and only serve as a shorthand for a set of permissions that are commonly used together.
Examples:
- rw_file_perms - { open getattr read write append ioctl lock }
- create_dir_perms - { getattr create }
- append_fifo_file_perms - { getattr open append lock ioctl }
All permission sets are defined in the obj_perm_sets.spt file. Because macro-expander
by default displays only allow rules, you must adjust the command as follows to expand standalone permission sets:
$ macro-expander -M "manage_blk_file_perms" | tail -n 1
{ create open getattr setattr read write append rename link unlink ioctl lock }
In this case, macro-expander
with the -M
option generates a complete module containing the query, and you can ignore the module header and the require
block.
Alternatively, supply a "dummy" allow rule, for example:
$ macro-expander "allow _ _:_ manage_blk_file_perms"
allow _ _:_ { create open getattr setattr read write append rename link unlink ioctl lock }
Missing interfaces
If you want to allow a valid access requirement for which no interface exists, work around the problem by using the gen_require
statement.
In the following example, the dhcpd policy module requires additional access based on the AVC listed in the output of the audit2allow -R
command:
$ audit2allow -R
type=AVC msg=audit(1674838508.590:840): avc: denied { write } for pid=2383 comm="dhcpd" name="bluetooth.conf" dev="vda2" ino=2 scontext=system_u:system_r:dhcpd_t:s0 tcontext=system_u:object_r:bluetooth_conf_t:s0 tclass=file permissive=1
require {
type bluetooth_conf_t;
type dhcpd_t;
class file write;
}
#============= dhcpd_t ==============
allow dhcpd_t bluetooth_conf_t:file write;
No interface for writing into bluetooth_conf_t files exists, but a quick search in the bluetooth.if file on the selinux-policy Github repository reveals bluetooth_read_config, which you can modify for this use case:
interface(`bluetooth_read_config',`
gen_require(`
type bluetooth_conf_t;
')
allow $1 bluetooth_conf_t:file read_file_perms;
')
You fix the missing access by replacing read_file_perms
with write_file_perms
1 and filling in the interface argument ($1
-> dhcpd_t
).
You must also surround the added policy with the optional_policy
block - just as if you use an actual interface defined in the bluetooth module.
optional_policy(`
gen_require(`
type bluetooth_conf_t;
')
allow dhcpd_t bluetooth_conf_t:file write_file_perms;
')
Process context (domain)
Types assigned to running processes are often referred to as domains. By default, a new process inherits the context of its parent. For example, a cat
command entered by a user in the Bash shell running in the unconfined_t
domain results in a new process running also as unconfined_t
.
You can change this behavior by using a type_transition rule, which is a part of domtrans_pattern
(domain transition pattern):
domtrans_pattern(<source>, <entrypoint>, <target>)
This pattern defines an automatic transition into the <target>
domain using the type_transition rule and adds a set of rules allowing the transition. It is commonly used as a part of the init_daemon_domain(<target>, <entrypoint>)
interface to facilitate the transition between init_t
(the init daemon domain) and the newly created domain.
File context
New files inherit the type assigned to the directory where you created them. To assign a custom type without changing the application code, use the filetrans_pattern
macro:
filetrans_pattern(<process_domain>, <directory>, <target_type>, <class(es)>, [<name>])
Where:
- <class(es)> - object classes the rule applies to (file, directory, socket, and so on)
- [
] - restricts the rule to objects of the given name (optional)
For example:
filetrans_pattern(httpd_t, var_t, httpd_cache_t, file, "apache_cache")
When a process running as httpd_t
creates a file named apache_cache
in a directory labeled var_t
, the system sets the httpd_cache_t
label on the resulting file.
You can use filetrans_pattern
inside more specialized macros, where the <directory>
argument is pre-filled based on a specific location, for example:
files_var_filetrans(httpd_t, httpd_cache_t, { file dir })
For more details, see the File Name Transition section in the RHEL 7 SELinux User's and Administrator's Guide.
Default file context definitions
All policy modules contain a file context definition file <module>.fc
, which determines the default SELinux context of its resources. This context is used by labeling utilities, such as matchpathcon or restorecon.
Each definition has the following format:
pathname_regexp [file_type] security_context
Where:
- pathname_regexp - defines the pathname that may be in the form of a regular expression
- file_type - represents the object class and is either blank (meaning all object classes) or one of:
- -b - Block Device
- -c - Character Device
- -d - Directory
- -p - Named Pipe (FIFO)
- -l - Symbolic Link
- -s - Socket File
- -- - Ordinary file
- security_context - the default security context assigned to the path
For more details, see the SELinux Contexts – Labeling Files section in the RHEL 7 SELinux User's and Administrator's Guide.
Troubleshooting CIL errors
When you load the compiled policy module onto the system, the compiler translates the module code into the Common Intermediate Language (CIL). This means that most errors encountered while loading the module are reported against the CIL version of your policy as demonstrated in the following example of a simple policy with a type definition that already exists in the system policy:
$ sepolicy generate --init /usr/bin/cat
$ echo "type abrt_t;" >> cat.te
$ sudo make -f /usr/share/selinux/devel/Makefile cat.pp
Compiling targeted cat module
Creating targeted cat.pp policy package
rm tmp/cat.mod.fc tmp/cat.mod
$ sudo semodule -i cat.pp
Re-declaration of type abrt_t
Previous declaration of type at /var/lib/selinux/targeted/tmp/modules/400/cat/cil:6
Bad type declaration at /var/lib/selinux/targeted/tmp/modules/400/cat/cil:6
Failed to build AST
semodule: Failed!
Because some CIL-related error messages are not clear, you might need to check the corresponding line in the CIL translation of the module source code. In this particular case, the error is related to line 6. Use the pp
tool to translate the policy binary to CIL, and display the problematic line:
$ cat cat.pp | /usr/libexec/selinux/hll/pp > cat.cil
$ head -6 cat.cil | tail -1
(type abrt_t)
For more information about CIL and how its keywords differ from the source policy language, see the CIL Policy Language section on the SELinux Project wiki.
Example policy
By performing the following example steps, you create and test a new SELinux policy for the bootupd
package. The package contains a service that is unconfined now.
Display all files installed by the bootupd
package:
$ rpm -ql bootupd
/usr/bin/bootupctl
/usr/lib/.build-id
/usr/lib/.build-id/e8
/usr/lib/.build-id/e8/3c00b4a3c33f2858d6730e9ba95d8286ab6dde
/usr/lib/.build-id/e8/3c00b4a3c33f2858d6730e9ba95d8286ab6dde.1
/usr/lib/systemd/system/bootupd.service
/usr/lib/systemd/system/bootupd.socket
/usr/libexec/bootupd
/usr/share/doc/bootupd
/usr/share/doc/bootupd/README.md
/usr/share/licenses/bootupd
/usr/share/licenses/bootupd/LICENSE
Among the listed files, you can see two unit files as well as two binaries, bootupd
and bootupctl
(remote command-line interface). You can ignore the rest of the files for the preparation of the new SELinux policy.
The bootupd.socket
file indicates that the service uses the /var/run/bootupd.sock
socket. Because the unit files of the bootupd
service use the init system, use the --init
parameter with the sepolicy generate
command. Based on the content of the bootupd.service
file, you know that /usr/libexec/bootupd
is the service binary. You use the rest of the files (the unit and socket files) as values for the --writepath
parameter so that the sepolicy generate
command includes them in the policy:
$ sepolicy generate --init /usr/libexec/bootupd -w /usr/lib/systemd/system/bootupd.service /usr/lib/systemd/system/bootupd.socket /var/run/bootupd.sock
Created the following files:
/home/user/bootupd/bootupd.te # Type Enforcement file
/home/user/bootupd/bootupd.if # Interface file
/home/user/bootupd/bootupd.fc # File Contexts file
/home/user/bootupd/bootupd_selinux.spec # Spec file
/home/user/bootupd/bootupd.sh # Setup Script
The bootupd.te
type enforcement file contains four new types:
* bootupd_t
- domain for the bootupd
process
* bootupd_exec_t
- file type for the bootupd
binary
* bootupd_var_run_t
- type for the /var/run/bootupd.sock
socket
* bootupd_unit_file_t
- file type for the two unit files
The init_daemon_domain(bootupd_t, bootupd_exec_t)
macro ensures that when the init daemon executes the bootupd
binary, the resulting process runs in the bootupd_t
domain.
The rest of the generated module is mostly an init daemon policy template, except for the group of macros granting access to bootupd_var_run_t
:
* manage_dirs_pattern(bootupd_t, bootupd_var_run_t, bootupd_var_run_t)
- manage directories labeled bootupd_var_run_t
* manage_files_pattern(bootupd_t, bootupd_var_run_t, bootupd_var_run_t)
- manage files labeled bootupd_var_run_t
* manage_lnk_files_pattern(bootupd_t, bootupd_var_run_t, bootupd_var_run_t)
- manage link files labeled bootupd_var_run_t
* files_pid_filetrans(bootupd_t, bootupd_var_run_t, { dir file lnk_file })
- if bootupd_t
creates a file in a directory labeled var_run_t
, the resulting file is labeled bootupd_var_run_t
The /var/run/bootupd.sock
path does not exist, and therefore sepolicy generate
generates access macros for directories, files, and link files. Because /var/run/bootupd.sock
is a socket file, you can remove all three manage_*
patterns. The file_patterns.spt file on the selinux-policy Github repository contains several socket patterns you can use instead, such as rw_sock_files_pattern
.
Because the socket file is generated by systemd
(not by bootupd
) as a result of using systemctl start bootupd.socket
, the files_pid_filetrans
macro is also redundant. In a case your application binary generates the socket file, you must replace { dir file lnk_file }by
{ sock_file }instead of removing the line with
files_pid_filetrans`.
The bootupd.fc
file-context configuration file also contains all the paths provided to the sepolicy generate
command:
/usr/lib/systemd/system/bootupd.service -- gen_context(system_u:object_r:bootupd_unit_file_t,s0)
/usr/lib/systemd/system/bootupd.socket -- gen_context(system_u:object_r:bootupd_unit_file_t,s0)
/usr/libexec/bootupd -- gen_context(system_u:object_r:bootupd_exec_t,s0)
/var/run/bootupd.sock -- gen_context(system_u:object_r:bootupd_var_run_t,s0)
Again, sepolicy generate
did not recognize the socket file correctly. Therefore, you must change the object class from --
(normal file) to -s
(socket file):
/var/run/bootupd.sock -s gen_context(system_u:object_r:bootupd_var_run_t,s0)
See the Default file context definitions section for the complete list of object classes.
After this step, the new policy matches the package. Compile and install it:
$ make -f /usr/share/selinux/devel/Makefile bootupd.pp
Compiling targeted bootupd module
Creating targeted bootupd.pp policy package
rm tmp/bootupd.mod.fc tmp/bootupd.mod
$ sudo semodule -i bootupd.pp
Use the restorecon
command on each path affected by the new file context definitions to apply the settings on the system:
$ sudo restorecon -v /usr/libexec/bootupd /usr/lib/systemd/system/bootupd.socket /usr/lib/systemd/system/bootupd.service
Relabeled /usr/libexec/bootupd from system_u:object_r:bin_t:s0 to system_u:object_r:bootupd_exec_t:s0
Relabeled /usr/lib/systemd/system/bootupd.socket from system_u:object_r:systemd_unit_file_t:s0 to system_u:object_r:bootupd_unit_file_t:s0
Relabeled /usr/lib/systemd/system/bootupd.service from system_u:object_r:systemd_unit_file_t:s0 to system_u:object_r:bootupd_unit_file_t:s0
After you enable full auditing, run the service:
$ sudo systemctl start bootupd.socket
$ sudo systemctl start bootupd
Check the file context and the domain of the service process. Search the log for AVCs:
$ ls -lZ /var/run/bootupd.sock
srw-------. 1 root root system_u:object_r:bootupd_var_run_t:s0 0 Mar 14 10:45 /var/run/bootupd.sock
$ ps -axZ | grep bootup[d]
system_u:system_r:bootupd_t:s0 2508 ? Ss 0:00 /usr/libexec/bootupd daemon -v
$ sudo ausearch -m AVC | tee avc.log
----
time->Tue Mar 14 10:24:55 2023
type=PROCTITLE msg=audit(1678803895.889:613): proctitle=2F7573722F6C6962657865632F626F6F74757064006461656D6F6E002D76
type=SYSCALL msg=audit(1678803895.889:613): arch=c000003e syscall=41 success=yes exit=4 a0=1 a1=80002 a2=0 a3=8080808080808080 items=0 ppid=1 pid=1995 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bootupd" exe="/usr/libexec/bootupd" subj=system_u:system_r:bootupd_t:s0 key=(null)
type=AVC msg=audit(1678803895.889:613): avc: denied { create } for pid=1995 comm="bootupd" scontext=system_u:system_r:bootupd_t:s0 tcontext=system_u:system_r:bootupd_t:s0 tclass=unix_dgram_socket permissive=1
----
time->Tue Mar 14 10:24:55 2023
type=PROCTITLE msg=audit(1678803895.889:614): proctitle=2F7573722F6C6962657865632F626F6F74757064006461656D6F6E002D76
type=SYSCALL msg=audit(1678803895.889:614): arch=c000003e syscall=44 success=yes exit=8 a0=4 a1=557718261310 a2=8 a3=4000 items=0 ppid=1 pid=1995 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bootupd" exe="/usr/libexec/bootupd" subj=system_u:system_r:bootupd_t:s0 key=(null)
type=AVC msg=audit(1678803895.889:614): avc: denied { sendto } for pid=1995 comm="bootupd" path="/run/systemd/notify" scontext=system_u:system_r:bootupd_t:s0 tcontext=system_u:system_r:kernel_t:s0 tclass=unix_dgram_socket permissive=1
----
time->Tue Mar 14 10:24:55 2023
type=PROCTITLE msg=audit(1674838508.590:840): proctitle=2F7573722F6C6962657865632F626F6F74757064006461656D6F6E002D76
type=PATH msg=audit(1674838508.590:840): item=0 name="/boot/efi" inode=413 dev=fc:02 mode=040755 ouid=0 ogid=0 rdev=00:00 obj=system_u:object_r:boot_t:s0 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
type=CWD msg=audit(1674838508.590:840): cwd="/usr"
type=SYSCALL msg=audit(1674838508.590:840): arch=c000003e syscall=137 success=yes exit=0 a0=7fff3dc6b6a0 a1=7fff3dc6c7c0 a2=9 a3=fff items=1 ppid=1 pid=2383 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="bootupd" exe="/usr/libexec/bootupd" subj=system_u:system_r:bootupd_t:s0 key=(null)
type=AVC msg=audit(1674838508.590:840): avc: denied { getattr } for pid=2383 comm="bootupd" name="/" dev="vda2" ino=2 scontext=system_u:system_r:bootupd_t:s0 tcontext=system_u:object_r:fs_t:s0 tclass=filesystem permissive=1
Even though the service is running in the new domain and the socket file has the correct label, SELinux still reported three access vectors not allowed in the policy. For detailed explanation of AVC messages, see the SELinux denials in the Audit log section in the RHEL 9 Using SELinux document.
You can use the audit2allow
tool for suggestions of allow rules missing in the new policy:
$ audit2allow -i avc.log
#============= bootupd_t ==============
allow bootupd_t fs_t:filesystem getattr;
allow bootupd_t kernel_t:unix_dgram_socket sendto;
allow bootupd_t self:unix_dgram_socket create;
Because you cannot compile a policy module containing types defined in another policy module (fs_t
and kernel_t
), you must use audit2allow
with the -R
option for interfaces or macros containing the necessary rules:
$ audit2allow -R -i avc.log
require {
type bootupd_t;
class unix_dgram_socket create;
}
#============= bootupd_t ==============
allow bootupd_t self:unix_dgram_socket create;
fs_getattr_xattr_fs(bootupd_t)
kernel_dgram_send(bootupd_t)
After verifying that the suggested interfaces cover exactly the required use case using macro-expander, add them to the policy module. You also must enclose any interface originating from contribution modules in an optional_policy block. Deploy the new policy in permissive mode first, and test as many use scenarios on various system configurations as possible.
Additional resources
- Macro expander
- File policy patterns
- Other common macros
- Reference policy macro descriptions
- Custom policy packaging guide
- Booleans - conditional policy rules
- SETools - policy information
- Writing a custom SELinux policy
- How to create custom SELinux policy module wisely
-
found in obj_perm_sets.spt on the selinux-policy Github repository ↩︎
Comments