Skip to content

Security hardening follow-ups from hosted-tenant assessment (app.bluee.sh) #158

Description

@cubeorgdev

Security hardening follow-ups from hosted-tenant assessment

Consolidated follow-up list from an authorized security assessment of the hosted tenant (app.bluee.sh / api.bluee.sh / bluee.sh), combining a white-box review of this repo (@ 2dc6e7a) with non-destructive black-box testing on 2026-09-16.

Headline result: no finding yields unauthorized access — auth enforcement, invitation-only sign-up, rate-limited login, OAuth client restrictions, TLS config, and patched framework versions all held up. The items below are hardening/defense-in-depth, ordered roughly by priority.


Medium

1. Clickjacking: no frame protection on the dashboard (device-approval impact)

Every page on app.bluee.sh is served with no Content-Security-Policy/frame-ancestors and no X-Frame-Options (verified live on /login and /device).

This matters most for the device-authorization approval page (/device/[token]): a signed-in user tricked into clicking a framed approval page could approve an attacker's CLI device code, yielding a session with governance:read / session:write / client-status:write scopes as that user.

Fix: emit Content-Security-Policy: frame-ancestors 'none' (or 'self') and X-Frame-Options: DENY from a headers() block in apps/dashboard/next.config.ts.

2. Missing security headers across all surfaces

None of app.bluee.sh, api.bluee.sh, or bluee.sh currently send:

  • Strict-Transport-Security — all hosts already 301 HTTP→HTTPS, so preload-ready
  • X-Content-Type-Options: nosniff
  • Referrer-Policy — URLs carry security-relevant tokens (invitation IDs, device tokens) that can leak via Referer to third-party resources
  • Permissions-Policy
  • Content-Security-Policy — hardening only; the dashboard currently avoids dangerouslySetInnerHTML, no active XSS found

Fix: central headers() in apps/dashboard/next.config.ts; ALB/ingress response-header policy for api.bluee.sh; nginx config for the static apex (bluee.sh).


Low

3. Public /metrics and verbose /health/* on api.bluee.sh

Unauthenticated (all verified 200 from the internet):

  • GET /metrics — DB pool sizes, KMS permit counts, TLS reload success/error counters (operational fingerprinting; a spike in tls_reload_errors_total is also an attack-timing signal)
  • GET /health/schema (DB migration state), /health/worker, /health/object-storage, /health/credential-resolver — internal component status oracle

Fix: keep bare /health and /ready public for the ALB; move /metrics and the detailed /health/* checks to the internal listener (:8082) or restrict by ALB rule to the monitoring network. Relevant code: services/control-api/src/lib.rs public route registrations (~lines 1555–1568).

4. No application-level rate limiting on control-api public surface

The Rust control-api has no rate limiter; only the ALB throttles. This covers /metrics, /health/*, and notably /scim/v2/* (static deployment bearer token — correctly fails closed with a constant-time compare and the token is high-entropy, so not practically guessable, but unthrottled attempts are possible at network speed). The Better-Auth sign-in path is rate-limited and verified working (429 after ~6 attempts).

Fix: WAF/ALB rate rules for public paths on api.bluee.sh and /scim/v2, or a tower middleware layer in control-api.

5. Technology/version disclosure

  • x-powered-by: Next.js header on the dashboard, and the exact version (16.3.4 at assessment time) recoverable from JS chunk content
  • server: nginx/1.27.5 on bluee.sh

Fix: poweredByHeader: false in next.config.ts; server_tokens off; in the nginx server block.


Informational

6. No security.txt

/.well-known/security.txt returns 404 on app and apex. For an enterprise-targeted open-source project, publish one pointing at the contact/policy in SECURITY.md.


Code/config hardening (from source review — not externally exploitable today)

7. ECS module runs internal credential hop in plaintext

deploy/tofu/aws-ecs/locals.tf:77 sets HARNESS_INTERNAL_TRANSPORT_MODE = "insecure-http", so decrypted upstream LLM credentials returned by :8082 (/internal/gateway/resolve) cross the VPC hop unencrypted (mitigated today by the self-referencing security group + OAuth M2M on the endpoint). The Helm chart defaults to mTLS (deploy/helm/values.yaml:36-38).

Fix: enable mTLS in the aws-ecs module to match the Helm posture.

8. Invitation bearer URLs written to control-api logs

log_invitation (services/control-api/src/lib.rs:8871-8877) writes the full accept URL — including the invitation UUID, which alone is sufficient to set the invited user's password — to control-api logs. Anyone with log read access can claim pending invitations. (No email sender is configured; links are distributed manually, which is why they're logged.)

Fix: log only the invitation ID; surface the URL once in the admin UI/API response instead. Until then, treat control-api log access as security-sensitive.

9. Pin JWT validation algorithm instead of trusting the token header

services/control-api/src/lib.rs:2880 uses Validation::new(header.alg) for user-JWT validation. Safe today because the Better-Auth JWKS publishes only an asymmetric Ed25519 key, but pinning the expected algorithm family removes the alg-confusion class entirely (the inference-proxy already pins RS256 — mirror that).

10. Keep the production boot-time secret validation (verified working)

Repo contains dev-only defaults (admin@example.com / change-me-in-production, all-zero gateway encryption key in deploy/docker-compose.yml, committed test JWT signing key under tests/fixtures/). Verified not live: production boot validation (apps/dashboard/lib/runtime-config.mjs) refuses sample secrets, and a live login attempt with the default credentials was rejected. No action beyond keeping that validation mandatory and documenting the test-key exception (already in .github/security-exceptions.yml).


Verified-secure during the assessment (no action needed)

401s on all authenticated/admin/SCIM endpoints without credentials · SCIM fails closed · sign-up blocked (invitation required), password reset disabled (no enumeration channel) · sign-in rate limiting active · no CORS headers reflected for foreign origins · blue-cli OAuth client restricted to device grant (authorization_code refused → no redirect_uri open redirect) · no .git/.env exposure · TLS 1.0/1.1 rejected (1.2/1.3 only), HTTP→HTTPS everywhere · generic error responses · no dangling subdomains · deployed Next.js 16.3.4 includes fixes for the 2025 middleware-bypass and React Server Components RCE classes (verified by fingerprint; no payloads sent) · SQL fully parameterized (sqlx) · org-scoped ownership checks on object access · SSRF egress restricted to public IPs with DNS pinning · gateway credentials AES-256-GCM envelope-encrypted, decrypted only on the internal listener.

Limitations / suggested follow-up testing: no test account was available, so authenticated surfaces (org-member IDOR, session-upload sharing) were assessed from source only — a second pass with a throwaway invited account is recommended. Re-run dependency scanning (cargo audit, npm audit/osv-scanner) in CI for CVEs published after the assessment date.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions