80a82c4cae129c5426f3ccd042d1be7da5471946
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
99ec6ebc53 |
fix(tailscale-admin): harden apiToken/tags/description validation (DC-080) [glm-grade=B]
DC-080 round-1 GLM-5.3 judge verdict: B. Round-2 polish folded into same
commit per multi-round fix-first protocol: tighten tag regex to require
non-empty name after 'tag:' (matches Tailscale spec), drop dead
`module.exports.createApp = null` line.
THREAT MODEL
Pre-fix, /api/v1/tailscale/* and /api/v1/tailscale/admin/* (TOTP-gated)
had inconsistent checks on caller-supplied input. Three coupled gaps:
(a) PUT /settings validated `apiToken.startsWith('tskey-api-')` but
had NO length cap — body-parser limit was the only ceiling. A 1 MB
string starting with `tskey-api-` would be `.trim()`-ed, sent to
Tailscale's /devices endpoint, and waste server-side CPU on a
request that will always 401.
(b) POST /settings/test accepted `apiToken` from the body with NO
validation at all. The PUT route's prefix check did NOT extend to
this path. An operator could submit arbitrary junk and the
container would still call /devices on the Tailscale API with it
(DoS-reflection + fingerprint timing for an attacker probing
whether this API token format is accepted).
(c) POST /admin/keys validated `tags` as Array but NOT per-element
type — `tags: ['tag:guest', null, 123, {injection: true}]` would
be forwarded to Tailscale verbatim. Tailscale's API is JSON-strict
and would 400 the request, but the bad shape reached the wire.
Similarly `description` had no length cap (Tailscale caps at 120
chars per their docs).
All three are gated by TOTP — this is a logged-in-operator / phished-
session threat surface, not anonymous-unauth. The fix is defense-in-
depth: a bug in the auth path (TOTP bypass, session theft, future route
handler trust-boundary drift) should not turn these endpoints into a
`submit anything and forward to Tailscale` relay.
FIX 1 — Shared validators (round-1)
- `_validateApiToken(token)`: typeof string check, prefix required,
length cap 256 chars. Catches empty/null/non-string AND oversize.
- `_validateTags(tags)`: undefined/null allowed (optional field),
Array.isArray check, max 32 entries, per-element string check,
per-element length cap 64 chars, regex
`/^tag:[a-z0-9][a-z0-9_-]{0,62}$/` (round-2: requires non-empty
name after `tag:` per Tailscale spec).
- `_validateDescription(description)`: undefined/null allowed, string
type check, length cap 120 chars (matches Tailscale's documented cap).
All three return null on success or an error string on failure. Route
layer maps to 400 via `errorResponse`. Validators exported via
`module.exports._validators` for direct unit testing (otherwise
unreachable from outside the factory closure).
FIX 2 — Endpoint wiring (round-1)
- PUT /settings: replaced inline `!startsWith('tskey-api-')` check with
`_validateApiToken(token)`. Single source of truth for the rule.
- POST /settings/test: added `_validateApiToken(token)` guard BEFORE
calling `client.setApiToken(token)`. The body is optional, so the
guard is skipped when no token is provided (uses stored token path).
- POST /admin/keys: replaced `Array.isArray(opts.tags)` shallow check
with `_validateTags(opts.tags)`, plus `_validateDescription(opts.description)`.
Old code already validated `expirySeconds`; that stays.
FIX 3 — Round-2 polish
- TAG_KEY_RE: `/^[a-z0-9][a-z0-9:_-]{0,63}$/` → `/^tag:[a-z0-9][a-z0-9_-]{0,62}$/`.
The old regex accepted `tag:` (empty name), which Tailscale's API
rejects. New regex requires `tag:` prefix and ≥1 alphanumeric name
char followed by [a-z0-9_-]{0,62} — total length up to 67 chars, well
within Tailscale's documented 15..63 char tag length.
- Removed `module.exports.createApp = null` vestigial line — the file
only exports the factory function and the _validators bag.
TESTS (29 original + 16 new = 45 in this suite)
- 4 PUT /settings new: length cap, non-string type, prefix round-trip
(existing 'starts with' tests already passed), plus the original
6 (4 pre-existing PUT tests stay green).
- 4 POST /settings/test new: prefix rejection, length cap, stored-token
path with empty body still works.
- 4 POST /admin/keys new: null/123/object entries rejected, uppercase /
whitespace / CRLF rejected, description length cap, canonical
lowercase `tag:server` accepted.
- 4 direct validator unit tests: validateApiToken (5 cases incl. cap-edge),
validateTags (8 cases incl. round-2 bare-'tag:' rejection), validateDescription
(3 cases incl. cap-edge), constants-export surface.
All 45 tests pass on DNS2 (verified). Full repo suite unchanged: 2351/2351.
|
||
|
|
6fb4f9b169 |
DC-043: tailscale coordination API client + admin/settings routes
Companion to DC-042 (tailscale-manager). Adds the write-side of Tailscale
integration — REST client for api.tailscale.com that lets DashCaddy
manage its own tailnet (devices, pre-auth keys, users, ACL).
src/managers/tailscale-coord.js — new module:
* Ping, list/get/delete devices, create/list/delete pre-auth keys,
list users, get/update ACL
* 5min read cache, 60s device-list cache, 1hr ping cache
* Cache invalidation on writes
* Graceful {configured:false} when no token
* TailscaleCoordError class with code mapping (unauthorized, not_found,
rate_limited, server_error)
* fetchImpl injection point for tests; native https in production
* 45 unit tests covering all paths
src/context/index.js — new tailscaleCoord namespace:
* getClient() lazy-builds a fresh client each call (token re-read from
credentialManager so settings changes take effect without restart)
* loadMetadata/saveMetadata for tailscale-config.json
* setApiToken/hasApiToken wrappers around credentialManager
routes/tailscale-admin.js — new routes:
* GET /api/v1/tailscale/settings — config status, never the token
* PUT /api/v1/tailscale/settings — validate + store encrypted
* DELETE /api/v1/tailscale/settings — wipe token + metadata
* POST /api/v1/tailscale/settings/test — ping without saving
* GET /api/v1/tailscale/admin/devices — full device list
* DELETE /api/v1/tailscale/admin/devices/:id — revoke device
* GET /api/v1/tailscale/admin/users — tailnet users
* GET /api/v1/tailscale/admin/keys — pre-auth key metadata
* POST /api/v1/tailscale/admin/keys — create pre-auth key (returns secret ONCE)
* DELETE /api/v1/tailscale/admin/keys/:id — revoke pre-auth key
* 29 route integration tests with supertest
src/app.js — wired the new router into the /api/v1/tailscale mount.
BACKLOG.md — DC-042 marked done, DC-043 added with full design notes.
Note: deliberately no token auto-rotation — Tailscale API keys
don't auto-renew, and silently re-issuing admin credentials
would erode the audit-trail checkpoint that token expiry
provides.
Tests: 1167 -> 1212 (+45), all green. Lint clean.
|