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.
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.shis served with noContent-Security-Policy/frame-ancestorsand noX-Frame-Options(verified live on/loginand/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 withgovernance:read/session:write/client-status:writescopes as that user.Fix: emit
Content-Security-Policy: frame-ancestors 'none'(or'self') andX-Frame-Options: DENYfrom aheaders()block inapps/dashboard/next.config.ts.2. Missing security headers across all surfaces
None of
app.bluee.sh,api.bluee.sh, orbluee.shcurrently send:Strict-Transport-Security— all hosts already 301 HTTP→HTTPS, so preload-readyX-Content-Type-Options: nosniffReferrer-Policy— URLs carry security-relevant tokens (invitation IDs, device tokens) that can leak viaRefererto third-party resourcesPermissions-PolicyContent-Security-Policy— hardening only; the dashboard currently avoidsdangerouslySetInnerHTML, no active XSS foundFix: central
headers()inapps/dashboard/next.config.ts; ALB/ingress response-header policy forapi.bluee.sh; nginx config for the static apex (bluee.sh).Low
3. Public
/metricsand verbose/health/*onapi.bluee.shUnauthenticated (all verified 200 from the internet):
GET /metrics— DB pool sizes, KMS permit counts, TLS reload success/error counters (operational fingerprinting; a spike intls_reload_errors_totalis also an attack-timing signal)GET /health/schema(DB migration state),/health/worker,/health/object-storage,/health/credential-resolver— internal component status oracleFix: keep bare
/healthand/readypublic for the ALB; move/metricsand 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.rspublic 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.shand/scim/v2, or a tower middleware layer in control-api.5. Technology/version disclosure
x-powered-by: Next.jsheader on the dashboard, and the exact version (16.3.4 at assessment time) recoverable from JS chunk contentserver: nginx/1.27.5onbluee.shFix:
poweredByHeader: falseinnext.config.ts;server_tokens off;in the nginx server block.Informational
6. No
security.txt/.well-known/security.txtreturns 404 on app and apex. For an enterprise-targeted open-source project, publish one pointing at the contact/policy inSECURITY.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:77setsHARNESS_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:2880usesValidation::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 indeploy/docker-compose.yml, committed test JWT signing key undertests/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-cliOAuth client restricted to device grant (authorization_coderefused → noredirect_uriopen redirect) · no.git/.envexposure · 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.