Summary
The nebula-mgmt Web UI host-creation path ignores both the server-wide enrollment_token_ttl security setting and per-network network_config.enrollment_token_ttl overrides. API host creation and token-regeneration paths use the configured TTL resolver, but POST /ui/hosts hardcodes now.Add(24 * time.Hour) for newly minted agent enrollment tokens. In deployments that intentionally reduce enrollment-token lifetime, any authenticated operator who can create a host through the Web UI can still mint a bearer enrollment token valid for about 24 hours.
Details
Enrollment tokens are bearer credentials for the public POST /api/v1/enroll endpoint: possession of a valid token allows enrolling the pending host and receiving a signed Nebula certificate/config for that host. The server configuration documents a security knob for their default lifetime and per-network overrides:
internal/config/server.go:82 defines EnrollmentTokenTTL as the default lifetime for freshly minted enrollment tokens.
internal/config/server.go:83 documents per-network overrides in network_config under enrollment_token_ttl.
The API server implements and consistently uses this resolver:
internal/api/server.go:77 defines tokenTTLFor, with precedence of per-network enrollment_token_ttl, then server default, then 24h fallback.
internal/api/server.go:82 reads network_config.enrollment_token_ttl.
internal/api/server.go:89 falls back to the configured server default.
internal/api/hosts.go:190 through internal/api/hosts.go:196 use now.Add(s.tokenTTLFor(r.Context(), host.NetworkID)) for API host creation.
The Web UI sibling path does not call the resolver and instead always sets a 24-hour expiry:
internal/web/handlers.go:874 mints the raw token for POST /ui/hosts.
internal/web/handlers.go:879 sets ExpiresAt: now.Add(24 * time.Hour).
This creates inconsistent behavior between API and Web UI host creation and bypasses an operator-configured token lifetime policy. The issue is reachable by an authenticated Web UI operator who can create hosts. Admins can create hosts in any network; non-admin operators can create hosts in networks whose CA they own.
Affected version evidence: the configurable enrollment-token TTL feature was introduced by commit 6c344a6 (feat(api): configurable enrollment-token TTL + regenerate endpoint (#75) (#79)), and git tag --contains 6c344a6 --sort=version:refname returns v0.3.0 through v0.3.8. Pattern checks across all release tags showed the TTL config/API resolver and the Web UI 24-hour hardcode are present in every v0.3.x release from v0.3.0 to v0.3.8, and are not meaningfully applicable to v0.1.x/v0.2.0 because the TTL policy knob was not present there. The current checkout at commit d92dd9a60de291e2bc1caf73b4e9a99567b31ec0 (git describe: v0.3.8-1-gd92dd9a) remains affected.
PoC
Safe local PoC run from a clean checkout at commit d92dd9a60de291e2bc1caf73b4e9a99567b31ec0 on 2026-06-12. The PoC is a temporary Go test that uses only in-memory SQLite and httptest; it does not start a real server and does not contact external services.
- Create a temporary test file
internal/web/security_audit_poc_test.go in package web.
- In the test, create an in-memory Web UI with
newTestWeb(t), create a network audit-poc-net with CIDR 10.77.0.0/24, and set network_config.enrollment_token_ttl to 30m.
- Log in as the seeded test admin through the normal Web UI helper and obtain a CSRF token from
GET /ui/hosts/new.
- Submit
POST /ui/hosts with network_id=audit-poc-net, name=audit-poc-host, nebula_ips=10.77.0.10, role=host, and kind=agent.
- Parse the one-shot enrollment token from the returned host-detail page and read the token row with
GetEnrollmentToken.
- Compare the observed expiry to the configured 30-minute network override.
Command run:
go test ./internal/web -run 'TestSecurityAuditPOC' -count=1 -v
Observed vulnerable output from this environment:
=== RUN TestSecurityAuditPOC_UIHostCreateIgnoresNetworkEnrollmentTokenTTL
POC_UI_TTL_BYPASS observed_token_ttl=24h0m0s configured_network_ttl=30m expires_at=2026-06-13T14:51:45Z
--- PASS: TestSecurityAuditPOC_UIHostCreateIgnoresNetworkEnrollmentTokenTTL (0.05s)
The meaningful control is the API sibling: internal/api/hosts.go:190 through internal/api/hosts.go:196 uses s.tokenTTLFor(...), and existing tests in internal/api/hosts_token_ttl_test.go verify API-created/regenerated enrollment tokens honor server-default and per-network TTLs. Variant review also found API regenerate-token, API re-enroll, and signed-poll rekey token minting use the resolver rather than a hardcoded 24h value. After recording the output, the temporary test file was removed and git status --short returned clean. The PoC was re-run after drafting this report and produced the output shown above.
Impact
An authenticated Web UI operator can bypass a configured enrollment-token lifetime policy and obtain a token valid for approximately 24 hours even when the deployment or network is configured for a much shorter lifetime such as 30 minutes. Because enrollment tokens are bearer credentials for the public enrollment endpoint, longer-than-intended validity increases the window in which a copied, logged, shared, or otherwise exposed token can be used to enroll the pending host and obtain its Nebula certificate/config. This weakens confidentiality and integrity for deployments relying on short token lifetimes to reduce enrollment-token exposure.
Suggested remediation: refactor the Web UI host-creation path to use the same TTL resolution as the API path, or move the resolver into a shared package/service used by both API and Web UI. Add a regression test under internal/web that sets network_config.enrollment_token_ttl = "30m", creates an agent host through POST /ui/hosts, and asserts the persisted enrollment token expires within the configured 30-minute window rather than 24 hours.
Summary
The
nebula-mgmtWeb UI host-creation path ignores both the server-wideenrollment_token_ttlsecurity setting and per-networknetwork_config.enrollment_token_ttloverrides. API host creation and token-regeneration paths use the configured TTL resolver, butPOST /ui/hostshardcodesnow.Add(24 * time.Hour)for newly minted agent enrollment tokens. In deployments that intentionally reduce enrollment-token lifetime, any authenticated operator who can create a host through the Web UI can still mint a bearer enrollment token valid for about 24 hours.Details
Enrollment tokens are bearer credentials for the public
POST /api/v1/enrollendpoint: possession of a valid token allows enrolling the pending host and receiving a signed Nebula certificate/config for that host. The server configuration documents a security knob for their default lifetime and per-network overrides:internal/config/server.go:82definesEnrollmentTokenTTLas the default lifetime for freshly minted enrollment tokens.internal/config/server.go:83documents per-network overrides innetwork_configunderenrollment_token_ttl.The API server implements and consistently uses this resolver:
internal/api/server.go:77definestokenTTLFor, with precedence of per-networkenrollment_token_ttl, then server default, then 24h fallback.internal/api/server.go:82readsnetwork_config.enrollment_token_ttl.internal/api/server.go:89falls back to the configured server default.internal/api/hosts.go:190throughinternal/api/hosts.go:196usenow.Add(s.tokenTTLFor(r.Context(), host.NetworkID))for API host creation.The Web UI sibling path does not call the resolver and instead always sets a 24-hour expiry:
internal/web/handlers.go:874mints the raw token forPOST /ui/hosts.internal/web/handlers.go:879setsExpiresAt: now.Add(24 * time.Hour).This creates inconsistent behavior between API and Web UI host creation and bypasses an operator-configured token lifetime policy. The issue is reachable by an authenticated Web UI operator who can create hosts. Admins can create hosts in any network; non-admin operators can create hosts in networks whose CA they own.
Affected version evidence: the configurable enrollment-token TTL feature was introduced by commit
6c344a6(feat(api): configurable enrollment-token TTL + regenerate endpoint (#75) (#79)), andgit tag --contains 6c344a6 --sort=version:refnamereturnsv0.3.0throughv0.3.8. Pattern checks across all release tags showed the TTL config/API resolver and the Web UI 24-hour hardcode are present in everyv0.3.xrelease fromv0.3.0tov0.3.8, and are not meaningfully applicable tov0.1.x/v0.2.0because the TTL policy knob was not present there. The current checkout at commitd92dd9a60de291e2bc1caf73b4e9a99567b31ec0(git describe:v0.3.8-1-gd92dd9a) remains affected.PoC
Safe local PoC run from a clean checkout at commit
d92dd9a60de291e2bc1caf73b4e9a99567b31ec0on 2026-06-12. The PoC is a temporary Go test that uses only in-memory SQLite andhttptest; it does not start a real server and does not contact external services.internal/web/security_audit_poc_test.goin packageweb.newTestWeb(t), create a networkaudit-poc-netwith CIDR10.77.0.0/24, and setnetwork_config.enrollment_token_ttlto30m.GET /ui/hosts/new.POST /ui/hostswithnetwork_id=audit-poc-net,name=audit-poc-host,nebula_ips=10.77.0.10,role=host, andkind=agent.GetEnrollmentToken.Command run:
Observed vulnerable output from this environment:
The meaningful control is the API sibling:
internal/api/hosts.go:190throughinternal/api/hosts.go:196usess.tokenTTLFor(...), and existing tests ininternal/api/hosts_token_ttl_test.goverify API-created/regenerated enrollment tokens honor server-default and per-network TTLs. Variant review also found API regenerate-token, API re-enroll, and signed-poll rekey token minting use the resolver rather than a hardcoded 24h value. After recording the output, the temporary test file was removed andgit status --shortreturned clean. The PoC was re-run after drafting this report and produced the output shown above.Impact
An authenticated Web UI operator can bypass a configured enrollment-token lifetime policy and obtain a token valid for approximately 24 hours even when the deployment or network is configured for a much shorter lifetime such as 30 minutes. Because enrollment tokens are bearer credentials for the public enrollment endpoint, longer-than-intended validity increases the window in which a copied, logged, shared, or otherwise exposed token can be used to enroll the pending host and obtain its Nebula certificate/config. This weakens confidentiality and integrity for deployments relying on short token lifetimes to reduce enrollment-token exposure.
Suggested remediation: refactor the Web UI host-creation path to use the same TTL resolution as the API path, or move the resolver into a shared package/service used by both API and Web UI. Add a regression test under
internal/webthat setsnetwork_config.enrollment_token_ttl = "30m", creates an agent host throughPOST /ui/hosts, and asserts the persisted enrollment token expires within the configured 30-minute window rather than 24 hours.