Linux Server Patching with Ansible

Eran Goldman-Malka · November 17, 2025

Linux patching looks deceptively simple until you have a mixed estate—Ubuntu LTS boxes, RHEL subscriptions, the odd SLES node in a regulated environment—each with different package managers, different security-update semantics, and different opinions on what “reboot required” means. A robust playbook handles all of them without becoming a conditional maze.

Package Manager Differences

Distro Family Manager Ansible Module Security-only flag
Debian / Ubuntu apt ansible.builtin.apt -security pocket via default_release
RHEL / CentOS 8+ dnf ansible.builtin.dnf security: true
RHEL / CentOS 7 yum ansible.builtin.yum security: true
SUSE / SLES zypper community.general.zypper type: patch

Use ansible_os_family and ansible_distribution facts to branch cleanly rather than maintaining separate playbooks per distro.


Security-Only vs Full Upgrade

Debian / Ubuntu — security updates only:

- name: Apply security updates (Debian/Ubuntu)
  ansible.builtin.apt:
    upgrade: dist
    update_cache: true
    default_release: "-security"
  when: ansible_os_family == "Debian"

RHEL / CentOS — security updates only:

- name: Apply security updates (RHEL/CentOS)
  ansible.builtin.dnf:
    name: "*"
    state: latest
    security: true
    update_cache: true
  when: ansible_os_family == "RedHat"

Full upgrade (all packages):

- name: Full upgrade (Debian/Ubuntu)
  ansible.builtin.apt:
    upgrade: dist
    update_cache: true
  when: ansible_os_family == "Debian"

- name: Full upgrade (RHEL/CentOS)
  ansible.builtin.dnf:
    name: "*"
    state: latest
  when: ansible_os_family == "RedHat"

Kernel Patching and Reboot Detection

Kernel updates require a reboot to take effect. Detect this reliably rather than rebooting unconditionally.

Debian / Ubuntu — check /run/reboot-required:

- name: Check if reboot is required (Debian/Ubuntu)
  ansible.builtin.stat:
    path: /run/reboot-required
  register: reboot_required_file
  when: ansible_os_family == "Debian"

- name: Set reboot flag (Debian/Ubuntu)
  ansible.builtin.set_fact:
    reboot_required: ""
  when: ansible_os_family == "Debian"

RHEL / CentOS — use needs-restarting:

- name: Check if reboot is required (RHEL/CentOS)
  ansible.builtin.command: needs-restarting -r
  register: reboot_check
  failed_when: reboot_check.rc not in [0, 1]
  changed_when: false
  when: ansible_os_family == "RedHat"

- name: Set reboot flag (RHEL/CentOS)
  ansible.builtin.set_fact:
    reboot_required: ""
  when: ansible_os_family == "RedHat"

Controlled Reboot with Service Validation

- name: Reboot if required and allowed
  ansible.builtin.reboot:
    reboot_timeout: 600       # wait up to 10 minutes for the host to come back
    pre_reboot_delay: 30      # wait 30s after sending reboot command
    post_reboot_delay: 30     # wait 30s after host returns before continuing
    test_command: uptime      # confirm the host is responsive
  when:
    - reboot_required | bool
    - patch_reboot_allowed | bool   # set per group/host in group_vars

- name: Validate critical services are running post-reboot
  ansible.builtin.service:
    name: ""
    state: started
  loop: ""   # defined in group_vars
  register: service_check

- name: Fail if any service did not recover
  ansible.builtin.fail:
    msg: "Service  is not running after reboot on "
  loop: ""
  when: item.state != "started"

Handling Package Locks and Failures

Debian — held packages:

- name: List held packages before patching
  ansible.builtin.command: apt-mark showhold
  register: held_packages
  changed_when: false

- name: Warn if held packages exist
  ansible.builtin.debug:
    msg: "Held packages on : "
  when: held_packages.stdout != ""

RHEL — excluded packages in dnf.conf:

- name: Show dnf exclusions
  ansible.builtin.command: grep -i exclude /etc/dnf/dnf.conf
  register: dnf_exclusions
  changed_when: false
  failed_when: false

- name: Report exclusions
  ansible.builtin.debug:
    var: dnf_exclusions.stdout_lines

Complete Rolling Patch Workflow

A full playbook combining detection, conditional reboot, and service validation:

---
- name: Linux rolling patch
  hosts: linux_servers
  become: true
  serial: ""
  vars:
    critical_services:
      - nginx
      - sshd

  tasks:
    - name: Update package cache
      ansible.builtin.package:
        update_cache: true
      when: ansible_os_family in ["Debian", "RedHat"]

    - name: Apply security patches
      ansible.builtin.dnf:
        name: "*"
        state: latest
        security: true
      when: ansible_os_family == "RedHat"
      register: patch_result_rhel

    - name: Apply security patches
      ansible.builtin.apt:
        upgrade: dist
        default_release: "-security"
      when: ansible_os_family == "Debian"
      register: patch_result_debian

    - name: Check reboot required (Debian/Ubuntu)
      ansible.builtin.stat:
        path: /run/reboot-required
      register: reboot_file
      when: ansible_os_family == "Debian"

    - name: Check reboot required (RHEL/CentOS)
      ansible.builtin.command: needs-restarting -r
      register: reboot_cmd
      failed_when: reboot_cmd.rc not in [0, 1]
      changed_when: false
      when: ansible_os_family == "RedHat"

    - name: Set unified reboot fact
      ansible.builtin.set_fact:
        reboot_required: >-
          

    - name: Reboot if required
      ansible.builtin.reboot:
        reboot_timeout: 600
        pre_reboot_delay: 30
        post_reboot_delay: 30
      when:
        - reboot_required | bool
        - patch_reboot_allowed | default(true) | bool

    - name: Validate services
      ansible.builtin.service:
        name: ""
        state: started
      loop: ""

Previous: Advanced Ansible Usage for Enterprise Environments

Next in the series: Windows Server Patching with Ansible — WinRM setup, win_updates, update categories, and the security risks of misconfigured Windows remoting.

Twitter, Facebook