Commit Graph

10 Commits

Author SHA1 Message Date
Anso be22e3ded1 docs(api-tokens): deep rewrite for v1, expand to fleet automation guide (#1140)
Rewrite docs/features/api-tokens.mdx (115 → 442 lines) as a full
product + technical guide: mental model, scope ladder, prerequisites,
step-by-step usage with HTTP/WS/multi-node examples, complete universal-
restriction table, cross-node proxy behaviour, lifecycle, rate-limit
ceiling, security model, limitations, three example workflows,
troubleshooting accordion, FAQ accordion.

Replace the single populated-list screenshot with five captured against
production: empty state, create form, reveal banner (token value
redacted), populated list with all three scope variants, revoke modal.

Sibling-doc edits keep tier statements coherent now that API tokens are
available on every tier:
- docs/api-reference/overview.mdx: drop the Admiral-only Note callout,
  drop API Tokens from the Admiral-gated license-tier table, correct
  the rate-limit table to say tokens are keyed per-credential
- docs/features/overview.mdx: drop the "Admiral only." sentence
- docs/security.mdx: drop "Admiral tier.", move API tokens row to every
  tier in the security matrix, repoint image to the new populated shot
2026-05-21 21:20:59 -04:00
Anso d882f223f4 feat(api-tokens): switch to sen_sk_ prefixed opaque keys (#1062)
* feat(api-tokens): switch to sen_sk_ prefixed opaque keys

Replace JWT-shaped API tokens with 56-char opaque keys of the form
`sen_sk_<43-char base62 random><6-char base62 checksum>` (256-bit
entropy, sha256-truncated checksum). Node-proxy tokens stay JWTs.

Why:
* The api_token path was already a sha256 DB lookup; the JWT signature
  was wasted work and the 400d JWT ceiling vs DB expires_at was a
  confusing dual bound.
* Opaque tokens carry a verifiable checksum so malformed/typoed values
  are rejected before any SQLite lookup.
* `sen_sk_` prefix is recognizable to GitHub, TruffleHog, GitGuardian
  and makes the on-wire shape visually distinct from node_proxy JWTs.

Changes:
* New `utils/apiTokenFormat.ts` (generate + checksum-verify, CSPRNG via
  randomInt, timingSafeEqual on the checksum compare).
* `middleware/auth.ts` and `websocket/upgradeHandler.ts` route opaque
  tokens before any jwt.verify; 401 messages unified to avoid a
  token-existence oracle.
* `middleware/rateLimiters.ts` short-circuits opaque tokens in the
  node_proxy detection and keys per-token via a non-reversible sha256
  slice so each token keeps its own bucket without a DB hit.
* All six existing tests migrated from jwt.sign({scope:'api_token'})
  to generateApiToken(); new format-only test suite covering prefix,
  length, alphabet, checksum reject paths, and a 10k-iteration
  collision/integrity loop.
* Docs (features/api-tokens.mdx, api-reference/overview.mdx) describe
  the shape and drop the obsolete JWT-ceiling note.

* fix(api-tokens): clear CI lint and CodeQL false positives

* Drop unused TEST_USERNAME import in remote-console-session.test.ts;
  the migration to generateApiToken() left it orphaned.
* Add a CodeQL barrier model so `generateApiToken`'s ReturnValue does
  not flow into the `insufficient-password-hash` query. The function
  emits 256-bit CSPRNG opaque keys; sha256 of the raw token is the
  correct construction for high-entropy API tokens (bcrypt-class
  hashes target low-entropy human passwords). CodeQL's name heuristic
  was treating "Token" as a password source and flagging the standard
  sha256 wrapping at all 9 call sites.

* ci(codeql): exclude js/insufficient-password-hash for token paths

The previous barrierModel data extension was a no-op for this rule: the
js/insufficient-password-hash query identifies its "password" sources via
SensitiveExpr's name heuristic ("token", "secret", "key" substrings),
which is upstream of the taint-tracking layer where barrierModel applies.
Verified by post-push re-analysis: 9 alerts still open, all undismissed.

Replace the dead extension with a path-scoped query-filter in
codeql-config.yml so the rule no longer fires on apiTokenFormat,
apiTokens, and the test directory. Real user-password hashing code
elsewhere in the repo (auth, users, setup routes) remains analyzed.

The 9 existing alerts on PR #1062 are dismissed via API as false
positives with a justification pointing at this config. Future runs
will not re-flag them because of the path filter.
2026-05-15 18:21:28 -04:00
Anso ed553f1f19 feat: change default listen port from 3000 to 1852 (#756)
Updates the backend listen port, Vite dev proxy target, Docker EXPOSE,
compose port mapping, .env.example default, GitHub Actions smoke-test
default, healthcheck URLs, and every doc/example reference. Test fixtures
that include example URLs were updated for consistency, though their
assertions are port-agnostic.

The rate-limit value of 3000 in middleware/rateLimiters.ts and the
3000 entry in WEB_UI_PORTS (which detects user containers like Grafana)
are intentionally untouched.
2026-04-24 22:23:31 -04:00
Anso c4ff58347e fix(container-exec): harden with security fixes, validation, and test coverage (#577)
* fix(container-exec): harden with security fixes, validation, and test coverage

- Enforce admin role at WebSocket upgrade for container exec sessions
- Validate container is running before creating exec
- Fix bash-to-sh fallback (move .start() inside try/catch)
- Register container-exec as a capability for fleet visibility
- Add standard and diagnostic logging for exec lifecycle
- Fix false Admiral license claim in API docs
- Fix design system violations in BashExecModal (hardcoded colors)
- Remove duplicate legacy xterm dependencies
- Add 18-test suite covering auth, validation, fallback, and cleanup

* fix(container-exec): use correct Duplex type for exec stream

The stream variable was typed as NodeJS.ReadWriteStream, which lacks
.destroy(). Dockerode's Exec.start() returns stream.Duplex per its
type definitions. This caused tsc to fail while Vitest (which skips
full type checking) passed.
2026-04-14 08:23:05 -04:00
Anso 8e1b9826cf fix(api): add tiered rate limiting to prevent polling lockouts (#460)
* fix(api): add tiered rate limiting to prevent polling lockouts

Replace the single global rate limiter (100 req/min/IP) with a tiered
system that separates high-frequency polling endpoints from standard
API traffic:

- Polling tier (300/min): /stats, /system/stats, /stacks/statuses,
  /metrics/historical, /health, /meta, /auth/status, /auth/sso/providers,
  /license. Exempt from the global limiter but governed by their own
  safety net to prevent resource exhaustion.
- Standard tier (200/min): All other endpoints, raised from 100.
- Webhook tier (500/min): POST /webhooks/:id/trigger, dedicated limiter
  for CI/CD platforms sharing datacenter IPs.
- Auth tier: Unchanged (5-10 attempts / 15 min).

Enterprise adaptations:
- Authenticated requests keyed by user session (JWT sub/username) instead
  of IP, preventing shared NAT/VPN environments from pooling budgets.
- Internal node-to-node traffic (node_proxy tokens) bypasses all rate
  limiters entirely.

Includes comprehensive stress tests (21 cases) validating tier
separation, node proxy bypass, and per-user keying.

* chore(deps): bump axios to 1.15.0 to fix SSRF vulnerability

Addresses GHSA-3p68-rc4w-qgx5 (NO_PROXY hostname normalization bypass).
2026-04-09 17:56:58 -04:00
Anso f516275834 refactor(licensing): replace Pro branding with Community/Skipper/Admiral tiers (#375)
Eliminate all references to "Pro" across backend, frontend, and docs.
Internal tier value renamed from 'pro' to 'paid'; user-facing text now
uses the thematic tier names (Community, Skipper, Admiral).

- Rename LicenseTier 'pro' to 'paid' in backend and frontend types
- Rename requirePro guard to requirePaid, error code PRO_REQUIRED to PAID_REQUIRED
- Rename ProGate.tsx to PaidGate.tsx with updated copy
- Fix: trial users can now see upgrade/purchase cards in Settings
- Update all docs and openapi.yaml to use correct tier names
2026-04-05 05:59:36 -04:00
Anso c4e2595ded docs: harden public docs by removing security-sensitive details (#331)
* docs: remove security-sensitive implementation details from public documentation

Generalize or remove internal architecture details that could aid targeted
attacks — CVE tables, database schema, rate limit thresholds, proxy internals,
encryption algorithm names, and WebSocket middleware bypass info.

* test(metrics): fix flaky minute-bucket aggregation test

Floor baseTime to the start of the current minute so baseTime + 5000
never crosses a minute boundary and produces 2 buckets instead of 1.
2026-04-01 23:48:49 -04:00
Anso b28ebfa6ff feat(api): add global rate limiter for all API endpoints (#317)
Apply a global rate limit of 100 requests/min per IP to all /api/ routes
in production, configurable via API_RATE_LIMIT env var. Auth endpoints
retain their existing stricter limits which stack independently.
Returns 429 Too Many Requests when exceeded.
2026-04-01 20:12:30 -04:00
Anso 1ab04be235 fix(security): enforce stack name validation on all routes (#314)
Audit found 11 routes with no stackName validation and 2 using a weaker
manual check. All 13 now use the canonical isValidStackName() guard
(^[a-zA-Z0-9_-]+$), returning 400 with { error: 'Invalid stack name' }.
2026-04-01 19:43:11 -04:00
Anso eae83c997b docs: add OpenAPI 3.1 spec and API Reference tab (#294)
Add a complete OpenAPI 3.1 specification covering ~55 public API endpoints
across 8 categories (Stacks, Containers, API Tokens, Webhooks, Nodes, Fleet,
Scheduled Tasks, Health). Wire it into Mintlify via native OpenAPI rendering
with an interactive "Try It" playground and a dedicated API Reference tab.

Includes an API overview page documenting authentication, token scopes,
node routing, error format, license tier requirements, and WebSocket endpoints.
2026-03-31 16:13:07 -04:00