Security and correctness release. Every finding below was reproduced with an executed PoC before it was fixed, and each fix has a test that fails without it.
Security
| Sev | ||
|---|---|---|
| S2 | High | Path traversal. All 63 tool endpoint paths were f-strings interpolating a caller-supplied name, and httpx resolves .. against the base URL — so an argument selected the endpoint. A read-only server with only the compliance toolset reached GET /v1/user; nv_delete_group("../api_key/mcp-writer") issued DELETE /v1/api_key/mcp-writer, and the confirm handshake authorised it. Closed centrally in client.request. |
| S1 | High | Bearer scopes were decorative — parsed, handed to StaticTokenVerifier, never read. An nv:read token called nv_delete_group over real HTTP and the DELETE landed. Removed rather than enforced: privilege belongs to the NeuVector API key's role, narrowed by NV_READ_ONLY, NV_TOOLSETS and the handshake. |
| S9 | High | Credential leak. Any Settings validation failure rendered pydantic's input_value — the whole settings dict — so a typo'd NV_TOOLSETS printed NV_API_SECRET_KEY and NV_PASSWORD to the pod log. |
| S3 | Med-High | NV_ALLOWED_NAMESPACES was fail-open and overstated. nv_delete_group passed no namespace at all. Now wired up — 18 of 67 write paths are namespace-checked. The other 49 cannot derive a namespace, and that limit is now documented instead of implied away. |
| S5 | Medium | NetworkPolicy ingress had no from: selector, so every pod in the cluster could reach :8080 while the comment above it claimed "Nothing else, in or out." |
Correctness and performance
| Sev | ||
|---|---|---|
| P1 | High | Writes were retried. Every method was retried 3× on a transient failure, including ERR_FAIL_WRITE_CLUSTER and 502/504 — a failing POST /v1/group was sent three times. Retries are now GET-only, with a retry= opt-in. |
| P2 | Medium | Re-login stampede. Ten concurrent 401s produced ten POST /v1/auth, risking ERR_TOO_MANY_LOGIN_USER. Measured 10 logins before, 1 after. |
⚠️ Before you roll this out
1.1.3 rejects a bearer token carrying a <token>:<scope> suffix at startup. That format was the 1.1.x convention and is now a hard configuration error, deliberately — silently stripping it would preserve belief in a control that never existed.
Fix the Secret first or the pod CrashLoops:
kubectl -n <ns> create secret generic neuvector-mcp-clients \
--from-literal=bearer-tokens="$(kubectl -n <ns> get secret neuvector-mcp-clients \
-o jsonpath='{.data.bearer-tokens}' | base64 -d | cut -d: -f1)" \
--dry-run=client -o yaml | kubectl apply -f -The token itself is unchanged, so clients need no reconfiguration — only the suffix comes off. Check for more than one comma-separated entry before using cut.
NV_ALLOWED_NAMESPACES also became honest rather than stronger. If you were relying on it to gate registry, webhook, sigstore or IAM writes, it never did — see the README section "What NV_ALLOWED_NAMESPACES does not cover."
Image
registry.ash4d.com/ash4d-lab/neuvector-mcp:1.1.3
sha256:484d127f507cc26051451b99e195ba6aa4c75493a79546bfb393f412a7c13489
linux/amd64, built on registry.suse.com/bci/python:3.13. The digest was verified by reading the tag back from the registry, not by trusting the builder's log.
Gate
119 tools, 700 passed / 1 xfailed, 97.65% coverage, 0 spec violations, ruff and mypy --strict clean.
Known divergence: SPEC.md was deliberately left frozen. SPEC 7.2 still describes the old all-methods retry policy and SPEC.md:260 still documents the removed scope format. Editing the spec to match the code would invert which one is authoritative.