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…
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
- high · 7.7CVE-2026-54910CVE-2026-54910: FileBrowser Quantum Path Traversal in Subtitle Handler
- critical · 9.6CVE-2026-54725CVE-2026-54725: vault-secrets-webhook Annotation SSRF and ServiceAccount Token Theft
- critical · 9.9CVE-2026-54680CVE-2026-54680: kube-logging/logging-operator Fluentd Configuration Injection RCE