# nebula-mesh: Certificate revocation is never enforced at the mesh

nebula-mesh agent decodes the server's per-CA certificate blocklist then discards it, so revoked hosts keep full mesh access until cert expiry.

- Payout: Research
- Severity: high
- Program: nebula-mesh
- Found by: Pig-Tail
- Disclosed: 2026-07-14
- CVE: CVE-2026-61699
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-cm26-5974-52h8
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-cm26-5974-52h8
- Tags: area:devices domain:infra domain:network impact:integrity impact:internal-access kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** Nebula mesh agent poller and config generator (Go)
**Root cause:** The server computes and ships a per-CA certificate blocklist in the agent-updates response, but the agent's poll() never applies UpdatesResponse.Blocklist and the config generator's pkiSection has no blocklist field, so pki.blocklist never reaches any peer's config.yml.
**Steps:**
1. Operator blocks host B; B's fingerprint enters the per-CA blocklist table via `GetBlocklistForCA`.
2. Peer A polls `GET /api/v1/agent/updates`; server returns `blocklist: [<B-fp>]` and `has_updates=true` (`internal/api/updates.go:182-191,277`).
3. A's agent decodes `Blocklist` at `internal/agent/poller.go:33` but `poll()` (lines 300-339) only applies CertificatePEM/CACertPEM/ConfigYAML, never the blocklist.
4. On config re-render, `configgen.Generate` emits `pki: {ca,cert,key}` with no blocklist key (`internal/configgen/marshal.go:42-46`).
5. A's Nebula daemon has empty blocklist and accepts B's still-valid cert; B retains full mesh access.
6. PoC: `go test ./internal/configgen/ -run TestPoC_NMESH001 -v` confirms generated config has pki section but no blocklist key.
**How found:** Code reading: noticed the server side fully implements blocklist computation and shipping (GetBlocklistForCA, resp.Blocklist, has_updates) while the agent's poll() had branches for every other update field but none for Blocklist, and the config generator's pkiSection lacked a blocklist field; confirmed with a configgen PoC test.
**Exploitation:** Attacker who exfiltrates `host.key`+`host.crt` runs stock slackhq/nebula directly, ignores the agent's 403/410 poll responses, and stays connected to the overlay after the operator revokes the host, for up to 30d (agent) or 365d (mobile) cert lifetime.
**Insight:** When a control is split across server and agent, diff the update-response fields the server populates against the branches the agent's apply function actually handles — a decoded-but-unreferenced struct field is a silent no-op control. Also check the config generator's schema for the enforcement key the daemon needs.

## Proof of concept (copied exactly from the original report)

**PoC test asserting generated config omits blocklist**

```
$ 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
```

