From b2256183e15a52c6387b7690f563b8ab5619fc7a Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 1 Mar 2026 13:06:43 +0300 Subject: [PATCH] config: add security and performance sections to sample config; validate Signed-off-by: NotAShelf Change-Id: Ieda42bcbd09014c45fb14bee579f829c6a6a6964 --- config.example.yaml | 41 +++++++++++++++-- internal/config/config.go | 96 ++++++++++++++++++++++++++++++++++----- 2 files changed, 121 insertions(+), 16 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index f3a63a4..991d72b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -8,13 +8,18 @@ site: # Options: "daily", "hourly" salt_rotation: "daily" + # Sampling rate (0.0 to 1.0, default: 1.0 = 100%) + # Use 0.1 to track 10% of traffic for high-volume sites + sampling: 1.0 + # Which dimensions to collect collect: pageviews: true country: true device: true - # Referrer collection mode: "off", "domain" + # Referrer collection mode: "off", "domain", "url" # "domain" - collect only the domain part (e.g., "google.com") + # "url" - collect full URL (not recommended - high cardinality) # "off" - don't collect referrer data referrer: "domain" @@ -32,7 +37,7 @@ site: strip_fragment: true # Collapse numeric segments to :id (e.g., /user/123 -> /user/:id) collapse_numeric_segments: true - # Maximum number of path segments to keep + # Maximum number of path segments to keep (0 = unlimited) max_segments: 5 # Normalize trailing slashes (e.g., /page/ -> /page) normalize_trailing_slash: true @@ -43,8 +48,36 @@ limits: max_paths: 10000 # Maximum number of unique referrer sources to track max_sources: 500 - # Maximum events per minute (for rate limiting, not yet implemented) - max_events_per_minute: 1000 + # Maximum number of unique custom event names to track + max_custom_events: 100 + # Maximum events per minute (rate limiting, 0 = unlimited) + max_events_per_minute: 10000 + + # Device classification breakpoints (screen width in pixels) + device_breakpoints: + mobile: 768 # < 768px = mobile + tablet: 1024 # < 1024px = tablet, >= 1024px = desktop + +# Security settings +security: + # Trusted proxy IPs/CIDRs - only trust X-Forwarded-For from these IPs + # Leave empty to never trust proxy headers + trusted_proxies: + - "127.0.0.1" + - "10.0.0.0/8" + # - "your-load-balancer-ip" + + # CORS configuration for cross-origin tracking + cors: + enabled: false + allowed_origins: + - "*" # Or specific domains: ["https://example.com", "https://www.example.com"] + + # Basic authentication for /metrics endpoint + metrics_auth: + enabled: false + username: "admin" + password: "changeme" # Server configuration server: diff --git a/internal/config/config.go b/internal/config/config.go index 499d545..6363511 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,9 +9,10 @@ import ( // Configuration structure type Config struct { - Site SiteConfig `yaml:"site"` - Limits LimitsConfig `yaml:"limits"` - Server ServerConfig `yaml:"server"` + Site SiteConfig `yaml:"site"` + Limits LimitsConfig `yaml:"limits"` + Server ServerConfig `yaml:"server"` + Security SecurityConfig `yaml:"security"` } // Site-specific settings @@ -21,6 +22,7 @@ type SiteConfig struct { Collect CollectConfig `yaml:"collect"` CustomEvents []string `yaml:"custom_events"` Path PathConfig `yaml:"path"` + Sampling float64 `yaml:"sampling"` // 0.0-1.0, 1.0 = track all traffic } // Which dimensions to collect @@ -42,9 +44,37 @@ type PathConfig struct { // Cardinality limits type LimitsConfig struct { - MaxPaths int `yaml:"max_paths"` - MaxEventsPerMinute int `yaml:"max_events_per_minute"` - MaxSources int `yaml:"max_sources"` + MaxPaths int `yaml:"max_paths"` + MaxEventsPerMinute int `yaml:"max_events_per_minute"` + MaxSources int `yaml:"max_sources"` + MaxCustomEvents int `yaml:"max_custom_events"` + DeviceBreakpoints DeviceBreaks `yaml:"device_breakpoints"` +} + +// Device classification breakpoints +type DeviceBreaks struct { + Mobile int `yaml:"mobile"` // < mobile = mobile + Tablet int `yaml:"tablet"` // < tablet = tablet, else desktop +} + +// Security settings +type SecurityConfig struct { + TrustedProxies []string `yaml:"trusted_proxies"` // IPs/CIDRs to trust X-Forwarded-For from + CORS CORSConfig `yaml:"cors"` + MetricsAuth AuthConfig `yaml:"metrics_auth"` +} + +// CORS configuration +type CORSConfig struct { + Enabled bool `yaml:"enabled"` + AllowedOrigins []string `yaml:"allowed_origins"` // ["*"] or specific domains +} + +// Authentication for metrics endpoint +type AuthConfig struct { + Enabled bool `yaml:"enabled"` + Username string `yaml:"username"` + Password string `yaml:"password"` } // Server endpoints @@ -74,29 +104,61 @@ func Load(path string) (*Config, error) { } // Check required fields and sets defaults -// FIXME: in the future we need to validate in the config parser func (c *Config) Validate() error { - // Validate site domain is required + // Site validation if c.Site.Domain == "" { return fmt.Errorf("site.domain is required") } - // Validate salt_rotation if provided + // Validate salt_rotation if c.Site.SaltRotation != "" && c.Site.SaltRotation != "daily" && c.Site.SaltRotation != "hourly" { return fmt.Errorf("site.salt_rotation must be 'daily' or 'hourly'") } - // Validate max_paths is positive + // Validate referrer setting + if c.Site.Collect.Referrer != "" && c.Site.Collect.Referrer != "off" && + c.Site.Collect.Referrer != "domain" && c.Site.Collect.Referrer != "url" { + return fmt.Errorf("site.collect.referrer must be 'off', 'domain', or 'url'") + } + + // Validate sampling rate + if c.Site.Sampling < 0.0 || c.Site.Sampling > 1.0 { + return fmt.Errorf("site.sampling must be between 0.0 and 1.0") + } + if c.Site.Sampling == 0.0 { + c.Site.Sampling = 1.0 // default to 100% + } + + // Limits validation if c.Limits.MaxPaths <= 0 { return fmt.Errorf("limits.max_paths must be greater than 0") } - // Validate max_sources is positive if c.Limits.MaxSources <= 0 { return fmt.Errorf("limits.max_sources must be greater than 0") } - // Set server defaults if not provided + if c.Limits.MaxEventsPerMinute < 0 { + return fmt.Errorf("limits.max_events_per_minute cannot be negative") + } + + if c.Limits.MaxCustomEvents <= 0 { + c.Limits.MaxCustomEvents = 100 // Default + } + + if c.Site.Path.MaxSegments < 0 { + return fmt.Errorf("site.path.max_segments cannot be negative") + } + + // Set device breakpoint defaults + if c.Limits.DeviceBreakpoints.Mobile == 0 { + c.Limits.DeviceBreakpoints.Mobile = 768 + } + if c.Limits.DeviceBreakpoints.Tablet == 0 { + c.Limits.DeviceBreakpoints.Tablet = 1024 + } + + // Server defaults if c.Server.ListenAddr == "" { c.Server.ListenAddr = ":8080" } @@ -107,5 +169,15 @@ func (c *Config) Validate() error { c.Server.IngestionPath = "/api/event" } + if c.Security.MetricsAuth.Enabled { + if c.Security.MetricsAuth.Username == "" || c.Security.MetricsAuth.Password == "" { + return fmt.Errorf("security.metrics_auth: username and password required when enabled") + } + } + + if c.Security.CORS.Enabled && len(c.Security.CORS.AllowedOrigins) == 0 { + return fmt.Errorf("security.cors: allowed_origins required when enabled") + } + return nil }