Tools: Report: I got tired of setenforce 0. So I built a tool in Rust to actually understand SELinux denials.

Tools: Report: I got tired of setenforce 0. So I built a tool in Rust to actually understand SELinux denials.

What it does

Analyze the latest denial in your system log

Analyze a specific log line

Pipe directly from the audit log

Why Rust

How it's built

The reception

What's next

Try it Every Fedora user has been there. You're setting up nginx, or configuring a custom app, or mounting a Docker volume — and suddenly everything stops working. You check the logs and you find something like this: And your brain just... stops. Most people at this point do one of two things: paste the log on StackOverflow and wait, or run sudo setenforce 0 and forget about it. I've done both. The second option is particularly dangerous because you're disabling the entire SELinux enforcement on your system just because you couldn't read a log line.

There are existing tools — sealert, audit2why, the SELinux plugin for Cockpit. They're useful if you know what you're doing. But they all have friction: sealert requires the setroubleshootd daemon running in the background with D-Bus, audit2why is part of a Python package that isn't always available on minimal or headless servers, and Cockpit requires a full Cockpit setup.I wanted something simpler. A single binary I could drop on any machine — a fresh VPS, an air-gapped server, a minimal Fedora install — and just pipe a log line into it. So I built selinux-explain. selinux-explain takes a raw SELinux AVC denial and turns it into something a human can actually act on.Instead of the wall of text above, you get: Three things: what happened, why SELinux blocked it, and what to actually do about it. No daemon, no internet, no Python. You can use it three ways: I'm a second-year Computer Engineering student at the University of Bologna and Rust is the language I've been learning independently over the past year. This project felt like the right fit for a few reasons.The main one is the deployment model. A Rust binary compiles to a single static executable with no runtime dependencies. You cargo build --release, copy the binary to /usr/local/bin/, and you're done. No Python interpreter, no shared libraries to worry about, no pip install in a system that might not even have pip. For a tool that targets sysadmins on production servers, this matters.The second reason is performance. Parsing log files with regex is fast in any language, but with Rust you also get predictable memory usage and no garbage collector pauses. For a CLI tool that might be piped across thousands of log lines, that's not irrelevant. The architecture is straightforward — four modules: The result is an AvcData struct: This is the "80/20" approach: cover the most common denial types first (httpd_t, container_t) and fall back gracefully for everything else. The plan is to eventually move these rules into an external rules.toml file so the community can contribute new cases without touching Rust code. I posted about this on r/redhat before even having a working version — just a day-0 post asking whether people would find it useful. The response surprised me: over 5K views and 14 comments in the first 24 hours, including a Red Hat employee who commented and pointed out a classic Unix anti-pattern in my example (I was using cat file | grep instead of just grep file — the infamous "useless use of cat"). That kind of immediate feedback from people who actually work with these systems every day is exactly what you want when building a tool like this.Several people pointed out sealert and audit2why as existing alternatives. Fair points — and I addressed them directly in the thread. The positioning isn't "this replaces everything" but "this works everywhere, with zero setup, offline." The immediate roadmap: The repo is at selinux-explain. There's a pre-compiled binary on the Releases page if you don't want to build from source.

If you run into a log line that doesn't parse correctly, open an issue with the raw log string. Every real-world case helps make the parser more robust — and if you have a fix that worked for you for a type that isn't covered yet, I'd love to hear it. The goal is simple: nobody should have to run setenforce 0 because they couldn't read a log file. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

type=AVC msg=audit(1612345678.123:456): avc: denied { read } for pid=1234 comm="nginx" name="index.html" dev="sda1" ino=12345 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0 type=AVC msg=audit(1612345678.123:456): avc: denied { read } for pid=1234 comm="nginx" name="index.html" dev="sda1" ino=12345 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0 type=AVC msg=audit(1612345678.123:456): avc: denied { read } for pid=1234 comm="nginx" name="index.html" dev="sda1" ino=12345 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0 🚨 SELinux Denial Detected! ============================= Who: nginx What: Tried to 'read' the target 'index.html' Why: A process with label 'httpd_t' is not allowed to access objects with label 'user_home_t' 💡 Suggestion: The web server is trying to read a file or directory labeled 'user_home_t'. Fix its context with: `sudo restorecon -Rv </path/to/index.html>` Or set it manually: `sudo chcon -t httpd_sys_content_t </path/to/index.html>` 🚨 SELinux Denial Detected! ============================= Who: nginx What: Tried to 'read' the target 'index.html' Why: A process with label 'httpd_t' is not allowed to access objects with label 'user_home_t' 💡 Suggestion: The web server is trying to read a file or directory labeled 'user_home_t'. Fix its context with: `sudo restorecon -Rv </path/to/index.html>` Or set it manually: `sudo chcon -t httpd_sys_content_t </path/to/index.html>` 🚨 SELinux Denial Detected! ============================= Who: nginx What: Tried to 'read' the target 'index.html' Why: A process with label 'httpd_t' is not allowed to access objects with label 'user_home_t' 💡 Suggestion: The web server is trying to read a file or directory labeled 'user_home_t'. Fix its context with: `sudo restorecon -Rv </path/to/index.html>` Or set it manually: `sudo chcon -t httpd_sys_content_t </path/to/index.html>` sudo selinux-explain --last sudo selinux-explain --last sudo selinux-explain --last selinux-explain --text "type=AVC msg=audit..." selinux-explain --text "type=AVC msg=audit..." selinux-explain --text "type=AVC msg=audit..." grep nginx /var/log/audit/audit.log | selinux-explain grep nginx /var/log/audit/audit.log | selinux-explain grep nginx /var/log/audit/audit.log | selinux-explain rustlet re = Regex::new( r#"denied\s*\{\s*(.*?)\s*\}.*?comm="(.*?)".*?name="(.*?)" .*?scontext=(\S+).*?tcontext=(\S+).*?tclass=(\S+)"# ).ok()?; rustlet re = Regex::new( r#"denied\s*\{\s*(.*?)\s*\}.*?comm="(.*?)".*?name="(.*?)" .*?scontext=(\S+).*?tcontext=(\S+).*?tclass=(\S+)"# ).ok()?; rustlet re = Regex::new( r#"denied\s*\{\s*(.*?)\s*\}.*?comm="(.*?)".*?name="(.*?)" .*?scontext=(\S+).*?tcontext=(\S+).*?tclass=(\S+)"# ).ok()?; rustpub struct AvcData { pub process: String, pub action: String, pub target: String, pub scontext: String, pub tcontext: String, pub tclass: String, } rustpub struct AvcData { pub process: String, pub action: String, pub target: String, pub scontext: String, pub tcontext: String, pub tclass: String, } rustpub struct AvcData { pub process: String, pub action: String, pub target: String, pub scontext: String, pub tcontext: String, pub tclass: String, } rustmatch (source_type, action, tclass) { ("httpd_t", "read" | "open" | "getattr", "file" | "dir") => { // web server trying to read a file with wrong label }, ("httpd_t", "name_connect", "tcp_socket") => { // web server trying to make outbound network connection }, ("container_t", _, _) => { // container trying to access host resource }, _ => { // generic fallback } } rustmatch (source_type, action, tclass) { ("httpd_t", "read" | "open" | "getattr", "file" | "dir") => { // web server trying to read a file with wrong label }, ("httpd_t", "name_connect", "tcp_socket") => { // web server trying to make outbound network connection }, ("container_t", _, _) => { // container trying to access host resource }, _ => { // generic fallback } } rustmatch (source_type, action, tclass) { ("httpd_t", "read" | "open" | "getattr", "file" | "dir") => { // web server trying to read a file with wrong label }, ("httpd_t", "name_connect", "tcp_socket") => { // web server trying to make outbound network connection }, ("container_t", _, _) => { // container trying to access host resource }, _ => { // generic fallback } } - parser.rs is the core. It takes a raw log line and extracts the relevant fields using a regex: - explainer.rs takes an AvcData and produces a human-readable explanation. The interesting part is get_specific_advice(), which matches on a tuple of (source_type, action, tclass) to give contextual suggestions: - reader.rs handles reading from /var/log/audit/audit.log for the --last flag — it scans the file line by line and returns the most recent AVC denial. - main.rs ties everything together with clap for argument parsing, and handles the three input modes: --last, --text, and stdin pipe detection via io::stdin().is_terminal(). - rules.toml support — an external file that maps (source_type, action, tclass) tuples to suggestions, so the community can contribute new cases without needing to know Rust - RPM package and COPR repository — once the tool is stable enough, proper distro packaging for Fedora and RHEL - More covered types — mysqld_t, sshd_t, smbd_t are next on the list