mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:01:30 +00:00
28205e1131
Add SHA-256 API key authentication with constant-time comparison, configurable token bucket rate limiter, CORS origin allowlist middleware, and React auth context with login page. Auth info endpoint bootstraps GUI without credentials. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
293 lines
7.8 KiB
Go
293 lines
7.8 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// Config represents the complete application configuration.
|
|
// All configuration values are read from environment variables with CERTCTL_ prefix.
|
|
type Config struct {
|
|
Server ServerConfig
|
|
Database DatabaseConfig
|
|
Scheduler SchedulerConfig
|
|
Log LogConfig
|
|
Auth AuthConfig
|
|
RateLimit RateLimitConfig
|
|
CORS CORSConfig
|
|
}
|
|
|
|
// ServerConfig contains HTTP server configuration.
|
|
type ServerConfig struct {
|
|
Host string
|
|
Port int
|
|
}
|
|
|
|
// DatabaseConfig contains database connection configuration.
|
|
type DatabaseConfig struct {
|
|
URL string
|
|
MaxConnections int
|
|
MigrationsPath string
|
|
}
|
|
|
|
// SchedulerConfig contains scheduler timing configuration.
|
|
type SchedulerConfig struct {
|
|
RenewalCheckInterval time.Duration
|
|
JobProcessorInterval time.Duration
|
|
AgentHealthCheckInterval time.Duration
|
|
NotificationProcessInterval time.Duration
|
|
}
|
|
|
|
// LogConfig contains logging configuration.
|
|
type LogConfig struct {
|
|
Level string // "debug", "info", "warn", "error"
|
|
Format string // "json" or "text"
|
|
}
|
|
|
|
// AuthConfig contains authentication configuration.
|
|
type AuthConfig struct {
|
|
Type string // "api-key", "jwt", "none"
|
|
Secret string // Secret key for signing (if applicable)
|
|
}
|
|
|
|
// RateLimitConfig contains rate limiting configuration.
|
|
type RateLimitConfig struct {
|
|
Enabled bool
|
|
RPS float64 // Requests per second
|
|
BurstSize int // Maximum burst size
|
|
}
|
|
|
|
// CORSConfig contains CORS configuration.
|
|
type CORSConfig struct {
|
|
AllowedOrigins []string // Allowed origins; empty = same-origin only; ["*"] = all
|
|
}
|
|
|
|
// Load reads configuration from environment variables and returns a Config.
|
|
// Environment variables must have the CERTCTL_ prefix.
|
|
// Example: CERTCTL_SERVER_HOST, CERTCTL_DATABASE_URL, etc.
|
|
func Load() (*Config, error) {
|
|
cfg := &Config{
|
|
Server: ServerConfig{
|
|
Host: getEnv("CERTCTL_SERVER_HOST", "127.0.0.1"),
|
|
Port: getEnvInt("CERTCTL_SERVER_PORT", 8080),
|
|
},
|
|
Database: DatabaseConfig{
|
|
URL: getEnv("CERTCTL_DATABASE_URL", "postgres://localhost/certctl"),
|
|
MaxConnections: getEnvInt("CERTCTL_DATABASE_MAX_CONNS", 25),
|
|
MigrationsPath: getEnv("CERTCTL_DATABASE_MIGRATIONS_PATH", "./migrations"),
|
|
},
|
|
Scheduler: SchedulerConfig{
|
|
RenewalCheckInterval: getEnvDuration("CERTCTL_SCHEDULER_RENEWAL_CHECK_INTERVAL", 1*time.Hour),
|
|
JobProcessorInterval: getEnvDuration("CERTCTL_SCHEDULER_JOB_PROCESSOR_INTERVAL", 30*time.Second),
|
|
AgentHealthCheckInterval: getEnvDuration("CERTCTL_SCHEDULER_AGENT_HEALTH_CHECK_INTERVAL", 2*time.Minute),
|
|
NotificationProcessInterval: getEnvDuration("CERTCTL_SCHEDULER_NOTIFICATION_PROCESS_INTERVAL", 1*time.Minute),
|
|
},
|
|
Log: LogConfig{
|
|
Level: getEnv("CERTCTL_LOG_LEVEL", "info"),
|
|
Format: getEnv("CERTCTL_LOG_FORMAT", "json"),
|
|
},
|
|
Auth: AuthConfig{
|
|
Type: getEnv("CERTCTL_AUTH_TYPE", "api-key"),
|
|
Secret: getEnv("CERTCTL_AUTH_SECRET", ""),
|
|
},
|
|
RateLimit: RateLimitConfig{
|
|
Enabled: getEnvBool("CERTCTL_RATE_LIMIT_ENABLED", true),
|
|
RPS: getEnvFloat("CERTCTL_RATE_LIMIT_RPS", 50),
|
|
BurstSize: getEnvInt("CERTCTL_RATE_LIMIT_BURST", 100),
|
|
},
|
|
CORS: CORSConfig{
|
|
AllowedOrigins: getEnvList("CERTCTL_CORS_ORIGINS", nil),
|
|
},
|
|
}
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// Validate checks that the configuration is valid.
|
|
func (c *Config) Validate() error {
|
|
// Validate server configuration
|
|
if c.Server.Port < 1 || c.Server.Port > 65535 {
|
|
return fmt.Errorf("invalid server port: %d", c.Server.Port)
|
|
}
|
|
|
|
// Validate database configuration
|
|
if c.Database.URL == "" {
|
|
return fmt.Errorf("database URL is required")
|
|
}
|
|
|
|
if c.Database.MaxConnections < 1 {
|
|
return fmt.Errorf("database max_connections must be at least 1")
|
|
}
|
|
|
|
// Validate log level
|
|
validLogLevels := map[string]bool{
|
|
"debug": true,
|
|
"info": true,
|
|
"warn": true,
|
|
"error": true,
|
|
}
|
|
if !validLogLevels[c.Log.Level] {
|
|
return fmt.Errorf("invalid log level: %s", c.Log.Level)
|
|
}
|
|
|
|
// Validate log format
|
|
validFormats := map[string]bool{
|
|
"json": true,
|
|
"text": true,
|
|
}
|
|
if !validFormats[c.Log.Format] {
|
|
return fmt.Errorf("invalid log format: %s", c.Log.Format)
|
|
}
|
|
|
|
// Validate auth type
|
|
validAuthTypes := map[string]bool{
|
|
"api-key": true,
|
|
"jwt": true,
|
|
"none": true,
|
|
}
|
|
if !validAuthTypes[c.Auth.Type] {
|
|
return fmt.Errorf("invalid auth type: %s", c.Auth.Type)
|
|
}
|
|
|
|
// If using JWT or API-key, secret is required
|
|
if (c.Auth.Type == "jwt" || c.Auth.Type == "api-key") && c.Auth.Secret == "" {
|
|
return fmt.Errorf("auth secret is required for auth type %s", c.Auth.Type)
|
|
}
|
|
|
|
// Validate scheduler intervals
|
|
if c.Scheduler.RenewalCheckInterval < 1*time.Minute {
|
|
return fmt.Errorf("renewal check interval must be at least 1 minute")
|
|
}
|
|
|
|
if c.Scheduler.JobProcessorInterval < 1*time.Second {
|
|
return fmt.Errorf("job processor interval must be at least 1 second")
|
|
}
|
|
|
|
if c.Scheduler.AgentHealthCheckInterval < 1*time.Second {
|
|
return fmt.Errorf("agent health check interval must be at least 1 second")
|
|
}
|
|
|
|
if c.Scheduler.NotificationProcessInterval < 1*time.Second {
|
|
return fmt.Errorf("notification process interval must be at least 1 second")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getEnv reads a string environment variable with the given key and default value.
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvInt reads an integer environment variable with the given key and default value.
|
|
func getEnvInt(key string, defaultValue int) int {
|
|
if value := os.Getenv(key); value != "" {
|
|
intVal, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return intVal
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvDuration reads a time.Duration environment variable.
|
|
// The value should be a valid Go duration string (e.g., "1h", "30s", "5m").
|
|
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
|
|
if value := os.Getenv(key); value != "" {
|
|
duration, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return duration
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvBool reads a boolean environment variable.
|
|
func getEnvBool(key string, defaultValue bool) bool {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value == "true" || value == "1" || value == "yes"
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvFloat reads a float64 environment variable.
|
|
func getEnvFloat(key string, defaultValue float64) float64 {
|
|
if value := os.Getenv(key); value != "" {
|
|
f, err := strconv.ParseFloat(value, 64)
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return f
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// getEnvList reads a comma-separated list environment variable.
|
|
func getEnvList(key string, defaultValue []string) []string {
|
|
if value := os.Getenv(key); value != "" {
|
|
var result []string
|
|
for _, s := range splitComma(value) {
|
|
s = trimSpace(s)
|
|
if s != "" {
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// splitComma splits a string by commas (no strings import needed).
|
|
func splitComma(s string) []string {
|
|
var parts []string
|
|
start := 0
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == ',' {
|
|
parts = append(parts, s[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
parts = append(parts, s[start:])
|
|
return parts
|
|
}
|
|
|
|
// trimSpace trims leading/trailing whitespace.
|
|
func trimSpace(s string) string {
|
|
start, end := 0, len(s)
|
|
for start < end && (s[start] == ' ' || s[start] == '\t') {
|
|
start++
|
|
}
|
|
for end > start && (s[end-1] == ' ' || s[end-1] == '\t') {
|
|
end--
|
|
}
|
|
return s[start:end]
|
|
}
|
|
|
|
// GetLogLevel returns the appropriate slog.Level from the configured log level.
|
|
func (c *Config) GetLogLevel() slog.Level {
|
|
switch c.Log.Level {
|
|
case "debug":
|
|
return slog.LevelDebug
|
|
case "info":
|
|
return slog.LevelInfo
|
|
case "warn":
|
|
return slog.LevelWarn
|
|
case "error":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|