Automating OS Patching Without Breaking Production - SysRoot

How to run unattended upgrades on Linux fleets safely, with reboot windows, kernel handling, and rollback paths that actually work.

Patching is the security control everyone agrees on and nobody operates well

Every security framework, auditor, and threat model lists the same boring control near the top: keep your systems patched. Yet patching is also one of the most common causes of self-inflicted outages. Teams either patch nothing for months because they're afraid of breakage, or they enable fully automatic upgrades and discover one morning that half the fleet rebooted into a broken kernel during business hours.

The gap is not awareness. It's operational design. Patching that survives contact with production is a system, not a cron job. This article walks through how to build automated OS patching for a small-to-mid Linux fleet that closes vulnerabilities quickly without becoming an outage generator.

We'll focus on Debian/Ubuntu (unattended-upgrades) and RHEL-family (dnf-automatic), but the principles apply to any distro.

The two failure modes you're choosing between

Before touching configuration, be honest about the tradeoff you're managing.

Failure mode A: patching too slowly. Known CVEs sit unpatched for weeks. This is the default state of most teams that disabled automation after one bad experience. The mean time to patch (MTTP) drifts into the 30-90 day range, and you're exposed to exploits that have public proof-of-concept code.

Failure mode B: patching too aggressively. Automatic upgrades pull in package changes that restart services, break dependencies, or reboot hosts at the wrong time. A glibc or openssl update silently restarts daemons; a kernel update needs a reboot that doesn't happen, so you accumulate a fleet running old kernels with new userland.

The goal is a middle path: security patches applied automatically and quickly, with reboots and service restarts controlled by you. Decouple "install the package" from "interrupt the workload."

Step one: separate security updates from everything else

The single most important decision is to automate only security-relevant updates, not all available package upgrades. Pulling in every minor version bump is how you get surprise behavior changes.

On Debian/Ubuntu, unattended-upgrades does this via origin matching. The default config restricts to the security archive:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

Leave the general -updates and -backports pockets out of automation. Apply those manually or in a separate, slower cadence.

On RHEL/Rocky/Alma, configure dnf-automatic with:

[commands]
upgrade_type = security
apply_updates = yes

This limits automation to advisories flagged as security errata. Everything else stays in your hands.

Why this matters operationally

Security-only scoping shrinks the blast radius dramatically. Most security updates are small, well-tested patches to specific libraries. The risky changes (major daemon versions, new defaults, removed config options) usually arrive through general updates, which you now control. You get fast MTTP on the things attackers care about and slow, deliberate handling for everything else.

Step two: control reboots, don't let the upgrader own them

Kernel, glibc, and systemd updates frequently require a reboot to take full effect. The dangerous default is to let the upgrade tooling reboot whenever it wants.

unattended-upgrades can reboot automatically:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:30";

This is fine for a single stateless VM. It is a disaster for a fleet, because every host will reboot at 02:30 simultaneously, and a clustered service may lose quorum or take a coordinated outage.

For anything beyond a couple of standalone boxes, set Automatic-Reboot "false" and detect the need for a reboot separately. Debian writes /var/run/reboot-required when a package needs it. RHEL provides needs-restarting -r (returns exit code 1 if a reboot is required).

Then schedule reboots through your orchestration layer (Ansible, a maintenance script, your scheduler) so you can:

  • Reboot in waves, not all at once.
  • Respect cluster membership and drain nodes first.
  • Avoid business hours and known high-traffic windows.
  • Verify a host comes back healthy before moving to the next.

Step three: handle service restarts explicitly

A subtle trap: even without a reboot, a library update like openssl or libssl leaves running processes using the old in-memory version. The package is patched on disk, but nginx, postgres, or your app is still running the vulnerable code until restarted. Auditors and vulnerability scanners will (correctly) flag this.

To find affected processes:

# Debian/Ubuntu
sudo needrestart -b

# RHEL family
sudo needs-restarting

Decide your policy:

  • Restart automatically for low-risk daemons where a quick restart is acceptable.
  • Defer restarts of stateful or latency-sensitive services to your maintenance orchestration.

needrestart can be configured to restart services automatically ($nrconf{restart} = 'a';) or to only list them ('l';). On a fleet, list-only plus orchestrated restarts is safer. A surprise postgres restart in the middle of the day is its own incident.

Step four: stagger across the fleet with rings

Do not apply patches everywhere at once, even security-only ones. Use deployment rings:

  1. Ring 0 — canary (1-2 hosts). Non-critical or internal-facing nodes. Patch first, watch for 24-48 hours.
  2. Ring 1 — staging / non-prod. Confirm nothing in your stack breaks against the new packages.
  3. Ring 2 — production, partial. A subset of production capacity, ideally behind a load balancer so you can shift traffic.
  4. Ring 3 — production, remainder.

The mechanism is simple: tag hosts with their ring and gate automation on the tag. With unattended-upgrades you control this by when the upgrade timer is allowed to run per host group, or by triggering the run from orchestration rather than the built-in timer. The point is that a bad security patch never hits all of production in the same hour.

A realistic scenario: the OpenSSL update that took down TLS

A team runs a dozen Ubuntu app servers behind a load balancer plus three database hosts. They enable unattended-upgrades with the default security origin and Automatic-Reboot "true" at 04:00.

A security update to libssl ships. Overnight, the package installs fine. But needrestart is not configured, so the running application processes keep using the old library. The vulnerability scanner still flags every host the next morning. The security team escalates: "patching is broken."

Meanwhile, a kernel update also landed. At 04:00, all twelve app servers reboot within the same minute. Eleven come back. One hits a fstab entry for a detached volume and hangs in emergency mode at boot — something that had been latent for months and only surfaced on reboot. The load balancer health-checks it out, so there's no outage, but the team is now down 1/12 capacity with no alert specifically tying it to the patch event.

Everything here was preventable:

  • No service-restart handling meant the security goal (running patched code) wasn't actually achieved.
  • Synchronized reboots turned a latent config bug into a near-miss instead of catching it on a single canary.
  • No post-reboot health verification meant a host sat degraded silently.

The fix wasn't "disable automation." It was adding needrestart policy, switching to orchestrated waved reboots, and adding a post-reboot health gate.

Step five: verify, don't assume

Automated patching without verification is just automated hope. After each patch cycle, confirm three things:

  1. The patch actually applied. Log the output of every run. unattended-upgrades writes to /var/log/unattended-upgrades/. Ship these logs centrally so a silent failure (held packages, dpkg lock contention, full disk) is visible.
  2. Running processes use the new code. Re-run needrestart -b / needs-restarting after restarts and confirm a clean state.
  3. The host is healthy after reboot. Check that critical services are up, the host rejoined its cluster/load balancer, and disk/mounts are correct before proceeding to the next ring.

Feed your MTTP metric from real data: time from CVE/erratum publication to confirmed-patched-and-restarted across the fleet. If you can't measure it, you can't claim you're patched.

Step six: have a rollback path

Security patches rarely need rollback, but "rarely" isn't "never," and you don't want to improvise during an incident.

  • Snapshots before risky reboots (kernel upgrades especially) give you a fast revert for VMs. Remember a snapshot is not a backup — it's a short-lived revert point for this specific operation, deleted afterward.
  • Keep old kernels installed. Both apt and dnf retain previous kernels by default; don't aggressively prune them. If a new kernel misbehaves, you can select the prior one from the bootloader.
  • Pin a known-good version when a specific package regresses. On Debian, apt-mark hold; on RHEL, dnf versionlock. Track held packages — a forgotten hold is how a host silently stops receiving security updates and becomes your next finding.

A practical baseline configuration checklist

Use this as the starting policy for a small fleet:

  1. Automate security updates only; handle general updates on a separate, slower cadence.
  2. Set Automatic-Reboot "false" on anything clustered or stateful; orchestrate reboots in waves.
  3. Configure needrestart / needs-restarting to list affected services; restart via orchestration with awareness of state.
  4. Tag hosts into rings (canary → staging → partial prod → full prod) and gate rollout on the tag.
  5. Ship patch logs centrally and alert on failures, not just successes.
  6. Add a post-reboot health gate: service up, cluster rejoined, mounts present, before advancing.
  7. Keep previous kernels; snapshot before kernel reboots where the platform allows.
  8. Track held/locked packages and review them monthly so nothing silently rots.
  9. Measure MTTP from erratum publication to confirmed-patched.
  10. Document the reboot window and rollback steps so on-call doesn't improvise.

The mindset shift

The teams that patch reliably stopped treating it as a yes/no toggle. They split the problem: acquiring the fix (fast, automated, low-risk because it's security-scoped) versus applying the disruptive parts — reboots and restarts — which they own, stagger, and verify.

That separation is what lets you patch quickly without fearing it. You get a short MTTP that satisfies your threat model and your auditors, and you stop accumulating the latent boot-time bombs that make every reboot feel risky. Patching becomes routine, boring, and observable — which is exactly what a security control should be.


Struggling with Infrastructure Security?

You don't have to solve it alone. SysRoot provides senior DevOps and infrastructure expertise without the cost of building a full in-house team. If your team is wrestling with Infrastructure Security, talk to the SysRoot team and we'll help you get it under control.

Published on: June 24, 2026
Tags: patching, infrastructure-security, linux, devops