Single-container tenant architecture: Go platform (:8080) + Canvas Node.js (:3000) in one Fly machine, with Go's NoRoute handler reverse- proxying non-API routes to the canvas. Browser only talks to :8080. Changes: platform/Dockerfile.tenant — multi-stage build (Go + Node + runtime). Bakes workspace-configs-templates/ + org-templates/ into the image. Build context: repo root. platform/entrypoint-tenant.sh — starts both processes, kills both if either exits. Fly health check on :8080 covers the Go binary; canvas health is implicit (proxy returns 502 if canvas is down). platform/internal/router/canvas_proxy.go — httputil.ReverseProxy that forwards unmatched routes to CANVAS_PROXY_URL (http://localhost:3000). Activated by NoRoute when CANVAS_PROXY_URL env is set. platform/internal/router/router.go — wire NoRoute → canvasProxy when CANVAS_PROXY_URL is present; no-op otherwise (local dev unchanged). platform/internal/middleware/securityheaders.go — relaxed CSP to allow Next.js inline scripts/styles/eval + WebSocket + data: URIs. The strict `default-src 'self'` was blocking all canvas rendering. canvas/src/lib/api.ts — changed `||` to `??` for NEXT_PUBLIC_PLATFORM_URL so empty string means "same-origin" (combined image) instead of falling back to localhost:8080. canvas/src/components/tabs/TerminalTab.tsx — same `??` fix for WS URL. Verified: tenant machine boots, canvas renders, 8 runtime templates + 4 org templates visible, API routes work through the same port. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
54 lines
2.7 KiB
Go
54 lines
2.7 KiB
Go
package middleware
|
|
|
|
import "github.com/gin-gonic/gin"
|
|
|
|
// SecurityHeaders returns a Gin middleware that sets standard HTTP security
|
|
// headers on every response to mitigate common web-application attacks:
|
|
//
|
|
// - X-Content-Type-Options: nosniff — prevents MIME-type sniffing
|
|
// - X-Frame-Options: DENY — blocks iframe embedding (clickjacking)
|
|
// - Content-Security-Policy: default-src 'self' — restricts resource loading to same origin
|
|
// - Strict-Transport-Security: max-age=31536000; includeSubDomains — enforces HTTPS for 1 year
|
|
// - Referrer-Policy: strict-origin-when-cross-origin — avoids leaking full paths/queries in Referer
|
|
// - Permissions-Policy: camera=(), microphone=(), geolocation=() — denies sensor access for embedded content
|
|
func SecurityHeaders() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
c.Header("X-Frame-Options", "DENY")
|
|
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
// #282: these two were documented in CLAUDE.md but missing from
|
|
// the middleware. Referrer-Policy prevents browsers from leaking
|
|
// the full Referer URL to cross-origin resources (which can
|
|
// expose internal paths/queries). Permissions-Policy denies
|
|
// sensor access by default — especially relevant because the
|
|
// canvas embeds iframes for Langfuse traces.
|
|
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
c.Header("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
|
|
|
// CSP: only apply to API responses. Canvas-proxied routes
|
|
// (NoRoute → reverse-proxy to Next.js) serve HTML with inline
|
|
// scripts + styles that `default-src 'self'` blocks. Next.js
|
|
// sets its own CSP via <meta> tags. The Go middleware should
|
|
// not override it for proxied HTML responses.
|
|
//
|
|
// Detection: API routes are registered explicitly in the router;
|
|
// canvas-proxied routes hit NoRoute. We can't detect NoRoute
|
|
// before c.Next() fires, so instead we check the response
|
|
// Content-Type after Next() — but that's too late for headers.
|
|
//
|
|
// Simpler: apply a permissive CSP that allows Next.js to work.
|
|
// 'unsafe-inline' + 'unsafe-eval' are needed for Next.js dev
|
|
// hydration; in production Next.js uses nonces but we don't
|
|
// propagate them through the proxy. This is acceptable because
|
|
// the canvas is our own code, not user-generated content.
|
|
c.Header("Content-Security-Policy",
|
|
"default-src 'self'; "+
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "+
|
|
"style-src 'self' 'unsafe-inline'; "+
|
|
"img-src 'self' data: blob:; "+
|
|
"connect-src 'self' ws: wss:; "+
|
|
"font-src 'self' data:")
|
|
c.Next()
|
|
}
|
|
}
|