DEV Community

Cover image for Remediating 18 OpenSSL CVEs at Scale with Puppet
Jason St-Cyr for puppet

Posted on • Edited on

Remediating 18 OpenSSL CVEs at Scale with Puppet

Written by Paul Reed.

The June 2026 OpenSSL advisory is a big one. 18 vulnerabilities, one rated high severity with remote code execution potential, and a disclosure credited in part to Anthropic's Mythos model working alongside researcher Alex Gaynor. Six of those CVEs trace back to that collaboration.

When an advisory like the OpenSSL one lands, the first question is always the same: where are we exposed? If you run Puppet, you can answer that question across the entire fleet right now, patch it through one mechanism that handles every platform for you, and have it stay patched without anyone watching afterwards. The rest of this article is how.

The Vulnerability: What CVE-2026-45447 Actually Does

CVE-2026-45447 is a heap use-after-free in PKCS7_verify(). The bug fires when OpenSSL processes a PKCS#7 or S/MIME signed message where the SignedData.digestAlgorithms field is an empty ASN.1 SET.

When OpenSSL encounters this condition, OpenSSL frees a BIO object that was passed in by the calling application and is still expected to be valid. The calling application then uses the freed pointer. Depending on heap layout, that results in heap corruption, a process crash, or with a controlled heap grooming primitive, code execution.

Affected ranges:

  • OpenSSL 3.0.x through 3.3.x (patch to 3.5.1)
  • OpenSSL 1.1.1x (patch to corresponding 1.1.1 update)

The other 17 CVEs in the advisory cover authentication bypass via forged certificates (moderate, roughly a 1-in-256 success rate), ciphertext forgery, private key recovery, root CA replacement, and several DoS vectors. None are trivial in regulated environments.

Step 1: Query Your Actual Exposure

Knowing where you're exposed is where Puppet earns its keep on day zero. There's no scanner to stand up and no spreadsheet to chase round the teams. The data is already sitting in PuppetDB.

Package inventory is fed by Puppet's resource abstraction layer, the same machinery behind puppet resource package. It enumerates every package provider Puppet Enterprise supports. This means package inventory sees well beyond the system package manager: OS packages across apt, dnf/yum and zypper, and language managers like gem and pip alongside them. One query, every node:

puppet query 'package_inventory[certname, package_name, version, provider] {
  package_name ~ "(?i)openssl$|libssl$|libcrypto$"
  and
  version ~ "^(3\\.[0-3]\\.|1\\.[0-1]\\.)"
}'
Enter fullscreen mode Exit fullscreen mode

Run the query against a live environment to review the results. On one fleet:

  • system libraries across multiple providers (libopenssl3/libopenssl1_1 via zypper, openssl and openssl-libs via dnf/yum, openssl via apt)
  • the Ruby openssl gem at several versions
  • openssl via puppet_gem on every agent node, because Puppet's own Ruby ships it
  • pyOpenSSL and python3-openssl via pip and the OS package manager

Most tools miss the bulk of that, because they only ever look at the system package manager. Here, anything a package manager put on the box is in scope, system libraries and language bindings together.

Scope the query to an environment by filtering against the inventory endpoint:

puppet query 'package_inventory[certname, package_name, version, provider] {
  package_name ~ "(?i)openssl$|libssl$|libcrypto$"
  and
  version ~ "^(3\\.[0-3]\\.|1\\.[0-1]\\.)"
  and
  certname in inventory[certname] { environment = "production" }
}'
Enter fullscreen mode Exit fullscreen mode

Step 2: Patch It (recommended for the actual remediation)

You don't hand-write a package resource per platform, and you shouldn't. The package name varies (openssl-libs on RHEL, libssl3 on Debian, libopenssl3 on SUSE), the versions differ again, and the openssl CLI isn't even the vulnerable piece, the runtime library is. Let the tooling that already models your estate carry that.

Use the patching framework: pe_patch on Puppet Enterprise, os_patching for open source and Bolt. Classify the class and each node reports its pending updates, including which are security updates. You patch through the PE console or a task, scoped to security updates only if you want the change tight.

The OS package manager applies the vendor's security update, so the correct library package and version are chosen per platform without you encoding any of it. Reboots, update ordering, and patch and blackout windows are the framework's job. A box that can't be touched in business hours is a blackout window in config, not a workaround.

The exposure query doubles as your target list. The orchestrator takes PQL directly with -q, so there's no glue script to write. You hand the orchestrator the query and let it resolve the nodes:

puppet task run pe_patch::patch_server -q 'package_inventory[certname]{
  package_name ~ "(?i)openssl$|libssl$|libcrypto$"
  and version ~ "^(3\\.[0-3]\\.|1\\.[0-1]\\.)"
}'
Enter fullscreen mode Exit fullscreen mode

That's pe_patch::patch_server on Puppet Enterprise, os_patching::patch_server for open source and Bolt. The orchestration runs the task against exactly the nodes the query returned and nothing else. Add security_only=true to keep the run tight, so a node that's already current is a no-op. If you'd rather not touch the command line, wire the same query into a node group in the PE console and drive it from there.

Step 3: Enforce It In Code

If you'd rather declare the state and have Puppet hold the state on every run, use the openssl module's class. The module knows the package names per platform:

class { 'openssl':
  package_ensure         => latest,
  ca_certificates_ensure => latest,
}
Enter fullscreen mode Exit fullscreen mode

Using latest keeps the library current; set package_ensure to a specific version from Hiera if you want a pinned, auditable rollout.

This enforcement step is something a one-off script and a scanner both miss. A patch run fixes the issue once; declaring the state is what makes it stay fixed. The agent runs on a schedule, every 30 minutes by default, and enforces desired state each time. So when a node drifts back to a vulnerable version, a VM reprovisioned off a stale image, say, or a change someone made by hand and forgot, the next run quietly puts the desired state back. A scanner would notice that regression on its next sweep and open you a ticket. Enforcement just doesn't let the gap stay open that long.

The two paths aren't either/or. The patching framework is the quicker way to clear the initial backlog; enforcement is what keeps the backlog cleared. Run both and the framework does the first sweep while the module holds the line from then on.

Step 4: Restart Processes That Link the Library

Patching the package is only half the job. A running process keeps the old .so mapped until the process restarts, and a library swap under a live OpenSSL is exactly the case where that bites.

If you patched with the framework, it already tracks this for you. pe_patch reports the processes that require a restart in the node's fact, so you don't go hunting:

"reboots" : {
    "app_restart_required" : true,
    "apps_needing_restart" : {
        "1" : "/usr/lib/systemd/systemd --switched-root --system --deserialize 31",
        "586" : "/usr/lib/systemd/systemd-journald",
        "601" : "/usr/lib/systemd/systemd-udevd",
        "657" : "/sbin/auditd",
        "691" : "/usr/bin/python3 -s /usr/sbin/firewalld --nofork --nopid",
        "694" : "/usr/sbin/rngd -f --fill-watermark=0 -x pkcs11 -x nist -x qrypt -x namedpipe -x jitter -D daemon:daemon",
        "696" : "/usr/lib/systemd/systemd-logind",
        "766" : "/usr/sbin/NetworkManager --no-daemon",
        "863" : "sshd: /usr/sbin/sshd -D [listener] 0 of 10-60 startups"
    },
    "reboot_required" : true
},
Enter fullscreen mode Exit fullscreen mode

apps_needing_restart is the list of processes still mapping a file that's been replaced underneath them, and reboot_required flags when a restart of individual services won't cut it. The patch run acts on these according to its reboot policy, so the same job that applies the update also clears the stale library, or tells you precisely which nodes still need a bounce. That's the manual lsof check in the next section, done for the whole fleet as a fact.

If you went the module route instead, chain the dependent services onto the class so they refresh when it changes the library:

Class['openssl'] ~> Service['nginx']
Enter fullscreen mode Exit fullscreen mode

Chaining on the class rather than a Package['...'] title means you don't have to know the package name the module picks per platform. The refresh fires only when something actually changes, so steady-state runs leave your services alone.

A note on the language-level copies inventory turned up. Where a gem or pyOpenSSL links the system library, patching libssl/libcrypto fixes the underlying crypto and these processes will pick up the patch on restart. The few that statically bundle their own copy get updated through their own toolchain (gem update, pip). The OS-packaged bindings ride along with the patch run either way.

Step 5: Verify the Process Has Reloaded the Library

The reboots fact above is your fleet-wide answer. When you want to confirm a single box by hand, or you're working somewhere the fact isn't available, lsof against the actual PID tells you what that process has mapped:

sudo lsof -p $(pgrep nginx | head -1) | grep libssl
Enter fullscreen mode Exit fullscreen mode

If the lsof query still shows the old path after the run, the restart didn't fire. Check the agent log or the patch run's reboot status. The openssl CLI version won't help here, the version says nothing about what a long-running daemon has mapped.

Step 6: Confirm Convergence

Re-run the inventory query to verify closure. This query doubles as audit evidence:

puppet query 'package_inventory[certname, package_name, version, provider] {
  package_name ~ "(?i)openssl$|libssl$|libcrypto$"
}'
Enter fullscreen mode Exit fullscreen mode

Anything still on an affected version, across any provider, is your remaining work. Puppet Enterprise users can pull the same data from the compliance dashboard for audit review.

The Point Isn't OpenSSL

OpenSSL is this month's fire drill. Next month it's something else, and the one after that hasn't been disclosed yet. None of the steps above were really about OpenSSL. They were about having a tool already in place that answers the questions every advisory asks, before the advisory lands.

Look back at what each step actually was:

  • The exposure query was inventory: what have I got, and where, as of the last check-in.
  • The patch task, scoped from that same query, was remediation. The reboots fact was reporting, telling you what's still exposed and what needs a bounce.
  • Enforcement was the bit that holds the line afterwards, so a reprovisioned box can't slip back in unnoticed.

That's the whole vulnerability-response loop, and the worst time to start building it is the morning a CVE lands with the clock already running.

That's the case for putting the capability in now, while nothing's on fire. The next disclosure becomes a query and a patch run instead of a fortnight of spreadsheets and change tickets. The same loop applies well beyond library upgrades, too. We recently showed how to deal with dirty frag and copy-fail with Puppet, which walks the same detect-mitigate-remediate pattern on a different class of problem.

Using Puppet provides a consistent way to address OpenSSL vulnerabilities across environments. It'll still be set up and ready when the next vulnerability lands.

🤖 AI Disclosure

This article has been reviewed by a human expert in the subject matter and all code samples have been reviewed by Puppet technical experts. Initial structural content has been generated by Claude AI tools to assist with editing for clarity, structure, grammar, and maintaining brand voice, and then passed through human review.

Top comments (2)

Collapse
 
sloan profile image
Sloan the DEV Moderator

Hey, this article appears to have been generated with the assistance of ChatGPT or possibly some other AI tool.

We allow our community members to use AI assistance when writing articles as long as they abide by our guidelines. Please review the guidelines and edit your post to add a disclaimer.

Failure to follow these guidelines could result in DEV admin lowering the score of your post, making it less visible to the rest of the community. Or, if upon review we find this post to be particularly harmful, we may decide to unpublish it completely.

We hope you understand and take care to follow our guidelines going forward!

Collapse
 
jasonstcyr profile image
Jason St-Cyr puppet

Thanks for the heads up! I'll check with the original author on what tools were used to create the content and update appropriately.