A service can fail to read a file even when chmod and chown look correct. SELinux adds a separate label on every file—usually shown as the type in ls -Z output. Ansible can automate persistent labels with community.general.sefcontext, but that module only updates the mapping database. You still need a second step such as restorecon to apply the label to existing files, then verify with ls -Z, semanage fcontext -l, or matchpathcon.
This guide is a practical troubleshooting and automation walkthrough—not a full SELinux policy tutorial. It assumes you can run playbooks with become and understand basic file tasks.
Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16;
community.generalcollection.
~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
What are SELinux File Contexts?
Every file carries an SELinux context: user, role, type, and level (MLS/MCS). For day-to-day automation you mostly manage the type—for example httpd_sys_content_t for web content Apache can read, or var_log_t for log directories.
Linux discretionary permissions (chmod/chown) and SELinux mandatory access control work together. Either one can block access even when the other looks fine.
Why Manage SELinux File Contexts with Ansible?
Custom paths break when packages assume default locations:
- Non-standard web document roots under
/optor/srv - Application data under
/var/lib/myapp - Service-specific log directories
- Scripts installed outside packaged paths
Manual chcon fixes do not survive relabeling. Ansible can keep persistent rules in sync across hosts: add the sefcontext mapping, run restorecon when needed, and verify labels in the same play. Network-facing trees also need firewalld and SELinux host rules; application paths often belong to service accounts from manage users, groups and sudo.
Quick Workflow: Add Rule, Restore Context, Verify
Memory rule: sefcontext stores the rule → restorecon applies the label → ls -Z confirms reality.
| Step | Task | Why |
|---|---|---|
| 1 | Ensure SELinux is enabled | Labels matter when SELinux is enforcing or permissive |
| 2 | Create directory or choose target path | You need a stable path pattern |
| 3 | Add persistent rule with sefcontext |
Survives relabel operations |
| 4 | Run restorecon when rule or path changes |
Applies labels to new or existing files |
| 5 | Verify with ls -Z |
Confirms actual file label |
| 6 | Verify with semanage fcontext -l |
Confirms persistent rule |
| 7 | Test service access | Confirms policy allows the service to use that type |
sefcontext vs restorecon vs chcon vs file Module
| Tool / module | Purpose | Persistent? | Applies label immediately? | Best use |
|---|---|---|---|---|
community.general.sefcontext |
Manage SELinux file-context mapping | Yes | No | Define permanent rules |
restorecon |
Apply expected context from policy/rules | Uses persistent rules | Yes | Relabel after rule changes |
chcon |
Change context directly on files | Usually no | Yes | Temporary testing only |
ansible.builtin.file setype |
Set SELinux type on a path | No persistent mapping | Yes | One-off direct labeling |
semanage fcontext |
CLI equivalent of sefcontext |
Yes | No | Manual rule management |
The official sefcontext docs state the module does not modify existing files. Red Hat documentation describes semanage fcontext as storing persistent mappings while restorecon applies them to the filesystem.
Persistent rules vs the current on-disk label
Two different layers confuse troubleshooting:
| Layer | What it is | How you inspect it | Survives reboot / relabel? |
|---|---|---|---|
| Persistent mapping | Policy rule stored by sefcontext / semanage fcontext |
semanage fcontext -l | grep /path |
Yes |
| Current label | What SELinux actually applied to the inode right now | ls -Z /path or stat -c %C /path |
Until something relabels the file |
sefcontext updates only the persistent mapping—it does not change ls -Z on files that already exist. chcon changes the current label immediately but is usually not persistent. restorecon reads persistent rules and policy, then aligns the current label on disk. A common failure pattern: sefcontext reports changed, but ls -Z still shows unlabeled_t until you run restorecon.
Prerequisites for Managing SELinux Contexts
On managed hosts:
- SELinux enabled (
getenforceshowsEnforcingorPermissive) policycoreutils-python-utilsforsemanagepython3-libselinuxfor SELinux Python bindings (thesefcontextmodule runs on the managed host and needs these)restoreconandmatchpathconfrompolicycoreutils
On Rocky/RHEL, install the management tools and bindings together:
- name: Install SELinux management packages
ansible.builtin.dnf:
name:
- policycoreutils-python-utils
- python3-libselinux
state: present
become: trueWithout python3-libselinux, Ansible often fails with a message that SELinux Python bindings are missing.
On the control node:
community.generalcollection (ansible-galaxy collection install community.general)sefcontextis not in ansible-core—it lives incommunity.general
ansible.posix.selinux changes SELinux mode or policy—not per-file labels. Do not confuse it with sefcontext.
Create a Persistent SELinux File Context Rule
community.general.sefcontext is the Ansible equivalent of semanage fcontext.
Common parameters:
| Parameter | Role |
|---|---|
target |
Path or regex pattern (often /path(/.*)? for directories) |
setype |
SELinux type to assign (mutually exclusive with substitute) |
substitute |
Map target to inherit contexts from another path |
ftype |
Limit mapping by file type: a all (default), d directories, f regular files, l symlinks |
state |
present (default) or absent |
reload |
Reload SELinux policy after change (default true; does not relabel existing files) |
Most directory-tree rules use the default ftype: a. Use ftype only when you intentionally want different labels for directories, files, or symlinks.
Understand target path patterns
For a directory and everything under it, use the recursive pattern:
/opt/demo-web(/.*)?Without (/.*)?, the rule may apply only to the directory inode—not files created inside it.
Set SELinux type with sefcontext
- name: Add persistent SELinux context for web root
community.general.sefcontext:
target: /opt/demo-web(/.*)?
setype: httpd_sys_content_t
state: present
register: web_contextRegister the task so you can run restorecon when the rule or the path changes.
Map one path to another context with substitute
When a custom directory should inherit the same labeling rules as an existing standard path, use substitute instead of repeating many setype rules. setype and substitute are mutually exclusive.
- name: Make /srv/containers use /var/lib/containers contexts
community.general.sefcontext:
target: /srv/containers
substitute: /var/lib/containers
state: present
register: containers_context
- name: Apply inherited contexts to custom path
ansible.builtin.command: restorecon -Rv /srv/containers
register: containers_restore
changed_when: containers_restore.stdout | length > 0
when: containers_context is changedRun restorecon on the custom path after adding the equivalence mapping. If your playbook also creates /srv/containers or copies files into it, register those tasks and include them in the restorecon condition as well.
Apply SELinux Contexts with restorecon
Why sefcontext alone does not relabel existing files
sefcontext updates the persistent mapping. Files already on disk keep their old type until something relabels them. That is the most common “my playbook succeeded but ls -Z is wrong” report on forums.
Create the rule before files exist when you can. For existing trees, always follow with restorecon.
Run restorecon when the rule or path changes
A rule that already exists (sefcontext reports ok) still leaves new files or directories mislabeled if restorecon runs only when web_context is changed. Register directory creation, copy, and sefcontext tasks, then restore when any of them change:
- name: Create custom web document root
ansible.builtin.file:
path: /opt/demo-web
state: directory
mode: "0755"
register: web_dir
- name: Add persistent SELinux context for web root
community.general.sefcontext:
target: /opt/demo-web(/.*)?
setype: httpd_sys_content_t
state: present
register: web_context
- name: Apply context to web root
ansible.builtin.command: restorecon -Rv /opt/demo-web
register: web_restore
changed_when: web_restore.stdout | length > 0
when: web_context is changed or web_dir is changedAfter a copy task into the tree, extend the condition:
when: web_context is changed or web_dir is changed or web_copy is changedAlso run restorecon when files were created or copied before the rule existed, even if sefcontext reports ok on a later play.
Restore context recursively for directories
-R walks the directory tree. -v prints relabeled paths—useful in lab output and troubleshooting.
Verify SELinux File Contexts
Check labels with ls -Z
cd ~/ansible-project && ansible rocky2 -m shell -a "ls -ldZ /opt/demo-web /var/lib/demo-app /var/log/demo-app" -bSample output:
rocky2 | CHANGED | rc=0 >>
drwxr-xr-x. 2 root root unconfined_u:object_r:httpd_sys_content_t:s0 4096 Jul 8 23:11 /opt/demo-web
drwxr-x---. 2 root root unconfined_u:object_r:var_lib_t:s0 4096 Jul 8 23:11 /var/lib/demo-app
drwxr-xr-x. 2 root root unconfined_u:object_r:var_log_t:s0 4096 Jul 8 23:11 /var/log/demo-appThe object_r:TYPE:s0 segment is the type you configured.
Check persistent rules with semanage fcontext
ansible rocky2 -m shell -a "semanage fcontext -l | grep demo" -bSample output:
rocky2 | CHANGED | rc=0 >>
/opt/demo-web(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
/var/lib/demo-app(/.*)? all files system_u:object_r:var_lib_t:s0
/var/log/demo-app(/.*)? all files system_u:object_r:var_log_t:s0To list local custom rules only (not the entire policy database):
ansible rocky2 -m shell -a "semanage fcontext -l -C" -bUse -C when you want locally added or modified file-context rules instead of every built-in policy entry.
Preview expected labels with matchpathcon
ansible rocky2 -m shell -a "matchpathcon /opt/demo-web" -bSample output:
rocky2 | CHANGED | rc=0 >>
/opt/demo-web system_u:object_r:httpd_sys_content_t:s0If matchpathcon shows the expected type but ls -Z does not, run restorecon on the path.
Common Real-World Examples
Save the full lab play as playbooks/selinux-context-demo.yml. Run it from the project root:
cd ~/ansible-project && ansible-playbook playbooks/selinux-context-demo.ymlSample output:
PLAY RECAP *********************************************************************
rocky2 : ok=13 changed=12 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0A second run reports changed=0 when rules and labels already match.
Label a custom web document root
Apache and nginx read content labeled httpd_sys_content_t from custom paths when policy allows. Writable upload areas may need httpd_sys_rw_content_t instead—check your service policy before choosing a type.
- name: Create custom web document root
ansible.builtin.file:
path: /opt/demo-web
state: directory
mode: "0755"
register: web_dir
- name: Add persistent SELinux context for web root
community.general.sefcontext:
target: /opt/demo-web(/.*)?
setype: httpd_sys_content_t
state: present
register: web_context
- name: Apply context to web root
ansible.builtin.command: restorecon -Rv /opt/demo-web
register: web_restore
changed_when: web_restore.stdout | length > 0
when: web_context is changed or web_dir is changedLabel an application data directory
- name: Create application data directory
ansible.builtin.file:
path: /var/lib/demo-app
state: directory
mode: "0750"
register: app_dir
- name: Add persistent context for application data
community.general.sefcontext:
target: /var/lib/demo-app(/.*)?
setype: var_lib_t
state: present
register: app_context
- name: Apply context to application data
ansible.builtin.command: restorecon -Rv /var/lib/demo-app
register: app_restore
changed_when: app_restore.stdout | length > 0
when: app_context is changed or app_dir is changedUse a dedicated application type from your policy when one exists; var_lib_t is a reasonable starting point for generic data paths in lab work.
Label a log directory for a service
- name: Create demo log directory
ansible.builtin.file:
path: /var/log/demo-app
state: directory
mode: "0755"
register: log_dir
- name: Add persistent context for log directory
community.general.sefcontext:
target: /var/log/demo-app(/.*)?
setype: var_log_t
state: present
register: log_context
- name: Apply context to log directory
ansible.builtin.command: restorecon -Rv /var/log/demo-app
register: log_restore
changed_when: log_restore.stdout | length > 0
when: log_context is changed or log_dir is changedWeb server logs often use httpd_log_t; generic application logs commonly use var_log_t.
Label a custom script or binary path
- name: Deploy demo script
ansible.builtin.copy:
content: |
#!/bin/bash
echo demo
dest: /usr/local/bin/demo-app.sh
mode: "0755"
register: script_copy
- name: Add persistent context for demo script
community.general.sefcontext:
target: /usr/local/bin/demo-app.sh
setype: bin_t
state: present
register: script_context
- name: Apply context to demo script
ansible.builtin.command: restorecon -v /usr/local/bin/demo-app.sh
register: script_restore
changed_when: script_restore.stdout | length > 0
when: script_context is changed or script_copy is changedService-specific executables may need a domain type from policy—not every script belongs in bin_t.
Remove an old SELinux file context rule
- name: Remove obsolete web root context rule
community.general.sefcontext:
target: /opt/old-web(/.*)?
setype: httpd_sys_content_t
state: absentRemoving the rule does not automatically relabel files. Run restorecon or remove the paths if they are no longer used.
Verify and fix wrong context after copying files
A file can carry the wrong type after manual chcon, a copy from another path, or creation before the rule existed:
ansible rocky2 -m command -a "chcon -t user_home_t /opt/demo-web/index.html" -bSample output:
rocky2 | CHANGED | rc=0 >>Check the wrong label:
ansible rocky2 -m shell -a "ls -Z /opt/demo-web/index.html" -bSample output:
rocky2 | CHANGED | rc=0 >>
system_u:object_r:user_home_t:s0 /opt/demo-web/index.htmlApply the persistent rule with restorecon:
ansible rocky2 -m command -a "restorecon -v /opt/demo-web/index.html" -bSample output:
rocky2 | CHANGED | rc=0 >>
Relabeled /opt/demo-web/index.html from system_u:object_r:user_home_t:s0 to system_u:object_r:httpd_sys_content_t:s0Confirm the fix:
ansible rocky2 -m shell -a "ls -Z /opt/demo-web/index.html" -bSample output:
rocky2 | CHANGED | rc=0 >>
system_u:object_r:httpd_sys_content_t:s0 /opt/demo-web/index.htmlSELinux File Context Troubleshooting
| Symptom | Likely cause | What to check | Fix |
|---|---|---|---|
Service cannot read file despite correct chmod/chown |
Wrong SELinux type | ls -Z, ausearch / audit logs |
Add sefcontext rule and run restorecon |
sefcontext changed but ls -Z shows old type |
Existing files not relabeled | ls -Z |
Run restorecon on the path |
| Rule applies only to top directory | Missing recursive pattern | semanage fcontext -l |
Use /path(/.*)? |
| Context works until relabel or reboot | Used chcon only |
semanage fcontext -l |
Replace with sefcontext + restorecon |
restorecon does not apply expected label |
Pattern does not match path | matchpathcon, semanage fcontext -l |
Correct target regex |
| Copied files have unexpected labels | Created before rule or unusual source path | ls -Z |
Run restorecon after copy |
| Same path keeps wrong context | More specific rule or volatile path (/tmp) |
semanage fcontext -l, matchpathcon |
Adjust pattern or avoid volatile paths |
If ausearch -m avc returns nothing, verify auditd is running and check /var/log/audit/audit.log. On hosts with setroubleshoot installed:
journalctl -t setroubleshootDo not change random labels from one denial alone. Confirm whether the issue is wrong file type, a boolean, port labeling, or a missing policy allowance before editing contexts.
Common Mistakes
| Mistake | Why it is a problem |
|---|---|
Expecting sefcontext to relabel existing files immediately |
It stores a persistent rule only |
Using chcon for permanent automation |
Labels can be lost after relabeling |
Forgetting restorecon after adding a rule |
Files keep their old type |
Running restorecon only when sefcontext changes |
New files or dirs created later may stay mislabeled |
Missing (/.*)? for directories |
Child files may stay mislabeled |
Using too broad a target pattern |
More files get the type than intended |
| Choosing the wrong SELinux type | Permissions look fine but service access still fails |
Running restorecon before creating the rule |
Nothing to apply yet |
Not verifying with ls -Z |
Playbook success does not prove correct labels |
Confusing ansible.posix.selinux with file contexts |
Mode/policy module ≠ path labeling |
| Disabling SELinux instead of fixing labels | Hides the problem and weakens security |
Best Practices
- Add the persistent
sefcontextrule before creating or copying files when possible. - Use
/path(/.*)?for directory trees unless you intentionally target one inode. - Register
sefcontext, path creation, and copy tasks; runrestoreconwhen any of them change. - Verify with
ls -Zon the path andsemanage fcontext -l -Cfor local custom rules. - Prefer
sefcontext+restoreconoverchconin playbooks. - Check audit denials (
ausearch -m avc -ts recent) when permissions look correct but access fails. - Preview with
ansible-playbook --checkwhere supported; SELinux labeling tasks still need verification on the host.
Summary
community.general.sefcontext creates persistent SELinux file-context rules—the Ansible equivalent of semanage fcontext. It does not relabel existing files. Run restorecon after rule changes, confirm with ls -Z, and use matchpathcon when the expected type is unclear. Avoid temporary chcon in automation, pick types that match how services actually access paths, and treat SELinux labels as a separate layer from chmod and chown.
References
- community.general.sefcontext module
- ansible.posix.selinux module
- Red Hat — Changing SELinux states and modes
- Red Hat — Managing SELinux booleans

