Level 3

Custom Policy Modules

Difficulty: Intermediate | Reward: +200 XP | Prerequisites: Level 2 - Contexts and Labels

Mission Briefing

Your team deployed a custom daemon called vaultsyncd that synchronizes encrypted secrets between hosts. It listens on port 8470 and reads configuration files from /opt/vaultsync/data/. In permissive mode it runs fine. In enforcing mode it dies immediately with a wall of AVC denials. Your mission: write a proper SELinux policy module from scratch so this daemon runs confined under its own type.

Why Stock Policy Is Not Enough

The targeted policy ships with types for well-known daemons - httpd_t, sshd_t, named_t, and dozens more. But your custom daemon has no predefined type. Without one, it inherits the type of whatever process started it. If systemd launches it, the daemon runs as init_t or unconfined_t, which either gives it too much access or causes unpredictable denials. A custom policy module solves this by defining a dedicated type with exactly the permissions your daemon needs - nothing more.

The Policy Module Skeleton

The fastest way to generate a starting point is sepolicy generate:

[root@gamehost ~]# sepolicy generate --init /usr/local/bin/vaultsyncd
Created the following files:
Type enforcement file: vaultsyncd.te
Interface file: vaultsyncd.if
File contexts file: vaultsyncd.fc
Setup script: vaultsyncd.sh

This creates three critical files. The .te file (type enforcement) defines the new type and its allowed access rules. The .fc file (file contexts) maps filesystem paths to SELinux labels. The .if file (interface) defines macros that other policy modules can use to interact with your type. For most custom daemons, you will spend 90% of your time in the .te and .fc files.

Anatomy of a .te File

A minimal type enforcement file for vaultsyncd looks like this:

policy_module(vaultsyncd, 1.0.0)

type vaultsyncd_t;
type vaultsyncd_exec_t;
type vaultsyncd_data_t;
type vaultsyncd_port_t;

init_daemon_domain(vaultsyncd_t, vaultsyncd_exec_t)

allow vaultsyncd_t vaultsyncd_data_t:file { read open getattr };
allow vaultsyncd_t vaultsyncd_data_t:dir { search getattr };
allow vaultsyncd_t vaultsyncd_port_t:tcp_socket { name_bind };

Each allow rule follows the same pattern: source type, target type, object class, and a set of permissions in braces. The init_daemon_domain macro handles the transition from init_t to vaultsyncd_t when systemd starts the binary.

The .fc File

The file contexts file tells restorecon how to label your daemon's files:

/usr/local/bin/vaultsyncd -- gen_context(system_u:object_r:vaultsyncd_exec_t,s0)
/opt/vaultsync/data(/.*)? -- gen_context(system_u:object_r:vaultsyncd_data_t,s0)

The audit2allow Trap

You will see countless tutorials that tell you to pipe AVC denials into audit2allow and load the result. This works, but it is dangerous. The tool generates the most permissive rules possible to satisfy the logged denials. If your daemon tried to read /etc/shadow by accident, audit2allow will happily create a rule allowing it. Always review the output. Use audit2allow -R to get reference policy macros instead of raw allow rules when possible, and treat the output as a starting point - not a finished policy.

[root@gamehost ~]# ausearch -m AVC -ts recent | audit2allow -R
require {
type vaultsyncd_t;
}
corenet_tcp_bind_generic_port(vaultsyncd_t)

Compiling and Loading

Once your .te and .fc files are ready, compile and install the module:

[root@gamehost ~]# checkmodule -M -m -o vaultsyncd.mod vaultsyncd.te
[root@gamehost ~]# semodule_package -o vaultsyncd.pp -m vaultsyncd.mod -f vaultsyncd.fc
[root@gamehost ~]# semodule -i vaultsyncd.pp
[root@gamehost ~]# semodule -l | grep vaultsyncd
vaultsyncd 1.0.0

After loading, apply the new file contexts and register the port:

[root@gamehost ~]# restorecon -Rv /usr/local/bin/vaultsyncd /opt/vaultsync/
[root@gamehost ~]# semanage port -a -t vaultsyncd_port_t -p tcp 8470

Challenge: Confine the Daemon

The vaultsyncd daemon needs to:

  1. Listen on TCP port 8470
  2. Read files under /opt/vaultsync/data/
  3. Write to its own log at /var/log/vaultsyncd.log
  4. Connect to a remote host on port 443 for secret synchronization

Your tasks:

  1. Generate a policy skeleton with sepolicy generate
  2. Add a vaultsyncd_log_t type and the appropriate allow rules for logging
  3. Add corenet_tcp_connect_http_port for outbound HTTPS connections
  4. Compile, load, relabel, and verify zero AVC denials with ausearch

Bonus objective: Run audit2allow against any remaining denials and explain why each suggested rule is or is not safe to add.

Common Mistakes

ACHIEVEMENT UNLOCKED

Policy Architect

You can write, compile, and load custom SELinux policy modules for any daemon. +200 XP

Next: Level 4 - Booleans Deep Dive