The error
You kick off an Ansible playbook against a RHEL or CentOS host and the very first task blows up:
fatal: [web01]: FAILED! => {
"msg": "Aborting, target uses selinux but python bindings (libselinux-python) are not installed!"
}
Nothing useful ran. Ansible detected SELinux on the remote host, tried to load its Python bindings, found nothing there, and quit. It won't proceed blind โ too much risk of corrupting file contexts.
Why this happens
Ansible relies on Python to talk to the remote OS. Many modules โ file, copy, template, package management โ quietly check SELinux context before doing their work. That check requires libselinux-python (Python 2) or python3-libselinux (Python 3) on the target machine, not on your control node.
Four situations cause this to bite people:
- Fresh minimal RHEL/CentOS installs โ SELinux bindings aren't included by default, even when SELinux itself is enabled
- Migrating from CentOS 7 to CentOS 8/RHEL 8 โ the package was renamed, and your old playbooks don't know that yet
- Stripped-down Docker or cloud images that cut everything non-essential
- Explicitly setting
ansible_python_interpreter=/usr/bin/python3on a host that only has the Python 2 version of the bindings installed
Fix 1: Install the correct package on the target host
SSH in and install based on OS version. The package name changed between RHEL 7 and 8.
RHEL/CentOS 7 โ Python 2
sudo yum install -y libselinux-python
RHEL/CentOS 8 / Rocky Linux 8 / AlmaLinux 8 โ Python 3
sudo dnf install -y python3-libselinux
RHEL/CentOS 9 / Rocky Linux 9 / AlmaLinux 9
sudo dnf install -y python3-libselinux
Not sure which Python version Ansible is targeting? Run this from your control node:
ansible all -i inventory.ini -m setup -a 'filter=ansible_python_version'
Fix 2: Add a pre-task in your playbook (best for teams)
Manually SSHing into 20 hosts is painful. Let Ansible handle the install itself via a pre-task:
---
- name: Deploy web application
hosts: all
become: yes
pre_tasks:
- name: Ensure SELinux Python bindings are installed (RHEL/CentOS 7)
yum:
name: libselinux-python
state: present
when:
- ansible_os_family == "RedHat"
- ansible_distribution_major_version | int == 7
- name: Ensure SELinux Python bindings are installed (RHEL/CentOS 8+)
dnf:
name: python3-libselinux
state: present
when:
- ansible_os_family == "RedHat"
- ansible_distribution_major_version | int >= 8
tasks:
- name: Your actual tasks here
...
There's a catch. This is a classic chicken-and-egg situation: Ansible needs the bindings to run, but you're installing them through Ansible. It often works anyway because yum/dnf modules don't always trigger the SELinux check before the install completes. If it still fails during the pre-task, jump to Fix 3.
Fix 3: Use raw to bootstrap the package
The raw module sidesteps Python completely โ it runs a plain SSH command. No Python, no SELinux check, no chicken-and-egg problem.
---
- name: Bootstrap SELinux bindings before anything else
hosts: all
become: yes
gather_facts: no # critical: fact gathering triggers the SELinux check
tasks:
- name: Install libselinux Python bindings (raw, no Python required)
raw: |
if command -v dnf &>/dev/null; then
dnf install -y python3-libselinux
else
yum install -y libselinux-python
fi
- name: Main playbook
hosts: all
become: yes
tasks:
- name: Your actual tasks
...
gather_facts: no is non-negotiable here. Fact gathering is exactly what triggers the SELinux binding check โ skip it until after the bootstrap play has run.
Fix 4: Disable the SELinux check (not for production)
Need something running in the next five minutes and you'll clean it up properly later? You can tell Ansible to skip the SELinux check entirely.
In your inventory:
# inventory.ini
[webservers]
web01 ansible_selinux_enabled=false
Or as a playbook variable:
vars:
ansible_selinux_enabled: false
With this set, Ansible won't manage SELinux file contexts at all. On a permissive system that's probably fine short-term. On an enforcing system, you're asking for silent misconfigurations โ files getting the wrong labels, services failing in confusing ways. Don't leave this in place.
Dealing with the Python 2 vs Python 3 mismatch
On CentOS 8+, you might install both packages and still see the error. This happens when Ansible is running Python 3 but only the Python 2 package (libselinux-python) is present โ they don't cross-pollinate.
Find out which interpreter Ansible is actually using:
ansible web01 -i inventory.ini -m debug -a 'var=ansible_python_interpreter'
/usr/bin/python โ install libselinux-python. /usr/bin/python3 โ install python3-libselinux. Simple rule.
To stop guessing, pin the interpreter explicitly in inventory:
# RHEL 8+
[webservers]
web01 ansible_python_interpreter=/usr/bin/python3
# RHEL 7
web01 ansible_python_interpreter=/usr/bin/python
Verify the fix
Three quick checks to confirm everything is working before re-running your playbook.
- Confirm the package is installed on the target:
# RHEL 7
rpm -q libselinux-python
# RHEL 8+
rpm -q python3-libselinux
- Ping to verify Ansible can connect and gather facts cleanly:
ansible web01 -i inventory.ini -m ping
- Pull SELinux facts to confirm Ansible sees them:
ansible web01 -i inventory.ini -m setup -a 'filter=ansible_selinux'
Healthy output looks like this:
"ansible_selinux": {
"config_mode": "enforcing",
"mode": "enforcing",
"policyvers": 33,
"status": "enabled",
"type": "targeted"
}
All three pass? Re-run your original playbook. The error won't come back.
Prevention: bake it into your base role
Managing a fleet of 50+ RHEL hosts means this will surface on every new machine you provision. The clean solution: add the SELinux bindings to your base server role so it's always there before any other role runs.
# roles/base/tasks/main.yml
- name: Install SELinux Python bindings
package:
name: "{{ 'python3-libselinux' if ansible_distribution_major_version | int >= 8 else 'libselinux-python' }}"
state: present
when: ansible_os_family == "RedHat"
The package module (not yum or dnf) keeps this forward-compatible โ it picks the right package manager automatically. The ternary expression handles the Python 2/3 name split based on OS version. One task, works across your entire RHEL estate.

