Files
Charon/docs/plans/current_spec.md
GitHub Actions 8cf762164f feat: implement HTTP Security Headers management (Issue #20)
Add comprehensive security header management system with reusable
profiles, interactive builders, and security scoring.

Features:
- SecurityHeaderProfile model with 11+ header types
- CRUD API with 10 endpoints (/api/v1/security/headers/*)
- Caddy integration for automatic header injection
- 3 built-in presets (Basic, Strict, Paranoid)
- Security score calculator (0-100) with suggestions
- Interactive CSP builder with validation
- Permissions-Policy builder
- Real-time security score preview
- Per-host profile assignment

Headers Supported:
- HSTS with preload support
- Content-Security-Policy with report-only mode
- X-Frame-Options, X-Content-Type-Options
- Referrer-Policy, Permissions-Policy
- Cross-Origin-Opener/Resource/Embedder-Policy
- X-XSS-Protection, Cache-Control security

Implementation:
- Backend: models, handlers, services (85% coverage)
- Frontend: React components, hooks (87.46% coverage)
- Tests: 1,163 total tests passing
- Docs: Comprehensive feature documentation

Closes #20
2025-12-19 18:55:48 +00:00

40 KiB

HTTP Security Headers Implementation Plan (Issue #20)

Created: December 18, 2025 Status: Planning Complete - Ready for Implementation Issue: GitHub Issue #20 - HTTP Security Headers


Executive Summary

This plan implements automatic security header injection for proxy hosts in Charon. The feature enables users to configure HTTP security headers (HSTS, CSP, X-Frame-Options, etc.) through presets or custom configurations, with a security score calculator to help users understand their security posture.

Key Goals:

  • Zero-config secure defaults for novice users
  • Granular control for advanced users
  • Visual security score feedback
  • Per-host and global configuration options
  • CSP builder that doesn't break sites

Architecture Overview

Data Flow

┌──────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Frontend   │────▶│  Backend API │────▶│    Models    │────▶│    Caddy     │
│  UI/Config   │     │   Handlers   │     │  (SQLite)    │     │ JSON Config  │
└──────────────┘     └──────────────┘     └──────────────┘     └──────────────┘
        │                    │                    │                    │
        │                    │                    │                    │
   ┌────▼────┐          ┌────▼────┐         ┌────▼────┐          ┌────▼────┐
   │ Presets │          │ CRUD    │         │ Profile │          │ Headers │
   │ Selector│          │ Profile │         │ Storage │          │ Handler │
   │ Score   │          │ Score   │         │ Host FK │          │ Inject  │
   └─────────┘          └─────────┘         └─────────┘          └─────────┘

Key Design Decisions

  1. Security Header Profiles - Reusable configurations that can be shared across hosts
  2. Per-Host Override - Each proxy host can reference a profile OR have inline settings
  3. Presets System - Pre-built profiles (basic, strict, paranoid) for easy setup
  4. Score Calculator - Visual feedback showing security level (0-100)
  5. CSP Builder - Interactive tool to build Content-Security-Policy without breaking sites

Phase 1: Backend Models and Migrations

1.1 New Model: SecurityHeaderProfile

File: backend/internal/models/security_header_profile.go

package models

import (
    "time"
)

// SecurityHeaderProfile stores reusable security header configurations.
// Users can create profiles and assign them to proxy hosts.
type SecurityHeaderProfile struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    UUID      string    `json:"uuid" gorm:"uniqueIndex;not null"`
    Name      string    `json:"name" gorm:"index;not null"`

    // HSTS Configuration
    HSTSEnabled           bool   `json:"hsts_enabled" gorm:"default:true"`
    HSTSMaxAge            int    `json:"hsts_max_age" gorm:"default:31536000"` // 1 year in seconds
    HSTSIncludeSubdomains bool   `json:"hsts_include_subdomains" gorm:"default:true"`
    HSTSPreload           bool   `json:"hsts_preload" gorm:"default:false"`

    // Content-Security-Policy
    CSPEnabled    bool   `json:"csp_enabled" gorm:"default:false"`
    CSPDirectives string `json:"csp_directives" gorm:"type:text"` // JSON object of CSP directives
    CSPReportOnly bool   `json:"csp_report_only" gorm:"default:false"`
    CSPReportURI  string `json:"csp_report_uri"`

    // X-Frame-Options
    XFrameOptions string `json:"x_frame_options" gorm:"default:DENY"` // DENY, SAMEORIGIN, or empty

    // X-Content-Type-Options
    XContentTypeOptions bool `json:"x_content_type_options" gorm:"default:true"` // nosniff

    // Referrer-Policy
    ReferrerPolicy string `json:"referrer_policy" gorm:"default:strict-origin-when-cross-origin"`

    // Permissions-Policy (formerly Feature-Policy)
    PermissionsPolicy string `json:"permissions_policy" gorm:"type:text"` // JSON array of policies

    // Cross-Origin Headers
    CrossOriginOpenerPolicy   string `json:"cross_origin_opener_policy" gorm:"default:same-origin"`
    CrossOriginResourcePolicy string `json:"cross_origin_resource_policy" gorm:"default:same-origin"`
    CrossOriginEmbedderPolicy string `json:"cross_origin_embedder_policy"` // require-corp or empty

    // X-XSS-Protection (legacy but still useful)
    XSSProtection bool `json:"xss_protection" gorm:"default:true"`

    // Cache-Control for security
    CacheControlNoStore bool `json:"cache_control_no_store" gorm:"default:false"`

    // Computed Security Score (0-100)
    SecurityScore int `json:"security_score" gorm:"default:0"`

    // Metadata
    IsPreset    bool   `json:"is_preset" gorm:"default:false"` // System presets can't be deleted
    PresetType  string `json:"preset_type"`                    // "basic", "strict", "paranoid", or empty for custom
    Description string `json:"description" gorm:"type:text"`

    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// CSPDirective represents a single CSP directive for the builder
type CSPDirective struct {
    Directive string   `json:"directive"` // e.g., "default-src", "script-src"
    Values    []string `json:"values"`    // e.g., ["'self'", "https:"]
}

// PermissionsPolicyItem represents a single Permissions-Policy entry
type PermissionsPolicyItem struct {
    Feature   string   `json:"feature"`   // e.g., "camera", "microphone"
    Allowlist []string `json:"allowlist"` // e.g., ["self"], ["*"], []
}

1.2 Update ProxyHost Model

File: backend/internal/models/proxy_host.go

Add the following fields to ProxyHost:

// Security Headers Configuration
// Either reference a profile OR use inline settings
SecurityHeaderProfileID *uint                  `json:"security_header_profile_id"`
SecurityHeaderProfile   *SecurityHeaderProfile `json:"security_header_profile" gorm:"foreignKey:SecurityHeaderProfileID"`

// Inline security header settings (used when no profile is selected)
// These override profile settings if both are set
SecurityHeadersEnabled bool   `json:"security_headers_enabled" gorm:"default:true"`
SecurityHeadersCustom  string `json:"security_headers_custom" gorm:"type:text"` // JSON for custom headers

1.3 Migration Updates

File: backend/internal/api/routes/routes.go

Add to AutoMigrate:

&models.SecurityHeaderProfile{},

1.4 Unit Tests

File: backend/internal/models/security_header_profile_test.go

Test cases:

  • Profile creation with default values
  • JSON serialization/deserialization of CSPDirectives
  • JSON serialization/deserialization of PermissionsPolicy
  • UUID generation on creation
  • Security score calculation triggers

Phase 2: Backend API Handlers

2.1 Security Headers Handler

File: backend/internal/api/handlers/security_headers_handler.go

package handlers

// SecurityHeadersHandler manages security header profiles
type SecurityHeadersHandler struct {
    db           *gorm.DB
    caddyManager *caddy.Manager
}

// NewSecurityHeadersHandler creates a new handler
func NewSecurityHeadersHandler(db *gorm.DB, caddyManager *caddy.Manager) *SecurityHeadersHandler

// RegisterRoutes registers all security headers routes
func (h *SecurityHeadersHandler) RegisterRoutes(router *gin.RouterGroup)

// --- Profile CRUD ---

// ListProfiles returns all security header profiles
// GET /api/v1/security/headers/profiles
func (h *SecurityHeadersHandler) ListProfiles(c *gin.Context)

// GetProfile returns a single profile by ID or UUID
// GET /api/v1/security/headers/profiles/:id
func (h *SecurityHeadersHandler) GetProfile(c *gin.Context)

// CreateProfile creates a new security header profile
// POST /api/v1/security/headers/profiles
func (h *SecurityHeadersHandler) CreateProfile(c *gin.Context)

// UpdateProfile updates an existing profile
// PUT /api/v1/security/headers/profiles/:id
func (h *SecurityHeadersHandler) UpdateProfile(c *gin.Context)

// DeleteProfile deletes a profile (not presets)
// DELETE /api/v1/security/headers/profiles/:id
func (h *SecurityHeadersHandler) DeleteProfile(c *gin.Context)

// --- Presets ---

// GetPresets returns the list of built-in presets
// GET /api/v1/security/headers/presets
func (h *SecurityHeadersHandler) GetPresets(c *gin.Context)

// ApplyPreset applies a preset to create/update a profile
// POST /api/v1/security/headers/presets/apply
func (h *SecurityHeadersHandler) ApplyPreset(c *gin.Context)

// --- Security Score ---

// CalculateScore calculates security score for given settings
// POST /api/v1/security/headers/score
func (h *SecurityHeadersHandler) CalculateScore(c *gin.Context)

// --- CSP Builder ---

// ValidateCSP validates a CSP string
// POST /api/v1/security/headers/csp/validate
func (h *SecurityHeadersHandler) ValidateCSP(c *gin.Context)

// BuildCSP builds a CSP string from directives
// POST /api/v1/security/headers/csp/build
func (h *SecurityHeadersHandler) BuildCSP(c *gin.Context)

2.2 API Routes Registration

File: backend/internal/api/routes/routes.go

Add to the protected routes section:

// Security Headers
securityHeadersHandler := handlers.NewSecurityHeadersHandler(db, caddyManager)
securityHeadersHandler.RegisterRoutes(protected)

2.3 Request/Response Types

// CreateProfileRequest for creating a new profile
type CreateProfileRequest struct {
    Name                      string `json:"name" binding:"required"`
    HSTSEnabled               bool   `json:"hsts_enabled"`
    HSTSMaxAge                int    `json:"hsts_max_age"`
    HSTSIncludeSubdomains     bool   `json:"hsts_include_subdomains"`
    HSTSPreload               bool   `json:"hsts_preload"`
    CSPEnabled                bool   `json:"csp_enabled"`
    CSPDirectives             string `json:"csp_directives"`
    CSPReportOnly             bool   `json:"csp_report_only"`
    CSPReportURI              string `json:"csp_report_uri"`
    XFrameOptions             string `json:"x_frame_options"`
    XContentTypeOptions       bool   `json:"x_content_type_options"`
    ReferrerPolicy            string `json:"referrer_policy"`
    PermissionsPolicy         string `json:"permissions_policy"`
    CrossOriginOpenerPolicy   string `json:"cross_origin_opener_policy"`
    CrossOriginResourcePolicy string `json:"cross_origin_resource_policy"`
    CrossOriginEmbedderPolicy string `json:"cross_origin_embedder_policy"`
    XSSProtection             bool   `json:"xss_protection"`
    CacheControlNoStore       bool   `json:"cache_control_no_store"`
    Description               string `json:"description"`
}

// CalculateScoreRequest for scoring
type CalculateScoreRequest struct {
    HSTSEnabled               bool   `json:"hsts_enabled"`
    HSTSMaxAge                int    `json:"hsts_max_age"`
    HSTSIncludeSubdomains     bool   `json:"hsts_include_subdomains"`
    HSTSPreload               bool   `json:"hsts_preload"`
    CSPEnabled                bool   `json:"csp_enabled"`
    CSPDirectives             string `json:"csp_directives"`
    XFrameOptions             string `json:"x_frame_options"`
    XContentTypeOptions       bool   `json:"x_content_type_options"`
    ReferrerPolicy            string `json:"referrer_policy"`
    PermissionsPolicy         string `json:"permissions_policy"`
    CrossOriginOpenerPolicy   string `json:"cross_origin_opener_policy"`
    CrossOriginResourcePolicy string `json:"cross_origin_resource_policy"`
    XSSProtection             bool   `json:"xss_protection"`
}

// ScoreResponse returns the calculated score
type ScoreResponse struct {
    Score       int            `json:"score"`
    MaxScore    int            `json:"max_score"`
    Breakdown   map[string]int `json:"breakdown"`
    Suggestions []string       `json:"suggestions"`
}

2.4 Unit Tests

File: backend/internal/api/handlers/security_headers_handler_test.go

Test cases:

  • TestListProfiles - List all profiles
  • TestGetProfile - Get profile by ID
  • TestGetProfileByUUID - Get profile by UUID
  • TestCreateProfile - Create new profile
  • TestUpdateProfile - Update existing profile
  • TestDeleteProfile - Delete custom profile
  • TestDeletePresetFails - Cannot delete system presets
  • TestGetPresets - List all presets
  • TestApplyPreset - Apply preset to create profile
  • TestCalculateScore - Score calculation
  • TestValidateCSP - CSP validation
  • TestBuildCSP - CSP building

Phase 3: Caddy Integration for Header Injection

3.1 Header Builder Function

File: backend/internal/caddy/config.go

Add new function:

// buildSecurityHeadersHandler creates a headers handler for security headers
// based on the profile configuration or host-level settings
func buildSecurityHeadersHandler(host *models.ProxyHost, profile *models.SecurityHeaderProfile) (Handler, error) {
    if profile == nil && !host.SecurityHeadersEnabled {
        return nil, nil
    }

    responseHeaders := make(map[string][]string)

    // Use profile settings or host defaults
    var cfg *models.SecurityHeaderProfile
    if profile != nil {
        cfg = profile
    } else {
        // Use host's inline settings or defaults
        cfg = getDefaultSecurityHeaderProfile()
    }

    // HSTS
    if cfg.HSTSEnabled {
        hstsValue := fmt.Sprintf("max-age=%d", cfg.HSTSMaxAge)
        if cfg.HSTSIncludeSubdomains {
            hstsValue += "; includeSubDomains"
        }
        if cfg.HSTSPreload {
            hstsValue += "; preload"
        }
        responseHeaders["Strict-Transport-Security"] = []string{hstsValue}
    }

    // CSP
    if cfg.CSPEnabled && cfg.CSPDirectives != "" {
        cspHeader := "Content-Security-Policy"
        if cfg.CSPReportOnly {
            cspHeader = "Content-Security-Policy-Report-Only"
        }
        responseHeaders[cspHeader] = []string{buildCSPString(cfg.CSPDirectives)}
    }

    // X-Frame-Options
    if cfg.XFrameOptions != "" {
        responseHeaders["X-Frame-Options"] = []string{cfg.XFrameOptions}
    }

    // X-Content-Type-Options
    if cfg.XContentTypeOptions {
        responseHeaders["X-Content-Type-Options"] = []string{"nosniff"}
    }

    // Referrer-Policy
    if cfg.ReferrerPolicy != "" {
        responseHeaders["Referrer-Policy"] = []string{cfg.ReferrerPolicy}
    }

    // Permissions-Policy
    if cfg.PermissionsPolicy != "" {
        responseHeaders["Permissions-Policy"] = []string{buildPermissionsPolicyString(cfg.PermissionsPolicy)}
    }

    // Cross-Origin headers
    if cfg.CrossOriginOpenerPolicy != "" {
        responseHeaders["Cross-Origin-Opener-Policy"] = []string{cfg.CrossOriginOpenerPolicy}
    }
    if cfg.CrossOriginResourcePolicy != "" {
        responseHeaders["Cross-Origin-Resource-Policy"] = []string{cfg.CrossOriginResourcePolicy}
    }
    if cfg.CrossOriginEmbedderPolicy != "" {
        responseHeaders["Cross-Origin-Embedder-Policy"] = []string{cfg.CrossOriginEmbedderPolicy}
    }

    // X-XSS-Protection
    if cfg.XSSProtection {
        responseHeaders["X-XSS-Protection"] = []string{"1; mode=block"}
    }

    // Cache-Control
    if cfg.CacheControlNoStore {
        responseHeaders["Cache-Control"] = []string{"no-store"}
    }

    if len(responseHeaders) == 0 {
        return nil, nil
    }

    return Handler{
        "handler": "headers",
        "response": map[string]interface{}{
            "set": responseHeaders,
        },
    }, nil
}

// buildCSPString converts JSON CSP directives to a CSP string
func buildCSPString(directivesJSON string) string

// buildPermissionsPolicyString converts JSON permissions to policy string
func buildPermissionsPolicyString(permissionsJSON string) string

// getDefaultSecurityHeaderProfile returns secure defaults
func getDefaultSecurityHeaderProfile() *models.SecurityHeaderProfile

3.2 Update GenerateConfig

File: backend/internal/caddy/config.go

In the GenerateConfig function, add security headers handler to the handler chain:

// After building securityHandlers and before the main reverse proxy handler:

// Add Security Headers handler
if secHeadersHandler, err := buildSecurityHeadersHandler(&host, host.SecurityHeaderProfile); err == nil && secHeadersHandler != nil {
    handlers = append(handlers, secHeadersHandler)
}

3.3 Update Manager to Load Profiles

File: backend/internal/caddy/manager.go

Update ApplyConfig to preload security header profiles:

// Preload security header profiles for hosts
var profiles []models.SecurityHeaderProfile
if err := m.db.Find(&profiles).Error; err != nil {
    return fmt.Errorf("failed to load security header profiles: %w", err)
}
profileMap := make(map[uint]*models.SecurityHeaderProfile)
for i := range profiles {
    profileMap[profiles[i].ID] = &profiles[i]
}

// Attach profiles to hosts
for i := range hosts {
    if hosts[i].SecurityHeaderProfileID != nil {
        hosts[i].SecurityHeaderProfile = profileMap[*hosts[i].SecurityHeaderProfileID]
    }
}

3.4 Unit Tests

File: backend/internal/caddy/config_security_headers_test.go

Test cases:

  • TestBuildSecurityHeadersHandler_AllEnabled - All headers enabled
  • TestBuildSecurityHeadersHandler_HSTSOnly - Only HSTS
  • TestBuildSecurityHeadersHandler_CSPOnly - Only CSP
  • TestBuildSecurityHeadersHandler_CSPReportOnly - CSP in report-only mode
  • TestBuildSecurityHeadersHandler_NoProfile - Default behavior
  • TestBuildSecurityHeadersHandler_Disabled - Headers disabled
  • TestBuildCSPString - CSP JSON to string conversion
  • TestBuildPermissionsPolicyString - Permissions JSON to string
  • TestGenerateConfig_WithSecurityHeaders - Full integration

Phase 4: Frontend UI Components

4.1 API Client

File: frontend/src/api/securityHeaders.ts

import client from './client'

// Types
export interface SecurityHeaderProfile {
  id: number
  uuid: string
  name: string
  hsts_enabled: boolean
  hsts_max_age: number
  hsts_include_subdomains: boolean
  hsts_preload: boolean
  csp_enabled: boolean
  csp_directives: string
  csp_report_only: boolean
  csp_report_uri: string
  x_frame_options: string
  x_content_type_options: boolean
  referrer_policy: string
  permissions_policy: string
  cross_origin_opener_policy: string
  cross_origin_resource_policy: string
  cross_origin_embedder_policy: string
  xss_protection: boolean
  cache_control_no_store: boolean
  security_score: number
  is_preset: boolean
  preset_type: string
  description: string
  created_at: string
  updated_at: string
}

export interface SecurityHeaderPreset {
  type: 'basic' | 'strict' | 'paranoid'
  name: string
  description: string
  score: number
  config: Partial<SecurityHeaderProfile>
}

export interface ScoreBreakdown {
  score: number
  max_score: number
  breakdown: Record<string, number>
  suggestions: string[]
}

export interface CSPDirective {
  directive: string
  values: string[]
}

// API Functions
export const listProfiles = async (): Promise<{ profiles: SecurityHeaderProfile[] }> => {
  const response = await client.get('/security/headers/profiles')
  return response.data
}

export const getProfile = async (id: number | string): Promise<{ profile: SecurityHeaderProfile }> => {
  const response = await client.get(`/security/headers/profiles/${id}`)
  return response.data
}

export const createProfile = async (data: Partial<SecurityHeaderProfile>): Promise<{ profile: SecurityHeaderProfile }> => {
  const response = await client.post('/security/headers/profiles', data)
  return response.data
}

export const updateProfile = async (id: number, data: Partial<SecurityHeaderProfile>): Promise<{ profile: SecurityHeaderProfile }> => {
  const response = await client.put(`/security/headers/profiles/${id}`, data)
  return response.data
}

export const deleteProfile = async (id: number): Promise<{ deleted: boolean }> => {
  const response = await client.delete(`/security/headers/profiles/${id}`)
  return response.data
}

export const getPresets = async (): Promise<{ presets: SecurityHeaderPreset[] }> => {
  const response = await client.get('/security/headers/presets')
  return response.data
}

export const applyPreset = async (presetType: string, name: string): Promise<{ profile: SecurityHeaderProfile }> => {
  const response = await client.post('/security/headers/presets/apply', { preset_type: presetType, name })
  return response.data
}

export const calculateScore = async (config: Partial<SecurityHeaderProfile>): Promise<ScoreBreakdown> => {
  const response = await client.post('/security/headers/score', config)
  return response.data
}

export const validateCSP = async (csp: string): Promise<{ valid: boolean; errors: string[] }> => {
  const response = await client.post('/security/headers/csp/validate', { csp })
  return response.data
}

export const buildCSP = async (directives: CSPDirective[]): Promise<{ csp: string }> => {
  const response = await client.post('/security/headers/csp/build', { directives })
  return response.data
}

4.2 React Query Hooks

File: frontend/src/hooks/useSecurityHeaders.ts

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as api from '../api/securityHeaders'
import toast from 'react-hot-toast'

export function useSecurityHeaderProfiles() {
  return useQuery({
    queryKey: ['securityHeaderProfiles'],
    queryFn: api.listProfiles,
  })
}

export function useSecurityHeaderProfile(id: number | string) {
  return useQuery({
    queryKey: ['securityHeaderProfile', id],
    queryFn: () => api.getProfile(id),
    enabled: !!id,
  })
}

export function useCreateSecurityHeaderProfile() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: api.createProfile,
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
      toast.success('Profile created')
    },
    onError: (err: Error) => {
      toast.error(`Failed to create profile: ${err.message}`)
    },
  })
}

export function useUpdateSecurityHeaderProfile() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: ({ id, data }: { id: number; data: Partial<api.SecurityHeaderProfile> }) =>
      api.updateProfile(id, data),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
      toast.success('Profile updated')
    },
    onError: (err: Error) => {
      toast.error(`Failed to update profile: ${err.message}`)
    },
  })
}

export function useDeleteSecurityHeaderProfile() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: api.deleteProfile,
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
      toast.success('Profile deleted')
    },
    onError: (err: Error) => {
      toast.error(`Failed to delete profile: ${err.message}`)
    },
  })
}

export function useSecurityHeaderPresets() {
  return useQuery({
    queryKey: ['securityHeaderPresets'],
    queryFn: api.getPresets,
  })
}

export function useApplySecurityHeaderPreset() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: ({ presetType, name }: { presetType: string; name: string }) =>
      api.applyPreset(presetType, name),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
      toast.success('Preset applied')
    },
    onError: (err: Error) => {
      toast.error(`Failed to apply preset: ${err.message}`)
    },
  })
}

export function useCalculateSecurityScore() {
  return useMutation({
    mutationFn: api.calculateScore,
  })
}

export function useValidateCSP() {
  return useMutation({
    mutationFn: api.validateCSP,
  })
}

export function useBuildCSP() {
  return useMutation({
    mutationFn: api.buildCSP,
  })
}

4.3 Security Headers Page

File: frontend/src/pages/SecurityHeaders.tsx

Main page with:

  • List of custom profiles
  • Preset cards (Basic, Strict, Paranoid)
  • Create/Edit profile modal
  • Security score display
  • Delete confirmation dialog

4.4 Security Header Profile Form

File: frontend/src/components/SecurityHeaderProfileForm.tsx

Form component with:

  • Name input
  • HSTS section (enable, max-age, subdomains, preload)
  • CSP section with builder
  • X-Frame-Options selector
  • X-Content-Type-Options toggle
  • Referrer-Policy selector
  • Permissions-Policy builder
  • Cross-Origin headers section
  • Live security score preview
  • Save/Cancel buttons

4.5 CSP Builder Component

File: frontend/src/components/CSPBuilder.tsx

Interactive CSP builder:

  • Directive selector dropdown
  • Value input with suggestions
  • Add/Remove directives
  • Preview CSP string
  • Validate button
  • Common presets (default strict, allow scripts, etc.)

4.6 Security Score Display

File: frontend/src/components/SecurityScoreDisplay.tsx

Visual component showing:

  • Circular progress indicator (0-100)
  • Color coding (red < 50, yellow 50-75, green > 75)
  • Score breakdown by category
  • Suggestions for improvement

4.7 Permissions Policy Builder

File: frontend/src/components/PermissionsPolicyBuilder.tsx

Interactive builder for:

  • Feature selector (camera, microphone, geolocation, etc.)
  • Allowlist selector (none, self, all, specific origins)
  • Preview policy string

4.8 Update ProxyHostForm

File: frontend/src/components/ProxyHostForm.tsx

Add to the form:

  • "Security Headers" section
  • Profile selector dropdown
  • "Create New Profile" button
  • Security score preview for selected profile
  • Quick preset buttons

4.9 Unit Tests

Files:

  • frontend/src/hooks/__tests__/useSecurityHeaders.test.ts
  • frontend/src/components/__tests__/SecurityHeaderProfileForm.test.tsx
  • frontend/src/components/__tests__/CSPBuilder.test.tsx
  • frontend/src/components/__tests__/SecurityScoreDisplay.test.tsx
  • frontend/src/pages/__tests__/SecurityHeaders.test.tsx

Phase 5: Presets and Score Calculator

5.1 Built-in Presets

File: backend/internal/services/security_headers_service.go

package services

// SecurityHeadersService manages security header profiles
type SecurityHeadersService struct {
    db *gorm.DB
}

// GetPresets returns the built-in presets
func (s *SecurityHeadersService) GetPresets() []models.SecurityHeaderProfile {
    return []models.SecurityHeaderProfile{
        {
            Name:                  "Basic Security",
            PresetType:            "basic",
            IsPreset:              true,
            Description:           "Essential security headers for most websites. Safe defaults that won't break functionality.",
            HSTSEnabled:           true,
            HSTSMaxAge:            31536000, // 1 year
            HSTSIncludeSubdomains: false,
            HSTSPreload:           false,
            CSPEnabled:            false, // CSP can break sites
            XFrameOptions:         "SAMEORIGIN",
            XContentTypeOptions:   true,
            ReferrerPolicy:        "strict-origin-when-cross-origin",
            XSSProtection:         true,
            SecurityScore:         65,
        },
        {
            Name:                      "Strict Security",
            PresetType:                "strict",
            IsPreset:                  true,
            Description:               "Strong security for applications handling sensitive data. May require CSP adjustments.",
            HSTSEnabled:               true,
            HSTSMaxAge:                31536000,
            HSTSIncludeSubdomains:     true,
            HSTSPreload:               false,
            CSPEnabled:                true,
            CSPDirectives:             `{"default-src":["'self'"],"script-src":["'self'"],"style-src":["'self'","'unsafe-inline'"],"img-src":["'self'","data:","https:"],"font-src":["'self'","data:"],"connect-src":["'self'"],"frame-src":["'none'"],"object-src":["'none'"]}`,
            XFrameOptions:             "DENY",
            XContentTypeOptions:       true,
            ReferrerPolicy:            "strict-origin-when-cross-origin",
            PermissionsPolicy:         `[{"feature":"camera","allowlist":[]},{"feature":"microphone","allowlist":[]},{"feature":"geolocation","allowlist":[]}]`,
            XSSProtection:             true,
            CrossOriginOpenerPolicy:   "same-origin",
            CrossOriginResourcePolicy: "same-origin",
            SecurityScore:             85,
        },
        {
            Name:                      "Paranoid Security",
            PresetType:                "paranoid",
            IsPreset:                  true,
            Description:               "Maximum security for high-risk applications. May break some functionality. Test thoroughly.",
            HSTSEnabled:               true,
            HSTSMaxAge:                63072000, // 2 years
            HSTSIncludeSubdomains:     true,
            HSTSPreload:               true,
            CSPEnabled:                true,
            CSPDirectives:             `{"default-src":["'none'"],"script-src":["'self'"],"style-src":["'self'"],"img-src":["'self'"],"font-src":["'self'"],"connect-src":["'self'"],"frame-src":["'none'"],"object-src":["'none'"],"base-uri":["'self'"],"form-action":["'self'"],"frame-ancestors":["'none'"]}`,
            XFrameOptions:             "DENY",
            XContentTypeOptions:       true,
            ReferrerPolicy:            "no-referrer",
            PermissionsPolicy:         `[{"feature":"camera","allowlist":[]},{"feature":"microphone","allowlist":[]},{"feature":"geolocation","allowlist":[]},{"feature":"payment","allowlist":[]},{"feature":"usb","allowlist":[]}]`,
            XSSProtection:             true,
            CrossOriginOpenerPolicy:   "same-origin",
            CrossOriginResourcePolicy: "same-origin",
            CrossOriginEmbedderPolicy: "require-corp",
            CacheControlNoStore:       true,
            SecurityScore:             100,
        },
    }
}

// EnsurePresetsExist creates default presets if they don't exist
func (s *SecurityHeadersService) EnsurePresetsExist() error

5.2 Security Score Calculator

File: backend/internal/services/security_score.go

package services

import "strings"

// ScoreBreakdown represents the detailed score calculation
type ScoreBreakdown struct {
    TotalScore  int            `json:"score"`
    MaxScore    int            `json:"max_score"`
    Breakdown   map[string]int `json:"breakdown"`
    Suggestions []string       `json:"suggestions"`
}

// CalculateSecurityScore calculates the security score for a profile
func CalculateSecurityScore(profile *models.SecurityHeaderProfile) ScoreBreakdown {
    breakdown := make(map[string]int)
    suggestions := []string{}
    maxScore := 100

    // HSTS (25 points max)
    hstsScore := 0
    if profile.HSTSEnabled {
        hstsScore += 10
        if profile.HSTSMaxAge >= 31536000 {
            hstsScore += 5
        } else {
            suggestions = append(suggestions, "Increase HSTS max-age to at least 1 year")
        }
        if profile.HSTSIncludeSubdomains {
            hstsScore += 5
        } else {
            suggestions = append(suggestions, "Enable HSTS for subdomains")
        }
        if profile.HSTSPreload {
            hstsScore += 5
        } else {
            suggestions = append(suggestions, "Consider HSTS preload for browser preload lists")
        }
    } else {
        suggestions = append(suggestions, "Enable HSTS to enforce HTTPS")
    }
    breakdown["hsts"] = hstsScore

    // CSP (25 points max)
    cspScore := 0
    if profile.CSPEnabled {
        cspScore += 15
        // Additional points for strict CSP
        if !strings.Contains(profile.CSPDirectives, "'unsafe-inline'") {
            cspScore += 5
        } else {
            suggestions = append(suggestions, "Avoid 'unsafe-inline' in CSP for better security")
        }
        if !strings.Contains(profile.CSPDirectives, "'unsafe-eval'") {
            cspScore += 5
        } else {
            suggestions = append(suggestions, "Avoid 'unsafe-eval' in CSP for better security")
        }
    } else {
        suggestions = append(suggestions, "Enable Content-Security-Policy")
    }
    breakdown["csp"] = cspScore

    // X-Frame-Options (10 points)
    xfoScore := 0
    if profile.XFrameOptions == "DENY" {
        xfoScore = 10
    } else if profile.XFrameOptions == "SAMEORIGIN" {
        xfoScore = 7
    } else {
        suggestions = append(suggestions, "Set X-Frame-Options to DENY or SAMEORIGIN")
    }
    breakdown["x_frame_options"] = xfoScore

    // X-Content-Type-Options (10 points)
    xctoScore := 0
    if profile.XContentTypeOptions {
        xctoScore = 10
    } else {
        suggestions = append(suggestions, "Enable X-Content-Type-Options: nosniff")
    }
    breakdown["x_content_type_options"] = xctoScore

    // Referrer-Policy (10 points)
    rpScore := 0
    strictPolicies := []string{"no-referrer", "strict-origin", "strict-origin-when-cross-origin"}
    for _, p := range strictPolicies {
        if profile.ReferrerPolicy == p {
            rpScore = 10
            break
        }
    }
    if profile.ReferrerPolicy == "origin-when-cross-origin" {
        rpScore = 7
    }
    if rpScore == 0 && profile.ReferrerPolicy != "" {
        rpScore = 3
    }
    if rpScore < 10 {
        suggestions = append(suggestions, "Use a stricter Referrer-Policy")
    }
    breakdown["referrer_policy"] = rpScore

    // Permissions-Policy (10 points)
    ppScore := 0
    if profile.PermissionsPolicy != "" {
        ppScore = 10
    } else {
        suggestions = append(suggestions, "Add Permissions-Policy to restrict browser features")
    }
    breakdown["permissions_policy"] = ppScore

    // Cross-Origin headers (10 points)
    coScore := 0
    if profile.CrossOriginOpenerPolicy != "" {
        coScore += 4
    }
    if profile.CrossOriginResourcePolicy != "" {
        coScore += 3
    }
    if profile.CrossOriginEmbedderPolicy != "" {
        coScore += 3
    }
    if coScore < 10 {
        suggestions = append(suggestions, "Add Cross-Origin isolation headers")
    }
    breakdown["cross_origin"] = coScore

    // Calculate total
    total := hstsScore + cspScore + xfoScore + xctoScore + rpScore + ppScore + coScore

    return ScoreBreakdown{
        TotalScore:  total,
        MaxScore:    maxScore,
        Breakdown:   breakdown,
        Suggestions: suggestions,
    }
}

5.3 Initialize Presets on Startup

File: backend/internal/api/routes/routes.go

Add to the Register function:

// Ensure security header presets exist
secHeadersSvc := services.NewSecurityHeadersService(db)
if err := secHeadersSvc.EnsurePresetsExist(); err != nil {
    logger.Log().WithError(err).Warn("Failed to initialize security header presets")
}

5.4 Unit Tests

File: backend/internal/services/security_score_test.go

Test cases:

  • TestCalculateScore_AllEnabled - Full score
  • TestCalculateScore_HSTSOnly - Partial score
  • TestCalculateScore_NoHeaders - Zero score
  • TestCalculateScore_UnsafeCSP - CSP with unsafe directives
  • TestCalculateScore_Suggestions - Verify suggestions generated

File: backend/internal/services/security_headers_service_test.go

Test cases:

  • TestGetPresets - Returns all presets
  • TestEnsurePresetsExist_Creates - Creates presets when missing
  • TestEnsurePresetsExist_NoOp - Doesn't duplicate presets

Phase 6: Route Registration and Final Integration

6.1 Frontend Routes

File: frontend/src/App.tsx

Add routes:

<Route path="/security/headers" element={<SecurityHeaders />} />
<Route path="/security/headers/:id" element={<SecurityHeadersEdit />} />
<Route path="/security/headers/new" element={<SecurityHeadersCreate />} />

6.2 Navigation Update

Add "Security Headers" to the Security section in the navigation.

6.3 Integration Test

File: backend/integration/security_headers_test.go

End-to-end test:

  1. Create a security header profile
  2. Assign to a proxy host
  3. Generate Caddy config
  4. Verify headers in generated config

Implementation Summary

Files to Create

Phase File Path Description
1 backend/internal/models/security_header_profile.go New model
1 backend/internal/models/security_header_profile_test.go Model tests
2 backend/internal/api/handlers/security_headers_handler.go API handlers
2 backend/internal/api/handlers/security_headers_handler_test.go Handler tests
3 backend/internal/caddy/config_security_headers_test.go Caddy integration tests
4 frontend/src/api/securityHeaders.ts API client
4 frontend/src/hooks/useSecurityHeaders.ts React Query hooks
4 frontend/src/pages/SecurityHeaders.tsx Main page
4 frontend/src/components/SecurityHeaderProfileForm.tsx Profile form
4 frontend/src/components/CSPBuilder.tsx CSP builder
4 frontend/src/components/SecurityScoreDisplay.tsx Score display
4 frontend/src/components/PermissionsPolicyBuilder.tsx Permissions builder
5 backend/internal/services/security_headers_service.go Service layer
5 backend/internal/services/security_score.go Score calculator
5 backend/internal/services/security_score_test.go Score tests
5 backend/internal/services/security_headers_service_test.go Service tests

Files to Modify

Phase File Path Changes
1 backend/internal/models/proxy_host.go Add security header fields
1 backend/internal/api/routes/routes.go Add AutoMigrate
2 backend/internal/api/routes/routes.go Register handlers
3 backend/internal/caddy/config.go Add header handler builder
3 backend/internal/caddy/manager.go Preload profiles
4 frontend/src/components/ProxyHostForm.tsx Add profile selector
4 frontend/src/App.tsx Add routes
6 Navigation component Add menu item

Configuration File Updates

.gitignore

No changes required - existing patterns cover new files.

.codecov.yml

No changes required - existing patterns cover new test files.

.dockerignore

No changes required - existing patterns cover new files.

Dockerfile

No changes required - no new build dependencies needed.


Acceptance Criteria Checklist

Criteria Phase Implementation
Security headers automatically added Phase 3 buildSecurityHeadersHandler()
CSP configurable without breaking sites Phase 4 CSPBuilder.tsx with validation
Presets available for easy setup Phase 5 Basic, Strict, Paranoid presets
Security score shown in UI Phase 4 SecurityScoreDisplay.tsx
HSTS with preload support Phase 1 Model fields for all HSTS options
Content-Security-Policy builder Phase 4 CSPBuilder.tsx
X-Frame-Options (DENY/SAMEORIGIN) Phase 1 Model field with validation
X-Content-Type-Options (nosniff) Phase 1 Model boolean field
Referrer-Policy configuration Phase 1 Model field with enum values
Permissions-Policy headers Phase 1/4 Model + PermissionsPolicyBuilder.tsx
Security header presets Phase 5 basic, strict, paranoid
Security score calculator Phase 5 CalculateSecurityScore()

Testing Requirements

Backend Unit Tests (Target: 85%+ coverage)

  • Model CRUD operations
  • Handler request/response validation
  • Score calculation accuracy
  • CSP string building
  • Permissions-Policy string building
  • Caddy config generation with headers

Frontend Unit Tests (Target: 85%+ coverage)

  • Component rendering
  • Form validation
  • API hook behavior
  • Score display logic
  • CSP builder interactions

Integration Tests

  • End-to-end profile creation and application
  • Caddy config generation verification
  • Preset application flow

Risk Assessment

Risk Mitigation
CSP breaks existing sites CSP disabled by default, builder has validation
HSTS preload locked forever Warning in UI, preload disabled by default
Performance impact Headers cached per-host, no per-request calculation
Migration issues New table, no schema changes to existing tables

Next Steps

  1. Phase 1: Create model and migration (1 day)
  2. Phase 2: Implement API handlers (1-2 days)
  3. Phase 3: Caddy integration (1 day)
  4. Phase 4: Frontend components (2-3 days)
  5. Phase 5: Presets and score calculator (1 day)
  6. Phase 6: Final integration and testing (1 day)

Total Estimated Time: 7-9 days


Plan created by Planning Agent for Issue #20