Quick start to write a custom SELinux policy

Updated -

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 files
    • init_daemon_domain(<domain>, <file_type>) - promotes the type given as the first argument to a domain, while is used as an entry-point executable by the init daemon
  • patterns:
    • rw_files_pattern(<domain>, <dir_type>, <file_type>) - gives read 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 executes a binary labeled , the resulting process runs 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 withfiles_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


  1. found in obj_perm_sets.spt on the selinux-policy Github repository ↩︎

Comments