Manage SELinux File Contexts with Ansible

Use Ansible sefcontext for persistent SELinux file labels, restorecon to apply them, and ls -Z or semanage fcontext to verify—plus troubleshooting when services fail despite correct chmod.

Published

Updated

Read time 12 min read

Reviewed byDeepak Prasad

Manage SELinux file contexts with Ansible on Rocky Linux 10

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.general collection.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/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 /opt or /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 (getenforce shows Enforcing or Permissive)
  • policycoreutils-python-utils for semanage
  • python3-libselinux for SELinux Python bindings (the sefcontext module runs on the managed host and needs these)
  • restorecon and matchpathcon from policycoreutils

On Rocky/RHEL, install the management tools and bindings together:

yaml
- name: Install SELinux management packages
      ansible.builtin.dnf:
        name:
          - policycoreutils-python-utils
          - python3-libselinux
        state: present
      become: true

Without python3-libselinux, Ansible often fails with a message that SELinux Python bindings are missing.

On the control node:

  • community.general collection (ansible-galaxy collection install community.general)
  • sefcontext is not in ansible-core—it lives in community.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:

output
/opt/demo-web(/.*)?

Without (/.*)?, the rule may apply only to the directory inode—not files created inside it.

Set SELinux type with sefcontext

yaml
- 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

Register 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.

yaml
- 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 changed

Run 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:

yaml
- 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 changed

After a copy task into the tree, extend the condition:

yaml
when: web_context is changed or web_dir is changed or web_copy is changed

Also 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

bash
cd ~/ansible-project && ansible rocky2 -m shell -a "ls -ldZ /opt/demo-web /var/lib/demo-app /var/log/demo-app" -b

Sample output:

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-app

The object_r:TYPE:s0 segment is the type you configured.

Check persistent rules with semanage fcontext

bash
ansible rocky2 -m shell -a "semanage fcontext -l | grep demo" -b

Sample output:

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:s0

To list local custom rules only (not the entire policy database):

bash
ansible rocky2 -m shell -a "semanage fcontext -l -C" -b

Use -C when you want locally added or modified file-context rules instead of every built-in policy entry.

Preview expected labels with matchpathcon

bash
ansible rocky2 -m shell -a "matchpathcon /opt/demo-web" -b

Sample output:

output
rocky2 | CHANGED | rc=0 >>
/opt/demo-web	system_u:object_r:httpd_sys_content_t:s0

If 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:

bash
cd ~/ansible-project && ansible-playbook playbooks/selinux-context-demo.yml

Sample output:

output
PLAY RECAP *********************************************************************
rocky2                     : ok=13   changed=12   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

A 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.

yaml
- 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 changed

Label an application data directory

yaml
- 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 changed

Use 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

yaml
- 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 changed

Web server logs often use httpd_log_t; generic application logs commonly use var_log_t.

Label a custom script or binary path

yaml
- 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 changed

Service-specific executables may need a domain type from policy—not every script belongs in bin_t.

Remove an old SELinux file context rule

yaml
- name: Remove obsolete web root context rule
      community.general.sefcontext:
        target: /opt/old-web(/.*)?
        setype: httpd_sys_content_t
        state: absent

Removing 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:

bash
ansible rocky2 -m command -a "chcon -t user_home_t /opt/demo-web/index.html" -b

Sample output:

output
rocky2 | CHANGED | rc=0 >>

Check the wrong label:

bash
ansible rocky2 -m shell -a "ls -Z /opt/demo-web/index.html" -b

Sample output:

output
rocky2 | CHANGED | rc=0 >>
system_u:object_r:user_home_t:s0 /opt/demo-web/index.html

Apply the persistent rule with restorecon:

bash
ansible rocky2 -m command -a "restorecon -v /opt/demo-web/index.html" -b

Sample output:

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:s0

Confirm the fix:

bash
ansible rocky2 -m shell -a "ls -Z /opt/demo-web/index.html" -b

Sample output:

output
rocky2 | CHANGED | rc=0 >>
system_u:object_r:httpd_sys_content_t:s0 /opt/demo-web/index.html

SELinux 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:

bash
journalctl -t setroubleshoot

Do 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 sefcontext rule 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; run restorecon when any of them change.
  • Verify with ls -Z on the path and semanage fcontext -l -C for local custom rules.
  • Prefer sefcontext + restorecon over chcon in playbooks.
  • Check audit denials (ausearch -m avc -ts recent) when permissions look correct but access fails.
  • Preview with ansible-playbook --check where 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

Frequently Asked Questions

1. Why does sefcontext not change ls -Z output?

sefcontext updates the persistent file-context mapping in the SELinux policy database. It does not relabel files already on disk. Run restorecon on existing paths after the rule changes.

2. What is the difference between sefcontext and restorecon?

sefcontext is Ansible automation for semanage fcontext—it stores a persistent rule. restorecon applies the expected label from policy and stored rules to files and directories.

3. Should I use chcon in Ansible playbooks?

Usually no. chcon changes the label immediately but is often lost after relabeling or policy updates. Prefer sefcontext plus restorecon for persistent automation.

4. What target pattern should I use for a directory tree?

Use /path(/.)? so the rule covers the directory and all files beneath it. A pattern without (/.)? may label only the directory inode.

5. Does ansible.posix.selinux set file contexts?

No. ansible.posix.selinux manages SELinux mode and policy state on the host. File path labels use sefcontext, restorecon, or file setype for direct labeling.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …