CVE-2026-61699 is a high-severity (CVSS 8.1) CWE-299 vulnerability in github.com/forgekeep/nebula-mesh. A fix is available for github.com/forgekeep/nebula-mesh — see the affected versions and patch details below.
nebula-mesh: Certificate revocation is never enforced at the mesh
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-61699.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
CVE-2026-61699 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 395,249 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
github.com/forgekeep/nebula-meshReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
Summary
nebula-mesh revokes a host by adding its certificate fingerprint to a per-CA blocklist and shipping that list to every other agent on each poll. Slack's Nebula enforces certificate revocation ONLY through the pki.blocklist list in config.yml (no CRL/OCSP). The project's own code states this: internal/pki/durations.go:15 — "Revocation via the blocklist remains the immediate security control."
The server side is fully implemented (computes per-CA blocklist via GetBlocklistForCA, returns it in the agent-updates response, sets has_updates=true when non-empty). The agent side was never implemented:
- The agent decodes the
blocklistJSON field intoUpdatesResponse.Blocklist(internal/agent/poller.go:33) and then DISCARDS it —poll()appliesCertificatePEM,CACertPEM,ConfigYAML, but never referencesupdates.Blocklist(internal/agent/poller.go:300-339). - The config generator has NO field to emit
pki.blocklist—pkiSectionis onlyca/cert/key(internal/configgen/marshal.go:42-46) andGeneratorInputcarries no blocklist (internal/configgen/generator.go:23-52). So even the server-renderedconfig.ymlshipped viaConfigYAMLcannot carry it.
Result: a blocked/offboarded/compromised host's certificate is never rejected by its peers. Its handshakes keep succeeding for the full remaining cert lifetime — up to 30 days for agent hosts (DefaultAgentCertDuration) and 365 days for mobile hosts (DefaultMobileCertDuration). Blocking a host in the UI/API has no effect on the data plane.
Affected components
- Agent drops the blocklist:
internal/agent/poller.go:33(decode target),internal/agent/poller.go:300-339(poll() applies cert/CA/config, never the blocklist). - Generator cannot emit it:
internal/configgen/marshal.go:42-46(pkiSection{CA,Cert,Key}),internal/configgen/generator.go:23-52(GeneratorInputhas no blocklist),internal/api/enroll.go:255-330(renderHostConfig, source of shipped ConfigYAML). - Server correctly produces/ships it (proves intent):
internal/store/sqlite.go:2034(GetBlocklistForCA),internal/api/updates.go:182-191(resp.Blocklist),internal/api/updates.go:277(has_updatesset on blocklist change). - Dead helper:
internal/pki/blocklist.go(Blocklisttype) is never used in non-test code — no server-side enforcement either.
Reachability (hop by hop)
- Operator clicks Block on host B (or B is compromised/offboarded). B's fingerprint enters the per-CA
blocklisttable. - Every other host A under the same CA polls
GET /api/v1/agent/updates; server returnsblocklist: [<B-fp>, ...]andhas_updates=true. - A's agent decodes
Blocklistthen discards it;poll()has no blocklist branch. - Even on a config re-render,
configgen.Generateemitspki: {ca,cert,key}with noblocklistkey (proven by PoC). - A's Nebula daemon has an empty blocklist and accepts handshakes from B's still-valid cert. B keeps full mesh access.
Impact
Revocation is the only in-band mechanism that isolates a compromised/offboarded host from a Nebula mesh. Because the blocklist never reaches any peer's config.yml, a Blocked host retains full overlay reachability to every peer under its CA (and internal services on the mesh) for up to 30d (agent) / 365d (mobile). An attacker who exfiltrates host.key+host.crt can run stock slackhq/nebula directly, ignore the agent's 403/410 poll responses, and stay connected after the operator revokes the host. Operator-visible state (UI shows blocked, audit log records it) is misleading.
Proof of Concept (benign)
internal/configgen/blocklist_poc_test.go renders a fully-populated host config and asserts the output contains the pki section but NO blocklist key:
$ 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
The agent half is verifiable by inspection: poll() has branches for CertificatePEM/CACertPEM/ConfigYAML/RekeyRequired but none for Blocklist.
Distinctness
NOT a duplicate of GHSA-339v / CVE-2026-53602 (revocation durability = a blocked host getting a NEW cert re-issued; its fix CheckIssuanceAllowed is present and orthogonal). This bug is that the EXISTING cert is never rejected at peers — the distribution/enforcement layer. Checked against all 17 known advisories; none cover blocklist application in the agent or pki.blocklist generation.
Remediation
- Add
Blocklist []safeStringtopkiSection(yamlblocklist,omitempty) andGeneratorInput; consider alsopki.disconnect_invalid: true. - Have the agent apply
updates.Blocklistby re-rendering/rewritingconfig.yml+ SIGHUP (same path asConfigYAML). Simplest: fold the blocklist into the server-renderedConfigYAMLso it flows through the existing write path. - Add a regression test asserting a non-empty server blocklist yields a
pki.blocklistentry in the agent's written config.yml.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/forgekeep/nebula-mesh | all versions | 0.7.1go get github.com/forgekeep/nebula-mesh@v0.7.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/forgekeep/nebula-mesh, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update github.com/forgekeep/nebula-mesh to 0.7.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-61699 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
Frequently Asked Questions
Is CVE-2026-61699 in your dependencies?
Find it across Go, including transitive dependencies.