Skip to main content

CloudNativePG's First CVE Is a 9.4: How Scraping Metrics Handed Out Postgres Superuser

10 min readDora NodaDora Noda
Share
On this page

CloudNativePG is the CNCF Sandbox project that's become the default answer to "how do I run Postgres on Kubernetes without paying a managed-database vendor" — and on May 8, 2026, it shipped its first CVE ever. CVE-2026-44477 is rated Critical, CVSS v4.0 score 9.4, and the exploit chain is almost insultingly simple: any low-privileged database user your application already trusts with a login could turn CNPG's own Prometheus metrics exporter into a path to full PostgreSQL superuser, then to arbitrary OS command execution inside the primary pod. No misconfiguration required — the default metrics setup on a stock CNPG install was enough.

Fixed in v1.29.1 and v1.28.3, the release also quietly closed a second, unrelated bug in the failover path that can lose committed writes during a network partition — no CVE attached to that one, but on a self-hosted Postgres fleet it's arguably the scarier of the two. Both are the same lesson from opposite directions: running "the CNCF-standard operator" instead of a managed vendor's black box buys you portability and buys you the vendor's old job too — reading every advisory, understanding what it actually touches, and patching on your own clock.

How SET ROLE Failed to Demote a Superuser

CNPG's metrics exporter needs to query internal Postgres statistics to expose them to Prometheus, so it opens a connection over the pod-local Unix socket. Vulnerable versions authenticated that connection as postgres — full superuser — and then ran:

sql
SET ROLE pg_monitor;

The intent was to scope the scrape session down to pg_monitor, PostgreSQL's built-in read-only monitoring role. The bug is a one-line misunderstanding of Postgres's own privilege model: SET ROLE only changes current_user, the identity permission checks evaluate against for the rest of the session. It does not touch session_user, which stays postgres for as long as the connection lives. Session_user is exactly what a later RESET ROLE restores.

That means any SQL statement that runs inside the scrape session — including a metric query the exporter itself was told to execute — can simply do this:

sql
RESET ROLE;
COPY (SELECT '') TO PROGRAM 'id > /tmp/pwned';

RESET ROLE recovers full superuser. COPY ... TO PROGRAM spawns an arbitrary OS subprocess as the postgres Linux user inside the primary pod. Postgres's READ ONLY transaction flag — which some CNPG monitoring configs set defensively — doesn't stop this either; it blocks writes to database state, not the exporter from shelling out to the host. The whole chain runs inside a single metrics scrape.

Two Ways In, and One of Them Needs No Configuration at All

What makes CVE-2026-44477 worse than a typical "if you wrote a risky custom query" advisory is that there were two independent ways to trigger it, and only one of them required the cluster operator to have done anything unusual:

Path 1 — custom metric queries. CNPG lets operators define custom Prometheus metric queries against arbitrary SQL. Any query that references an unqualified table or function name (no explicit schema prefix) is exploitable: a low-privileged user who owns a schema in that database can create a same-named "shadow" object — a view or function — that Postgres's search path resolves instead of the intended system catalog object. The next scrape executes the attacker's object inside the superuser-descended exporter session. On a 15–30 second Prometheus scrape interval, that's the exploit window.

Path 2 — the default config, unmodified. CNPG ships a built-in pg_extensions metric out of the box. Its query contains an unqualified call to current_database(), and it runs against every database CNPG is configured to scrape — including target_databases: '*' setups, which are common. Because the call is unqualified, the same shadow-object trick works, and it works from CNPG's default metric set, with zero custom monitoring configuration. Any non-superuser role that owns a user database — including app, the role CNPG creates by default for every new cluster — could trigger the full escalation chain. If you deployed CNPG with defaults and let your application connect as anything other than the Postgres superuser, path 2 applied to you.

That second path is the detail that turns this from "an advisory about a monitoring feature power users might misuse" into "an advisory about the box CNPG ships out of the crate."

The Failover Bug Nobody Mentions When They Say "Critical CVE Fix"

The same 1.29.1/1.28.3 releases also fixed something with no CVE number, no CVSS score, and arguably more direct blast radius on a production database: a label bug that could silently lose committed writes during a network partition.

Here's the mechanic, from PR #10409. When CNPG's operator triggers a failover, the old primary's pod keeps its cnpg.io/instanceRole=primary label until a ReconcileMetadata pass runs — and that pass is skipped for the entire duration of the failover window. Because Kubernetes Services route by label selector, the cluster's -rw (read-write) Service kept the demoted primary as a valid endpoint the whole time failover was in progress.

Now picture the actual trigger condition: a transient network partition knocks the primary out of contact, the operator starts failing it over — and the old primary reconnects before the window closes. Replicas still routing through the stale -rw Service label can reconnect to it, the old primary can satisfy sync-replication quorum against them, and it keeps accepting writes it has no business accepting. When the new primary is finally confirmed and pg_rewind reconciles the timeline, those writes on the old primary are the ones that get discarded — gone, with no error surfaced to the application that thought they'd committed.

The fix adds a third instance-role label value, unhealthy, applied to the old primary the instant failover starts — before ReconcileMetadata would otherwise run. Neither the -rw nor -ro Service selector matches unhealthy, so the demoted pod is pulled out of all Service traffic immediately, closing the exact window the split-brain-adjacent write loss depended on. Two related fixes shipped alongside it: #10448 makes failover trigger correctly when a primary's pod goes fully unready (previously some unreachability patterns didn't trigger failover at all), and #10445 stops spurious failovers caused by transient HTTP health-check failures that weren't real outages.

None of that is a privilege-escalation story. It's a "your database quietly lost data during exactly the kind of infrastructure blip that self-hosted fleets on commodity hardware see more often than a managed vendor's redundant network fabric does" story — which is precisely why bundling it into the same release as a flashy CVSS-9.4 CVE was so easy to scroll past.

Are You Affected? A Fleet Audit

Run this against every cluster running CNPG, across every namespace and every tenant:

1. Find every CNPG operator version in the fleet:

bash
kubectl get deployment -A -l app.kubernetes.io/name=cloudnative-pg \
  -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.spec.template.spec.containers[0].image}{"\n"}{end}'

Vulnerable ranges for CVE-2026-44477: < 1.28.3 and >= 1.29.0, < 1.29.1. Both the 1.28.x and 1.29.x supported branches needed a patch — this wasn't a "just bump a minor version" situation on either track.

2. Check whether metrics scraping is even exposed to untrusted tenants. If Prometheus (or any scrape target) can reach the CNPG metrics port on a cluster where application-layer users aren't fully trusted — the normal case on any multi-tenant PaaS — you were exposed via Path 2 regardless of custom configuration:

bash
kubectl get cluster -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{.spec.monitoring.customQueriesConfigMap}{"\n"}{end}'

3. After patching, audit custom metric queries for unqualified identifiers — the patch fixes the shipped default query, but any custom query your team wrote inherits the same class of bug if it doesn't schema-qualify its references (pg_catalog.current_database() instead of bare current_database()).

The Fix, and a Gotcha That Bites Replica Clusters

Versions 1.29.1 and 1.28.3 stop authenticating the metrics exporter as postgres entirely. Instead, they create a dedicated cnpg_metrics_exporter role with exactly pg_monitor privileges — no superuser identity to RESET ROLE back into, because there isn't one on the connection anymore. The default pg_extensions query was also fixed to schema-qualify its catalog references.

If you can't upgrade immediately, three mitigations reduce exposure without eliminating it: schema-qualify every identifier in any custom metric query you run, restrict target_databases away from '*' to only the databases that actually need scraping, and make sure only trusted roles can create objects in schemas that a monitoring query might implicitly search.

One upgrade-order detail that's easy to get wrong on a multi-cluster setup: the new cnpg_metrics_exporter role is created on the source primary and replicates downstream through normal streaming replication — it isn't independently provisioned on replica clusters. If you run CNPG replica clusters (cross-region DR, read replicas fed from a source cluster), upgrade the source primary cluster first. A replica cluster patched ahead of its source will have its metrics exporter looking for a role that doesn't exist yet, breaking scraping until the source catches up.

What "CNCF-Standard" Actually Buys — and Costs

The reason CNPG is the default choice for self-hosted Postgres on Kubernetes isn't marketing — it's a real CNCF Sandbox project, actively maintained, and this is the pattern you want: a privilege-escalation CVE landed, a security response happened fast, and the fix wasn't a band-aid — it redesigned the exporter's authentication model instead of patching around the symptom. That's a maturity signal, not a red flag.

But "CNCF-standard" doesn't mean "someone else's job to track." A managed Postgres vendor absorbs advisories like this into a control plane you never see — your database gets patched on their schedule, sometimes before you'd have noticed the CVE existed. Choosing CNPG on a self-hosted fleet — which is exactly the trade a platform like Bex.co makes, running CNPG as the default managed-Postgres layer on Cluster-API-provisioned hardware instead of reselling a vendor's managed database — means that transparency and portability come with the advisory-tracking obligation attached. There's no support contract quietly patching your cluster overnight; there's an operator, a release feed, and whoever on your team is subscribed to it.

That's not a knock on self-hosting — it's the actual shape of the trade, and CVE-2026-44477 is as concrete an example as you'll get: a nine-day gap between "the fix shipped" and "most fleets running CNPG had actually applied it" is nine days of a default-config privilege-escalation path sitting open on infrastructure nobody but you is watching.

Going Forward

CNPG's first CVE debut is a reasonable one to grade the project on, and it holds up: real severity, honest CVSS scoring, a structural fix instead of a patch-the-symptom band-aid, and a same-release fix for an unrelated data-safety bug that got less attention than it deserved. The practical takeaway for anyone running it is boring but non-optional — subscribe to the CNPG security advisories, check your version against the vulnerable ranges above today, and if you run replica clusters, upgrade the source primary first. "Self-hosted, not managed" was never a promise that nobody has to read the changelog. It's a promise that the changelog is now unambiguously your job.

Bex.co runs CloudNativePG as the default managed-Postgres layer for apps deployed with a git push — patched, monitored, and upgraded as part of the platform instead of one more operator your team has to subscribe to advisories for on its own. Star the repo on GitHub or deploy your first app today.

Sources:

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex