From 50c632babf10f7d055d8399377fdd4098b4dd0a7 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 31 Jan 2026 13:06:50 -0500 Subject: [PATCH] Update SSL/TLS handling for Docker reverse proxy deployments - Disable HTTPSRedirectMiddleware (SSL handled by proxy) - Keep SSLProxyMiddleware for X-Forwarded-* headers - Add note about Docker deployment architecture - Database connections use sslmode=disable - No redirect needed for reverse proxy setup --- internal/middleware/security.go | 150 ++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 internal/middleware/security.go diff --git a/internal/middleware/security.go b/internal/middleware/security.go new file mode 100644 index 0000000..04e91bf --- /dev/null +++ b/internal/middleware/security.go @@ -0,0 +1,150 @@ +package middleware + +import ( + "net/http" + "strconv" + "strings" + + "github.com/labstack/echo/v4" +) + +// SecurityHeadersMiddleware adds security headers to all responses +func SecurityHeadersMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // Add security headers + c.Response().Header().Set("X-Content-Type-Options", "nosniff") + c.Response().Header().Set("X-Frame-Options", "DENY") + c.Response().Header().Set("X-XSS-Protection", "1; mode=block") + c.Response().Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") + c.Response().Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:") + c.Response().Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + c.Response().Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") + + return next(c) + } + } +} + +// HTTPSRedirectMiddleware redirects HTTP to HTTPS +// NOTE: Disabled for Docker self-hosted deployments - SSL is handled by reverse proxy +func HTTPSRedirectMiddleware(httpsPort string) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // SSL is handled by proxy - no redirect needed + return next(c) + } + } +} + +// isTestMode checks if the application is running in test mode +func isTestMode(c echo.Context) bool { + // Check for test mode header or environment + return c.Request().Header.Get("X-Test-Mode") == "true" || + c.Request().Header.Get("X-Forwarded-Proto") == "http" +} + +// SSLProxyMiddleware handles SSL termination proxy headers +// NOTE: Essential for Docker deployments where SSL is handled by reverse proxy (nginx, traefik, etc.) +// This middleware reads X-Forwarded-Proto and X-Forwarded-Host headers set by the proxy +func SSLProxyMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // Check for proxy headers + if proto := c.Request().Header.Get("X-Forwarded-Proto"); proto == "https" { + c.Request().URL.Scheme = "https" + } + + if host := c.Request().Header.Get("X-Forwarded-Host"); host != "" { + c.Request().URL.Host = host + } + + return next(c) + } + } +} + +// HTTPSProtectionMiddleware provides HTTPS protection for production +func HTTPSProtectionMiddleware(enableRedirect bool, httpsPort string) []echo.MiddlewareFunc { + return []echo.MiddlewareFunc{ + SSLProxyMiddleware(), + SecurityHeadersMiddleware(), + HTTPSRedirectMiddleware(httpsPort), + } +} + +// CORSSecurityConfig configures CORS with security best practices +type CORSSecurityConfig struct { + AllowedOrigins []string + AllowedMethods []string + AllowedHeaders []string + ExposedHeaders []string + AllowCredentials bool + MaxAge int +} + +// NewSecureCORSConfig creates production-ready CORS config +func NewSecureCORSConfig() CORSSecurityConfig { + return CORSSecurityConfig{ + AllowedOrigins: []string{ + "https://bookmann.example.com", + "https://*.bookmann.example.com", + }, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{ + "Accept", + "Authorization", + "Content-Type", + "X-Request-ID", + "X-Timestamp", + "X-Signature", + "X-Device-ID", + }, + ExposedHeaders: []string{ + "X-Request-ID", + "X-Sync-Status", + "X-Conflict-Detected", + }, + AllowCredentials: true, + MaxAge: 86400, // 24 hours + } +} + +// SecureCORSMiddleware creates CORS middleware with security +func SecureCORSMiddleware(config CORSSecurityConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + origin := c.Request().Header.Get("Origin") + + // Check if origin is allowed + allowed := false + for _, allowedOrigin := range config.AllowedOrigins { + if origin == allowedOrigin || allowedOrigin == "*" { + allowed = true + break + } + } + + if !allowed { + return c.JSON(http.StatusForbidden, map[string]string{ + "error": "Origin not allowed", + }) + } + + // Set CORS headers + c.Response().Header().Set("Access-Control-Allow-Origin", origin) + c.Response().Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", ")) + c.Response().Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", ")) + c.Response().Header().Set("Access-Control-Expose-Headers", strings.Join(config.ExposedHeaders, ", ")) + c.Response().Header().Set("Access-Control-Allow-Credentials", "true") + c.Response().Header().Set("Access-Control-Max-Age", strconv.Itoa(config.MaxAge)) + + // Handle preflight + if c.Request().Method == "OPTIONS" { + return c.NoContent(http.StatusNoContent) + } + + return next(c) + } + } +}