Skip to main content
RunBook Academy

AnsibleXL · Patch and Reboot ManagementPatch and Reboot Management

Patching RHEL, Rocky and Alma

Advanced⏱ ~26 minansible-playbookansible-doc

What you'll learn

  • Apply a security-only patch set and explain what its dependency handling actually does
  • Choose between ansible.builtin.dnf and ansible.builtin.dnf5 and know what use_backend selects
  • Scope a transaction with exclude, enablerepo and disablerepo without hiding a security update
  • Use download_only to stage packages before a window and apply them inside a shorter one

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

The RHEL family gives you something the Debian family does not: package updates carry advisory metadata, so the package manager can be asked for “only the security updates” and answer meaningfully.

That capability is the reason a lot of organisations patch RHEL-family hosts on a different cadence from their Debian-family ones, and it comes with a set of semantics that are easy to state wrongly. security: true does not mean “install only packages flagged as security fixes”. What it actually means is worth getting right, because the gap between the two readings is a set of packages that moved when you thought nothing would.

Which module is running

There are two modules, and on a modern host you may not be executing the one you named.

ansible.builtin.dnf drives the dnf-4 Python API and needs python3-dnf. ansible.builtin.dnf5 drives dnf-5 and needs python3-libdnf5. They are separate modules, not aliases, and the dnf5 documentation carries an explicit warning that dnf-5 is still under development and that not every option the dnf module provides is implemented in dnf5.

use_backend on the dnf module selects between them:

use_backendEffect
auto (default)select from the ansible_facts.pkg_mgr fact
dnf, dnf4force ansible.builtin.dnf
dnf5force ansible.builtin.dnf5
yum, yum4compatibility aliases

Another asymmetry worth carrying: lock_timeout defaults to 30 seconds on ansible.builtin.dnf, and on ansible.builtin.dnf5 the documentation records it as currently a no-op, because dnf-5 does not expose an option to configure it. A play that raises lock_timeout to survive a competing transaction gets that behaviour on dnf-4 hosts and not on dnf-5 ones.

Security-only patching, and what it really installs

Service impact possiblea security-only fleet patch
- name: Apply security updates only
ansible.builtin.dnf:
  name: '*'
  state: latest
  security: true
  lock_timeout: 300
register: dnf_security

- name: Record the transaction, for the audit trail
ansible.builtin.debug:
  var: dnf_security.results
when: dnf_security.changed

Three things about that task.

security: true only has an effect with state: latest. The documentation states the condition explicitly. With state: present it does nothing, and the task will happily report ok while applying no security updates at all.

name: '*' is what makes it a fleet patch. The documentation notes that with state: latest, name can be '*', which means run dnf -y update. Without it you are filtering an empty set.

The dependency handling is upgrade-minimal semantics. This is the part that is usually stated wrongly. From the module documentation: “If set to true, and state=latest then only installs updates that have been marked security related. Note that, similar to dnf upgrade-minimal, this filter applies to dependencies as well.”

upgrade-minimal means dnf resolves to the lowest version that carries the fix, rather than the newest available. So a security-only run does not freeze your dependency tree — it moves dependencies, just conservatively, to the minimum versions the advisories require.

bugfix: true works identically for bugfix-flagged advisories, and the two can be combined. Both default to false.

Verify the repositories actually publish advisories

The security filter is only as good as the updateinfo metadata in the repositories the host is configured with. A repository that ships packages but no advisory metadata contributes nothing to a security-only run, and does so silently: the filter matches no advisories in that repository, so nothing from it is upgraded, and the task reports success.

This bites hardest with third-party and internal repositories, which frequently publish no advisory data at all.

Read-only / Safechecking that the filter has anything to match
- name: List the security advisories dnf can see
ansible.builtin.command:
  argv: [dnf, '-q', 'updateinfo', 'list', '--security']
register: advisories
changed_when: false
failed_when: false

- name: Warn when a host has no visible advisories
ansible.builtin.debug:
  msg: >-
    {{ inventory_hostname }} reports no security advisories. Confirm the
    repositories publish updateinfo metadata before treating a
    security-only patch run as complete.
when: advisories.stdout | trim | length == 0

On dnf-5 the command was renamed: dnf5 advisory list is the current form, with dnf5 updateinfo list retained as a compatibility alias.

Scoping the transaction

Four parameters narrow what a run may touch, and each has a way of going wrong.

update_only: true upgrades a named package only if it is already installed, and installs nothing new. It is the RHEL-family counterpart of apt’s only_upgrade, and it exists for the same reason: a state: latest on an absent package is an install.

Configuration changeupgrade where present, install nowhere
- name: Upgrade the packages we own, where they are installed
ansible.builtin.dnf:
  name:
    - nginx
    - openssl
    - openssh-server
  state: latest
  update_only: true

exclude holds packages back. It is genuinely necessary — a database host that must not take a postgresql minor bump during a routine window is a real requirement — and it is also how a security update quietly stops being applied.

enablerepo / disablerepo scope the transaction to specific repositories for that operation only; the change does not persist beyond the transaction. Both default to an empty list.

The safe pattern is to enable a repository for the one task that needs it rather than leaving it enabled on the host, because a repository enabled permanently on a host is a source of packages nobody reviewed.

allowerasing: true permits dnf to erase installed packages to resolve dependencies. It defaults to false, and that default is the one you want in a patch run. Turning it on is the RHEL-family equivalent of upgrade: full on Debian: it converts “this update cannot be applied” into “this update was applied and something was removed to make room”.

Related: nobest and its inverse best both default to null in ansible-core 2.17 and later, meaning the value comes from the operating system distribution rather than from Ansible. Do not assume either state — if the behaviour matters to you, set it explicitly.

Splitting the transaction across two windows

download_only: true fetches packages without installing them. That turns one long risky operation into two operations with very different risk profiles.

Read-only / Safestage the packages hours before the window
- name: Stage the security updates without installing them
ansible.builtin.dnf:
  name: '*'
  state: latest
  security: true
  download_only: true
  download_dir: /var/cache/dnf/staged
register: staged

- name: Fail the host if nothing could be staged
ansible.builtin.assert:
  that: staged is not failed
  fail_msg: >-
    {{ inventory_hostname }} could not stage its updates. This host is
    excluded from tonight's window rather than discovered mid-run.
Service impact possibleapply from cache inside the window
- name: Apply the staged updates
ansible.builtin.dnf:
  name: '*'
  state: latest
  security: true
  cacheonly: true
  lock_timeout: 300

The staging run is READ-ONLY in the sense that matters: it changes no package, restarts no service, and can be run at 14:00 on a Tuesday against the whole fleet. What it buys you is that the risky window contains only the install, and the two commonest ways a patch window overruns — a slow mirror and a full disk — have both been discovered hours earlier, when someone was awake.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A play sets security: true with state: present on ansible.builtin.dnf and name: "*". What happens?

  2. Q2. What does the dnf module documentation mean when it says the security filter "applies to dependencies as well", similar to dnf upgrade-minimal?

  3. Q3. Which of these are true of ansible.builtin.dnf5 relative to ansible.builtin.dnf on ansible-core 2.21? Select all that apply.

  4. Q4. Because RHEL-family repositories carry advisory metadata, a play using security: true is guaranteed to apply every security update available to the host.

Passing score: 75%. Answers are checked in this browser.