Writing policies with CIL: type, attribute, and modern rule declarations, compilation with secilc, modular policy hierarchy, the difference between .pp and CIL modules, inter-module dependencies, and maintainability best practices.

In episode 12 you wrote your first module — both in classic Type Enforcement and CIL. In episode 16 you learned to investigate live machines down to their kernel bridge. Now we combine the two at a larger scale: how is a healthy production policy composed as a system of modules, not one giant file?
This episode is the peak of authoring technique. We dissect CIL as the modern intermediate language — type, attribute, and rule declarations in S-expressions — then compile with secilc. Next we enter architecture: the modular policy hierarchy, the difference between .pp vs CIL modules, managing inter-module dependencies, and best practices that keep the policy maintainable for years, not just functional today.
Every modern SELinux policy — including the ones generated from refpolicy source — is ultimately converted into CIL (Common Intermediate Language) before being compiled into the binary the kernel consumes. Think of CIL as an intermediate bytecode: not a "high-level" language like .te, but also not binary. Because of that position, CIL becomes a stable meeting point: whatever the source language, everything ends up in CIL.
There's one pleasant practical consequence: you can write policies directly in CIL without touching .te at all, and the result will be loaded by a modern kernel without intermediaries. That's the fastest path from idea to a working policy.
CIL syntax is S-expressions: everything is wrapped in parentheses. Let's dissect the core declarations:
(type myapp_t)
(type myapp_exec_t)
(typeattribute daemon)
(typeattributeset daemon (myapp_t))
(roletype system_r myapp_t)
(allow myapp_t self (process (sigchld)))
(allow myapp_t etc_t (file (read)))
(typeattributeset cil_gen_require (etc_t))Line by line:
(type myapp_t) — declares a type, equivalent to type in TE.(typeattribute daemon) — declares an attribute (a "category" that holds many types).(typeattributeset daemon (myapp_t)) — puts a type into an attribute; equivalent to typeattribute+type in TE.(roletype system_r myapp_t) — allows the type to be used as a system process.(allow ...) — a rule; the argument order is the same: source, target, class, and the permission set.(typeattributeset cil_gen_require (etc_t)) — CIL's dependency mechanism: "I need the etc_t type defined by another module". This is equivalent to the require block in TE.Notice how structures scattered across several keywords in TE (type, typeattribute, require) become uniform in CIL. This uniformity is what makes CIL easier to parse, validate, and generate with tools.
The CIL compiler is secilc. The flow is the same as episode 12, but now directly from CIL text:
secilc -o myapp.cil.bin myapp.cil
semodule -i myapp.cil.binCompilation succeeded.Two things worth noting. First, semodule -i also accepts raw .cil files — libsemanage will call secilc behind the scenes; however, compiling explicitly gives you control and faster error feedback during development. Second, secilc runs neverallow checks (episode 14) at compile time — so a "neverallow violation" error appears early, before the policy touches a production machine. That's a guarantee you can't buy through ordinary config management.
A production SELinux system doesn't consist of one policy — it consists of a base policy + a collection of modules. The base policy is carried by the distro (in selinux-policy-targeted), and the modules (including yours) stack on top of it. See the hierarchy:
semodule --list-modules400 myapp cil
400 mydaemon pp
100 base baseThis structure is a deliberate architecture: the base policy contains the foundation (core types, classes, rules that almost always apply), and each module adds one area of responsibility. Because modules are loaded in layers, a fix never forces you to rebuild the entire policy — just semodule -i for the changed module. Compare that to replacing the entire base policy to change one application: that's what this modular design avoids.
Two module formats whose differences you need to understand:
| Aspect | .pp module | .cil module |
|---|---|---|
| Source | Compiled from .te | Direct CIL text |
| Binary? | Yes, compiled | Text or .cil.bin |
| Easy to review | Hard (binary) | Easy (text) |
| Modern features | Limited | Full (block, macro, optional) |
| Audit/versioning | Hard | Easy (git diff) |
.pp still exists for compatibility — many distro modules and old tooling produce it (checkmodule + semodule_package). But for policies you write yourself, CIL is the better choice: it can be diffed, reviewed in code review, and carries features .pp doesn't have.
Modules don't live alone. When module A uses a type owned by module B, a dependency appears. In CIL, dependencies are declared explicitly with cil_gen_require, and CIL provides tools to make dependencies optional — a feature that makes your modular hierarchy resilient to change:
(optional httpd_integration
(typeattributeset cil_gen_require (httpd_t))
(allow httpd_t myapp_t (tcp_socket (name_connect)))
)
(macro daemon_domain ((type domain_t))
(roletype system_r domain_t)
(allow domain_t self (process (sigchld)))
)An optional block contains rules that only activate if their dependency is satisfied — if the module providing httpd_t isn't installed, the block is skipped without error, and when that module arrives later, the policy doesn't need to be rebuilt. Meanwhile, macro lets you define templates: daemon_domain is a "make this a system daemon" recipe called repeatedly with different types. These two mechanisms, optional and macro, are the key to keeping your modules flexible and not fragile against new service additions.
Warning
Uncontrolled inter-module dependencies create "spaghetti". A rule of thumb: each module is one application/one service, and modules must not use each other's internal types except through intentional interfaces (or cil_gen_require). If you find module A allowing module B's types for something that isn't real interaction, that's a design alarm — move that rule to the right place before stacking more.
A policy is code — treat it like code, not a one-off artifact:
myapp, not all-apps). This makes debugging easier: semodule -l | grep points straight at the suspect.typeattribute and macro (see the examples above) keep you from duplicating the same rules for similar types. One change in a macro, all users change along with it.dontaudit to cover problems. dontaudit (episode 8) swallows denials silently; it hides signals, it doesn't solve the root cause. Use it as a documented exception, not a pattern.secilc (and selint if available) as a CI gate. A passing compilation means your module is valid; selint gives style warnings that prevent design bugs early./usr/share/selinux/devel/Makefile — with make -f /usr/share/selinux/devel/Makefile myapp.pp you get a TE→PP compilation consistent with the distro version, while still keeping the source in git.In this episode 17, you've reached the peak of policy authoring: understanding CIL as the intermediate language that's the meeting point of all modern policies, writing type, attribute, and rule declarations in S-expressions, compiling with secilc (with built-in neverallow checks), understanding the modular policy hierarchy (base + modules), distinguishing .pp and CIL modules and when to choose each, managing inter-module dependencies with optional and macro, and applying maintainability best practices for policies that last for years.
The essentials to take with you:
secilc compiles and checks neverallow — free validation before production..pp for compatibility, CIL for the future — choose per context.optional and macro keep modules flexible against change.With this, you're officially a policy engineer — not just an operator pushing buttons. The policies you build are now modular, documented, and tested. But there's a question that only becomes tangible when scale grows: does all this hierarchy stay efficient when the policy is large and the machines are busy? In the next episode 18, we enter Performance & Scalability: the AVC cache, userspace policy caches, the impact of booleans on decision speed, and strategies for maintaining large policies without sacrificing responsiveness. See you in episode 18!