Secure Linux environments demand more than just turning SELinux on; they require tailored policies that let your custom applications run safely without opening unnecessary doors. In this guide we’ll walk through the entire lifecycle of building a SELinux policy module for a bespoke service—from installing the right toolset, through generating a template, to testing, troubleshooting, and finally making the policy permanent. By the end you’ll have a reusable, well‑documented policy that blends seamlessly with the system’s existing security posture.
What You'll Need
- A RHEL, CentOS, Fedora, or Rocky Linux system with SELinux in Enforcing mode.
- Root or sudo privileges.
- policycoreutils, policycoreutils-devel, setools-console, and audit packages installed.
- Basic knowledge of Linux file permissions and systemd service units.
- The source code or binary of the custom application you intend to protect.
Step 1: Install the SELinux Development Toolchain
Before you can write or compile policies you need the development libraries and utilities. On a RHEL‑based distro run:
sudo dnf install -y policycoreutils-devel setools-console selinux-policy-devel audit This command pulls in checkmodule, semodule_package, and audit2allow, which are the workhorses for policy compilation and analysis. Verify the installation with:
rpm -q policycoreutils-devel setools-console If any package is missing, the build step later will fail with cryptic errors.
Step 2: Identify the Application's Security Context
SELinux labels every process, file, and socket. To create a policy you first need a unique domain (type) for your application. Start the application in permissive mode to let SELinux log what it would have blocked:
sudo setenforce 0 # temporarily switch to permissive Then launch your binary (or systemd unit) and generate an audit log:
sudo journalctl -f -t avc Watch for entries that look like type=AVC msg=audit(...): avc: denied { read } for pid=1234 comm="myapp" name="/etc/myapp.conf" dev="sda1" ino=56789 scontext=system_u:system_r:unconfined_t:s0 tcontext=system_u:object_r:etc_t:s0 tclass=file. The tcontext tells you the current label (etc_t) and the scontext shows the process is still running as unconfined_t. You’ll replace unconfined_t with a dedicated domain later.
Take note of all file paths, sockets, and capabilities the app touches; you’ll feed these into audit2allow to generate allow rules.
Step 3: Generate a Policy Module Template
With the audit log captured, use audit2allow to scaffold a minimal module. First, collect the relevant AVC messages into a file:
sudo ausearch -m avc -ts recent | audit2allow -M myapp This command does two things: it extracts recent denials and runs them through audit2allow, which outputs a module named myapp.te (source) and myapp.pp (binary package). Open myapp.te in your editor; you’ll see something like:
module myapp 1.0;
require {
type unconfined_t;
type etc_t;
class file { read open };
}
#============= unconfined_t ==============
allow unconfined_t etc_t:file { read open }; This is a starting point, but you’ll want to replace unconfined_t with a custom domain, e.g., myapp_t. Add a domain declaration at the top:
type myapp_t;
init_daemon_domain(myapp_t) Now change the allow line to reference myapp_t instead of unconfined_t. Repeat this process for each resource the audit log highlighted.
Step 4: Refine the Module with Precise Allow Rules
Auto‑generated rules tend to be overly permissive. Manually prune them by asking two questions for each rule:
- Is the permission truly required for the application’s functionality?
- Can the rule be narrowed to a more specific type (e.g.,
myapp_var_lib_tinstead ofvar_t)?
Define custom file types for directories your app owns:
type myapp_var_lib_t;
files_type(myapp_var_lib_t) Then label the directory:
sudo semanage fcontext -a -t myapp_var_lib_t "/opt/myapp(/.*)?"
sudo restorecon -Rv /opt/myapp Replace generic var_t references in the policy with myapp_var_lib_t. For network sockets, declare a port type if you use a non‑standard port:
semanage port -a -t myapp_port_t -p tcp 8085 And add the corresponding allow rule:
allow myapp_t myapp_port_t:tcp_socket name_bind; Iterate this refinement until audit2allow -w -a reports “No further AVCs” for your test runs.
Step 5: Build and Load the Policy Module
When you’re satisfied with myapp.te, compile it into a binary module:
checkmodule -M -m -o myapp.mod myapp.te
semodule_package -o myapp.pp -m myapp.mod Load the module into the kernel:
sudo semodule -i myapp.pp Verify that the module is active:
semodule -l | grep myapp If the command returns myapp, the policy is now part of the running SELinux policy set.
Step 6: Test the Application Under Enforcing Mode
Switch SELinux back to Enforcing:
sudo setenforce 1 Start your service normally (e.g., systemctl start myapp) and monitor the logs:
sudo journalctl -u myapp -f If the service starts without AVC denials, you’ve succeeded. If you see new denials, capture them with ausearch -m avc -ts recent and feed them back into audit2allow to extend the module. Remember to rebuild and reload after each change.
Step 7: Make the Policy Persistent Across Reboots
Policy modules loaded with semodule -i are stored in /etc/selinux/targeted/modules/active/modules, so they survive reboots. However, it’s good practice to keep the source files in version control and document the build steps in a script, for example build_myapp_policy.sh:
#!/bin/bash
set -e
# Build SELinux policy for myapp
checkmodule -M -m -o myapp.mod myapp.te
semodule_package -o myapp.pp -m myapp.mod
sudo semodule -i myapp.pp
Deploy the script alongside your application’s deployment pipeline to guarantee the policy is always installed on new hosts.
Common Mistakes to Avoid
1. Leaving SELinux in Permissive for too long. It’s tempting to develop entirely in permissive mode, but this defeats the purpose of policy testing. Switch back to Enforcing early and iterate.
2. Copy‑pasting auto‑generated rules without review. audit2allow may suggest broad permissions like allow myapp_t var_t:file { read write getattr open }; which can open a security hole. Always narrow the type and the permission set.
3. Forgetting to label custom directories. If you create /opt/myapp/data but never run semanage fcontext, the files will retain the default default_t label, causing unnecessary denials.
4. Using the wrong SELinux policy target. The guide assumes a “targeted” policy. On a “mls” or “strict” policy the syntax for type enforcement can differ, and additional attributes may be required.
5. Not cleaning up stale modules. Over time you may accumulate obsolete modules. List them with semodule -l and remove with semodule -r oldmodule to keep the policy base lean.
Tips and Tricks
• semanage permissive -a myapp_t lets you test a new domain without affecting the rest of the system—useful for early debugging.
• The sesearch utility can query the current policy to verify that a specific allow rule exists: sesearch -A -s myapp_t -t myapp_var_lib_t -c file -p read.
• When dealing with network daemons, prefer type_transition rules to automatically label sockets created by the service, e.g., type_transition myapp_t myapp_port_t:tcp_socket myapp_socket_t;.
• Keep your policy files under /usr/share/selinux/devel/include if you plan to distribute them as part of a custom RPM. The make install step will automatically register the module.
Frequently Asked Questions
Can I use the same module on different distributions?
Generally yes, as long as they share the same SELinux policy base (e.g., “targeted”). However, minor differences in type names or attribute sets may require small adjustments. Test on each target OS before production rollout.
What if my application needs to run in a container?
Containers have their own SELinux labeling (e.g., svirt_lxc_net_t). You can either extend the host policy to allow the container domain to interact with your custom types, or embed the module inside the container image using semanage -i during image build.
Do I need to reload the policy after every code change?
No. The policy governs the process’s domain, not its binary. Only changes to file types, ports, or additional resources require a policy rebuild. Regular code updates are safe as long as the required permissions stay the same.
Conclusion
Configuring SELinux for a custom application is a disciplined process that blends system administration, security reasoning, and a bit of scripting. By installing the right toolchain, harvesting real AVC data, crafting a minimal yet precise policy module, and iteratively testing under Enforcing mode, you can lock down your software without sacrificing functionality. Remember to document every step, keep the source policy under version control, and automate the build‑install cycle. With these practices in place, your custom applications will enjoy the robust protection SELinux promises, while you maintain confidence that the policy surface area remains as small as possible.
Photo by Markus Winkler on Unsplash





