CVE-2026-61699: nebula-mesh Certificate Blocklist Never Enforced on Mesh Peers
Blocking a host in nebula-mesh has no effect on the network: the revocation blocklist is never written into any peer's config, so a blocked or compromised host keeps full mesh access until its certifi
The problem
nebula-mesh lets operators revoke a host by adding its certificate fingerprint to a per-CA blocklist. The server computes and ships that blocklist correctly on every poll response.
The agent side was never wired up. `poll()` in `internal/agent/poller.go` applies new cert, CA, and config payloads from the server response but silently discards `updates.Blocklist`. Separately, the config generator (`internal/configgen/marshal.go`) only emits `pki.ca`, `pki.cert`, and `pki.key`, so even a server-rendered `config.yml` cannot carry a `pki.blocklist` key.
The practical result: a blocked, offboarded, or compromised host retains full overlay connectivity to every peer under its CA for up to 30 days (agent cert lifetime) or 365 days (mobile cert lifetime). The UI shows the block as applied; the data plane ignores it entirely.
Proof of concept
A working proof-of-concept for CVE-2026-61699 in github.com/forgekeep/nebula-mesh, with the exact payload below.
# Benign PoC: render a host config with a non-empty blocklist and confirm pki.blocklist is absent.
# From the advisory's internal/configgen/blocklist_poc_test.go (TestPoC_NMESH001):
$ go test ./internal/configgen/ -run TestPoC_NMESH001 -v
=== RUN TestPoC_NMESH001_GeneratedConfigOmitsBlocklist
CONFIRMED: generated config has a pki section but no blocklist key
pki:
ca: /etc/nebula/ca.crt
cert: /etc/nebula/host.crt
key: /etc/nebula/host.key
--- PASS
# Attacker consequence: run stock slackhq/nebula directly with the
# exfiltrated host.key + host.crt after the operator blocks the host.
# Poll returns 403/410 to the agent, but the Nebula daemon never
# receives a blocklist update, so peer handshakes succeed unchanged.
nebula -config /path/to/stolen/config.ymlThe root cause is a missing distribution and application path for the blocklist (CWE-299 / CWE-672). The server calls `GetBlocklistForCA` and sets `has_updates=true`, proving intent, but the agent's `poll()` function has branches only for `CertificatePEM`, `CACertPEM`, `ConfigYAML`, and `RekeyRequired`, with no branch for `Blocklist`.
The config generator's `pkiSection` struct (`marshal.go:42-46`) has only `CA`, `Cert`, and `Key` fields, so `pki.blocklist` can never appear in any rendered YAML regardless of server state. The helper type `internal/pki/blocklist.go` exists but is unreferenced in all non-test code, confirming the feature was stubbed and never completed.
Patch commit `0426e2f` fixes both gaps: it adds a `Blocklist []safeString` field (yaml tag `blocklist,omitempty`) to `pkiSection` and `GeneratorInput`, then wires `updates.Blocklist` through `poll()` so it is folded into the next config write and SIGHUP cycle.
The fix
Upgrade to nebula-mesh v0.7.1 (patch commit `0426e2f224a9b1e2029029bf923c93ed39d21cdb`). If an immediate upgrade is not possible, treat the blocklist as non-functional: rotate (revoke and re-issue under a new CA) rather than block, shorten `DefaultAgentCertDuration` as far as operationally tolerable, and use network-layer controls to isolate compromised hosts while the upgrade is planned.
Related research
- high · 7.7nebula-mesh: Non-Admin SSRF via Unguarded allow_private Webhook Flag
- highCVE-2026-61549CVE-2026-61549: Woodpecker CI Kubernetes Backend Privilege Escalation via serviceAccountName
- high · 7.5CVE-2026-54629CVE-2026-54629: Anyquery Local File Read via Unrestricted SQLite Virtual Table Modules
- highCVE-2026-54448CVE-2026-54448: Trivy Helm Chart Tar Bomb OOM via Unbounded io.ReadAll