Introduction

Red Hat Enterprise Linux 10 ships with Python 3.12 as the system Python, updated package names, and systemd/firewalld changes that affect Ansible playbooks. If you're managing RHEL 9 hosts and planning to add RHEL 10, this guide covers what changed, what breaks, and how to update your playbooks for both versions.

What Changed in RHEL 10

AreaRHEL 9RHEL 10
Python3.9 (default), 3.11/3.12 optional3.12 (default)
Kernel5.146.12
systemd252256
Firewalld1.x2.x
Podman4.x5.x
OpenSSL3.03.2
GCC1114
ansible-core (AppStream)2.142.17
Network configNetworkManagerNetworkManager (nmstate default)
Default editorvivi (nano available)
Crypto policyDEFAULTDEFAULT (stricter TLS defaults)
SupportUntil 2032Until 2035

Install Ansible on RHEL 10

As Controller (Run Ansible From RHEL 10)

# ansible-core from AppStream
sudo dnf install ansible-core

# Full Ansible with collections
sudo dnf install ansible

# Verify
ansible --version
# ansible [core 2.17.x]
# python version = 3.12.x

As Managed Node (Target Host)

RHEL 10 targets need Python 3.12 (installed by default):

# Verify Python is available
python3 --version
# Python 3.12.x

# Install minimal dependencies for Ansible
sudo dnf install python3 python3-libselinux python3-dnf

Inventory Configuration

[rhel10]
rhel10-web01 ansible_host=10.0.0.11
rhel10-web02 ansible_host=10.0.0.12

[rhel10:vars]
ansible_python_interpreter=/usr/bin/python3

Python Changes

Python 3.12 as Default

RHEL 10's system Python is 3.12. Key impacts for Ansible:

# ansible.cfg or inventory — usually auto-detected, but be explicit
[defaults]
interpreter_python = auto_silent

# Or per host
# ansible_python_interpreter = /usr/bin/python3

Removed Python 2

Python 2 is completely removed. If your playbooks or custom modules use Python 2 syntax, they will fail:

# ❌ Python 2 syntax — fails on RHEL 10
print "hello"
except Exception, e:

# ✅ Python 3 syntax
print("hello")
except Exception as e:

pip and Virtual Environments

- name: Create Python virtual environment on RHEL 10
  ansible.builtin.pip:
    name:
      - flask
      - gunicorn
    virtualenv: /opt/myapp/venv
    virtualenv_command: python3 -m venv

Package Name Changes

Some packages have been renamed or split:

# ❌ May not work on RHEL 10
- ansible.builtin.dnf:
    name: python3-pip
    state: present

# ✅ Works on both RHEL 9 and 10
- ansible.builtin.package:
    name: python3-pip
    state: present

Use ansible.builtin.package for Portability

# Works across RHEL 8, 9, and 10
- name: Install common packages
  ansible.builtin.package:
    name:
      - vim-enhanced
      - tmux
      - curl
      - git
      - python3-pip
    state: present

RHEL 10-Specific Packages

- name: Install RHEL 10 specific packages
  ansible.builtin.dnf:
    name:
      - python3.12-pip
      - container-tools
      - nmstate
    state: present
  when: ansible_distribution_major_version == '10'

Firewalld 2.x Changes

RHEL 10 ships firewalld 2.x with policy-based filtering:

# Works on both versions
- name: Allow HTTPS
  ansible.posix.firewalld:
    service: https
    permanent: true
    state: enabled
    immediate: true

# RHEL 10 — new policy objects
- name: Configure firewall policy
  ansible.posix.firewalld:
    policy: allow-web-traffic
    rich_rule: 'rule family="ipv4" source address="10.0.0.0/8" service name="https" accept'
    permanent: true
    state: enabled
  when: ansible_distribution_major_version == '10'

systemd 256 Changes

# Credentials directory (new in systemd 256)
- name: Deploy service with credentials
  ansible.builtin.copy:
    dest: /etc/systemd/system/myapp.service
    content: |
      [Unit]
      Description=My Application
      After=network.target

      [Service]
      Type=notify
      ExecStart=/opt/myapp/bin/server
      # New: systemd credentials
      LoadCredential=db-password:/etc/myapp/db-password
      # New: resource control improvements
      MemoryMax=2G
      CPUWeight=100

      [Install]
      WantedBy=multi-user.target

- name: Reload and start
  ansible.builtin.systemd_service:
    name: myapp
    state: started
    enabled: true
    daemon_reload: true

Crypto Policy Changes

RHEL 10 has stricter default crypto policies:

# Check current policy
- name: Get crypto policy
  ansible.builtin.command: update-crypto-policies --show
  register: crypto_policy
  changed_when: false

# Set policy if needed (e.g., for legacy compatibility)
- name: Set crypto policy
  ansible.builtin.command: update-crypto-policies --set DEFAULT:SHA1
  when: crypto_policy.stdout != 'DEFAULT:SHA1'
  notify: reboot

Migration Playbook: RHEL 9 → RHEL 10

---
- name: Prepare for RHEL 10 upgrade
  hosts: rhel9_servers
  become: true
  tasks:
    - name: Verify current RHEL version
      ansible.builtin.assert:
        that:
          - ansible_distribution == 'RedHat'
          - ansible_distribution_major_version == '9'

    - name: Update all RHEL 9 packages
      ansible.builtin.dnf:
        name: '*'
        state: latest
        update_cache: true

    - name: Install leapp upgrade tool
      ansible.builtin.dnf:
        name:
          - leapp-upgrade
        state: present

    - name: Run leapp preupgrade check
      ansible.builtin.command: leapp preupgrade
      register: preupgrade
      failed_when: false
      changed_when: false

    - name: Display preupgrade report
      ansible.builtin.debug:
        var: preupgrade.stdout_lines

    - name: Check for inhibitors
      ansible.builtin.assert:
        that: "'inhibitor' not in preupgrade.stdout"
        fail_msg: "Upgrade inhibitors found — review /var/log/leapp/leapp-report.txt"

Multi-Version Playbooks

Support both RHEL 9 and 10 in one playbook:

---
- name: Configure web server (RHEL 9 + 10)
  hosts: webservers
  become: true
  tasks:
    - name: Install packages
      ansible.builtin.dnf:
        name:
          - nginx
          - python3-pip
          - firewalld
        state: present

    - name: Configure firewall
      ansible.posix.firewalld:
        service: "{{ item }}"
        permanent: true
        state: enabled
        immediate: true
      loop:
        - http
        - https

    - name: RHEL 10 specific — configure nmstate
      ansible.builtin.dnf:
        name: nmstate
        state: present
      when: ansible_distribution_major_version == '10'

    - name: Start services
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
        enabled: true
      loop:
        - nginx
        - firewalld

Testing with Molecule

# molecule/default/molecule.yml
---
driver:
  name: podman
platforms:
  - name: rhel9
    image: registry.access.redhat.com/ubi9/ubi:latest
    command: /usr/sbin/init
    privileged: true
  - name: rhel10
    image: registry.access.redhat.com/ubi10/ubi:latest
    command: /usr/sbin/init
    privileged: true

Troubleshooting

"No module named 'dnf'"

# RHEL 10 uses dnf5 — ensure python3-dnf is installed
- name: Bootstrap dnf Python bindings
  ansible.builtin.raw: dnf install -y python3-dnf
  when: ansible_distribution_major_version == '10'

SELinux Context Errors

# Ensure SELinux Python bindings are installed
- name: Install SELinux bindings
  ansible.builtin.dnf:
    name: python3-libselinux
    state: present

SSH Key Exchange Failures

RHEL 10's stricter crypto policy may reject older SSH key types:

# Use ed25519 keys (recommended)
- name: Deploy SSH key
  ansible.posix.authorized_key:
    user: ansible
    key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"

Conclusion

RHEL 10 brings Python 3.12 as default, firewalld 2.x, systemd 256, and stricter crypto policies. For Ansible, the main change is ensuring ansible_python_interpreter points to Python 3.12 and updating any Python 2 syntax in custom modules. Use ansible.builtin.package for cross-version compatibility and when: ansible_distribution_major_version conditionals for version-specific tasks. Test with Molecule against both RHEL 9 and 10 containers before rolling out.