Quick start to write a custom SELinux policy

Updated -

This guide introduces the conceptual foundation, tools, and resources needed for writing new custom SELinux policies and can also be applied to modifying existing ones. It guides you through developing a new custom policy from scratch. Although it is an introduction to writing policies and does not address more advanced aspects, it requires some experience and familiarity with what SELinux is and its operational aspects (for example, modes, loading modules, setting booleans). For an introduction to SELinux refer to the RHEL 9 Using SELinux document.

Overview

The general outline of the article is:

  1. presenting the core concepts of SELinux
  2. policies and their constructs used in policy modules,
  3. walking through the process of creating a new SELinux custom policy module
  4. guidance on deploying a custom module: packaging and distribution.

Prerequisites

  • RHEL 8 or Fedora 37 and later, a development environment
    • All of the commands work both on RHEL and Fedora, except the example policy is generated for bootupd, which is only available in Fedora (but you can adapt the example for a different daemon).
  • root access to the system
  • selinux-policy-devel and setools-console packages installed on your system

This guide does not provide instructions for the graphical interface. However, you can perform many steps in the tools provided by the setools-gui and policycoreutils-gui packages instead of the command line.

Conventions

The following conventions are used in the document:

  • CLI commands prefaced with $ enter as a regular user
  • CLI commands prefaced with # enter as a root

Policy concepts

An SELinux security policy is a collection of SELinux rules. A policy is a core component of SELinux and is loaded into the kernel by SELinux user-space tools. The kernel enforces the use of an SELinux policy to evaluate access requests on the system. By default, SELinux denies all requests except for requests that correspond to the rules specified in the loaded policy.

Each SELinux policy rule describes an interaction between a subject and a target resource.

Policy rules have the general form of:

<rule type> <subject> <object>:<object class> <permissions>;

  • Subject - label of the process
  • Object - label of the resource being accessed
  • Object class - for example, a directory or a file
  • Permissions - the access being granted (for example, read, write)

An example rule would be:

allow <process> <log_file>:FILE READ;

This is a very simplified explanation but creates a foundation for understanding the policy constructs that follow. For more in depth information, see Access Vector Rules on the SELinux Project wiki.

Policy languages and files

You can use the following languages for writing policies:
+ Standard SELinux source format - the original, most widely used and the one we use primarily for suggested approach to create policies and the example used in the article
+ M4 macros - this extends the standard format to include programming macros that increase productivity and promote re-use and consistency
+ Common Intermediate Language (CIL) - the newest and not commonly used even though the SELinux policy compiler converts everything into it. We briefly discuss it because it relates to troubleshooting later in the guide.

To customize SELinux policies in the standard language, use the following 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

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>

NOTE: if the reader is using the selinux-polgengui interface, select "standard init daemon" for the application type for the application type you want to confine (which should be the default) and then skip all of the other options in the subsequent windows that are not specified in the CLI commands.

See the Creating and enforcing an SELinux policy for a custom application section in the RHEL 9 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

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.

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.

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.

Interfaces available in RHEL, Fedora and CentOS Stream 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.

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

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 access to a resource 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)
  • [<name>] - 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.

Writing an example policy

Writing SELinux policies is no different than writing code for any other domain. It's an iterative process of creating policy code which includes fine tuning by resolving the AVCs, making changes from the results and verifying it again. This section will go through that process and introduce some tools and techniques helpful in this endeavor.

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 } with { 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

Because the directories and files still have the original context and not the newly create ones; you must 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

Fine tuning the policy

After you install the initial policy on the system, you can improve its effectiveness and ability to account for all access to resources required by the application by observing and addressing access denials to those resources that happen at runtime. For this, you must run the application and collect access denial logs.

Enabling auditing

To get the full logging that is available, you can enhance the default logging settings of the operating system.

To include all accessed paths in the 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 allow the process to continue to run if there are violations and 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. A better approach is to make 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 currently running policy module i.e the <myapp>.te file. Therefore, you can skip this step if you preserved the rule.

After you enable full auditing and permissive mode (system-wide or domain-specific), run the application in its new SELinux domain. To see all access (policy rules) required by the application, test as many use cases as possible to exercise all of the functionality (expected behavior of the application). The kernel logs all access requests that are not allowed with the current policy in the form of AVCs (Access Vector Cache denial log messages). Note that you may need to repeat this process several times because addressing an AVC may produce others.

Processing AVC denials

When SELinux denies an action, the system adds an Access Vector Cache (AVC denial) message to the /var/log/audit/audit.log and /var/log/messages files or the Journal daemon logs.

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 of 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.

To see this in action for our example; 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 interface file: bootupd.if. 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.

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.

Deploying a custom policy

When you have a custom module ready to deploy, you can choose from the following common ways to distribute it to other systems:

  1. Manual - package up all of the files that were generated (e.g. <app>.pp, <app>.te, <app>.if, <app>.fc …) and move them to each system. Place them according to the convention (this assumes you are using the targeted reference policy, which is the default for RHEL and Fedora) into /usr/share/selinux/targeted/<app>. Load the policy as previously described with the example. If the reference policy of the system, that the new policy is being loaded on, is different than the one on the system used to compile the module and loading produces errors, you must recompile the policy module on the new system.

  2. RPM package - refer to the Custom policy packaging guide

  3. Ansible - refer to Chapter 10 - Deploying The Same SELinux Configuration on Multiple Systems of the RHEL 9 Using SELinux title.

Conclusion

In this article, we have provided an introduction and quick start guide to creating custom SELinux policies for your applications and services. You have also learned how to troubleshoot and resolve SELinux AVC denials in general. You should be encouraged to take the next steps of understanding booleans for greater flexibility, expanded use of policies and to dive deeper into the presented topics. The following list of additional resources is provided to help with that.

Additional resources


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

Comments