Config injection prevention: - Add FQDN validation for domain names (RFC 1123) in Register/Update — rejects newlines, spaces, shell metacharacters that could inject into HAProxy/dnsmasq configs - Add backend address validation (valid host:port format, valid IP or hostname, port 1-65535). DNS-only backends allow bare IPs. - Add header key/value validation — keys must be HTTP token chars only, values must not contain newlines or NULs - Add WireGuard peer name validation (alphanumeric + hyphens + underscores) - Add defense-in-depth domain validation in certbot Provision() Secrets protection: - Remove ?raw=true bypass on GET /api/v1/secrets — secrets are now always redacted in API responses regardless of query parameters - Update test to verify redaction cannot be bypassed NATS authentication: - Generate random auth token on first startup, store in secrets.yaml - Pass token to embedded NATS server via Authorization option - Internal client connects with the same token - External NATS clients (Wild Cloud) must now authenticate Security headers: - Add X-Content-Type-Options: nosniff - Add X-Frame-Options: DENY - Add Cache-Control: no-store
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
package v1
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// statusResponseWriter wraps http.ResponseWriter to capture the status code.
|
|
// Implements http.Hijacker so WebSocket upgrades work through the proxy.
|
|
type statusResponseWriter struct {
|
|
http.ResponseWriter
|
|
status int
|
|
}
|
|
|
|
func (w *statusResponseWriter) WriteHeader(code int) {
|
|
w.status = code
|
|
w.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func (w *statusResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
if hj, ok := w.ResponseWriter.(http.Hijacker); ok {
|
|
return hj.Hijack()
|
|
}
|
|
return nil, nil, fmt.Errorf("upstream ResponseWriter does not implement http.Hijacker")
|
|
}
|
|
|
|
func (w *statusResponseWriter) Flush() {
|
|
if fl, ok := w.ResponseWriter.(http.Flusher); ok {
|
|
fl.Flush()
|
|
}
|
|
}
|
|
|
|
// RequestLoggingMiddleware logs method, path, status, and duration for each request.
|
|
// Long-lived connections (SSE, WebSocket) are excluded.
|
|
func RequestLoggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Path
|
|
|
|
// Skip SSE and WebSocket endpoints (long-lived connections)
|
|
if strings.HasSuffix(path, "/events") || strings.HasSuffix(path, "/ws") || strings.HasSuffix(path, "/stream") {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Security response headers
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
|
|
start := time.Now()
|
|
sw := &statusResponseWriter{ResponseWriter: w, status: http.StatusOK}
|
|
next.ServeHTTP(sw, r)
|
|
|
|
attrs := []any{
|
|
"status", sw.status,
|
|
"method", r.Method,
|
|
"path", path,
|
|
"duration", time.Since(start),
|
|
}
|
|
|
|
// Add route params if present
|
|
vars := mux.Vars(r)
|
|
if domain := vars["domain"]; domain != "" {
|
|
attrs = append(attrs, "domain", domain)
|
|
}
|
|
|
|
if sw.status >= 400 {
|
|
slog.Error("request", attrs...)
|
|
} else {
|
|
slog.Info("request", attrs...)
|
|
}
|
|
})
|
|
}
|