security: implement comprehensive secrets protection
Critical fixes: - Add SanitizeConnectionString() in storage/db_connect.go to remove passwords - Add SecureEnvVar() in api/factory.go to clear env vars after reading (JWT_SECRET) - Clear DB password from config after connection Logging improvements: - Enhance logging/sanitize.go with patterns for: - PostgreSQL connection strings - Generic connection string passwords - HTTP Authorization headers - Private keys CLI security: - Add --security-audit flag to api-server for security checks: - Config file permissions - Exposed environment variables - Running as root - API key file permissions - Add warning when --api-key flag used (process list exposure) Files changed: - internal/storage/db_connect.go - internal/api/factory.go - internal/logging/sanitize.go - internal/auth/flags.go - cmd/api-server/main.go
This commit is contained in:
parent
6446379a40
commit
412d7b82e9
6 changed files with 143 additions and 1 deletions
BIN
api-server
Executable file
BIN
api-server
Executable file
Binary file not shown.
|
|
@ -21,6 +21,7 @@ func main() {
|
|||
apiKey := flag.String("api-key", "", "API key for authentication")
|
||||
showVersion := flag.Bool("version", false, "Show version and build info")
|
||||
verifyBuild := flag.Bool("verify", false, "Verify build integrity")
|
||||
securityAudit := flag.Bool("security-audit", false, "Run security audit and exit")
|
||||
flag.Parse()
|
||||
|
||||
// Handle version display
|
||||
|
|
@ -38,6 +39,12 @@ func main() {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Handle security audit
|
||||
if *securityAudit {
|
||||
runSecurityAudit(*configFile)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Create and start server
|
||||
server, err := api.NewServer(*configFile)
|
||||
if err != nil {
|
||||
|
|
@ -54,3 +61,75 @@ func main() {
|
|||
// Reserved for future authentication enhancements
|
||||
_ = apiKey
|
||||
}
|
||||
|
||||
// runSecurityAudit performs security checks and reports issues
|
||||
func runSecurityAudit(configFile string) {
|
||||
fmt.Println("=== Security Audit ===")
|
||||
issues := []string{}
|
||||
warnings := []string{}
|
||||
|
||||
// Check 1: Config file permissions
|
||||
if info, err := os.Stat(configFile); err == nil {
|
||||
mode := info.Mode().Perm()
|
||||
if mode&0077 != 0 {
|
||||
issues = append(issues, fmt.Sprintf("Config file %s is world/group readable (permissions: %04o)", configFile, mode))
|
||||
} else {
|
||||
fmt.Printf("✓ Config file permissions: %04o (secure)\n", mode)
|
||||
}
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("Could not check config file: %v", err))
|
||||
}
|
||||
|
||||
// Check 2: Environment variable exposure
|
||||
sensitiveVars := []string{"JWT_SECRET", "FETCH_ML_API_KEY", "DATABASE_PASSWORD", "REDIS_PASSWORD"}
|
||||
exposedVars := []string{}
|
||||
for _, v := range sensitiveVars {
|
||||
if os.Getenv(v) != "" {
|
||||
exposedVars = append(exposedVars, v)
|
||||
}
|
||||
}
|
||||
if len(exposedVars) > 0 {
|
||||
warnings = append(warnings, fmt.Sprintf("Sensitive environment variables exposed: %v (will be cleared on startup)", exposedVars))
|
||||
} else {
|
||||
fmt.Println("✓ No sensitive environment variables exposed")
|
||||
}
|
||||
|
||||
// Check 3: Running as root
|
||||
if os.Getuid() == 0 {
|
||||
issues = append(issues, "Running as root (UID 0) - should run as non-root user")
|
||||
} else {
|
||||
fmt.Printf("✓ Running as non-root user (UID: %d)\n", os.Getuid())
|
||||
}
|
||||
|
||||
// Check 4: API key file permissions
|
||||
apiKeyFile := os.Getenv("FETCH_ML_API_KEY_FILE")
|
||||
if apiKeyFile != "" {
|
||||
if info, err := os.Stat(apiKeyFile); err == nil {
|
||||
mode := info.Mode().Perm()
|
||||
if mode&0077 != 0 {
|
||||
issues = append(issues, fmt.Sprintf("API key file %s is world/group readable (permissions: %04o)", apiKeyFile, mode))
|
||||
} else {
|
||||
fmt.Printf("✓ API key file permissions: %04o (secure)\n", mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Report results
|
||||
fmt.Println()
|
||||
if len(issues) == 0 && len(warnings) == 0 {
|
||||
fmt.Println("✓ All security checks passed")
|
||||
} else {
|
||||
if len(issues) > 0 {
|
||||
fmt.Printf("✗ Found %d security issue(s):\n", len(issues))
|
||||
for _, issue := range issues {
|
||||
fmt.Printf(" - %s\n", issue)
|
||||
}
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
fmt.Printf("⚠ Found %d warning(s):\n", len(warnings))
|
||||
for _, warning := range warnings {
|
||||
fmt.Printf(" - %s\n", warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,11 @@ func (s *Server) initDatabase() error {
|
|||
s.db = db
|
||||
s.logger.Info("database initialized", "type", s.config.Database.Type)
|
||||
|
||||
// Clear database password from memory after connection
|
||||
if s.config.Database.Password != "" {
|
||||
s.config.Database.Password = "[CLEARED]"
|
||||
}
|
||||
|
||||
// Add cleanup function
|
||||
s.cleanupFuncs = append(s.cleanupFuncs, func() {
|
||||
s.logger.Info("closing database connection...")
|
||||
|
|
@ -183,7 +188,11 @@ func (s *Server) initDatabaseSchema() error {
|
|||
func (s *Server) initSecurity() {
|
||||
authConfig := s.config.BuildAuthConfig()
|
||||
rlOpts := s.buildRateLimitOptions()
|
||||
s.sec = middleware.NewSecurityMiddleware(authConfig, os.Getenv("JWT_SECRET"), rlOpts)
|
||||
|
||||
// Read JWT secret securely and clear from environment
|
||||
jwtSecret := SecureEnvVar("JWT_SECRET")
|
||||
|
||||
s.sec = middleware.NewSecurityMiddleware(authConfig, jwtSecret, rlOpts)
|
||||
}
|
||||
|
||||
// buildRateLimitOptions builds rate limit options from configuration
|
||||
|
|
@ -254,3 +263,13 @@ func getSecurityConfig(cfg *ServerConfig) *config.SecurityConfig {
|
|||
IPWhitelist: cfg.Security.IPWhitelist,
|
||||
}
|
||||
}
|
||||
|
||||
// SecureEnvVar reads an environment variable and clears it from the environment
|
||||
// to prevent exposure via /proc/*/environ. Returns empty string if not set.
|
||||
func SecureEnvVar(name string) string {
|
||||
value := os.Getenv(name)
|
||||
if value != "" {
|
||||
os.Unsetenv(name)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ func ParseAuthFlags() *Flags {
|
|||
func GetAPIKeyFromSources(flags *Flags) string {
|
||||
// 1. Command line flag (highest priority)
|
||||
if flags.APIKey != "" {
|
||||
// Warn about process list exposure
|
||||
log.Println("WARNING: --api-key exposes credential in process list. Use --api-key-file for better security.")
|
||||
return flags.APIKey
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,18 @@ var (
|
|||
|
||||
// Redis URLs with passwords
|
||||
redisPasswordPattern = regexp.MustCompile(`redis://:[^@]+@`)
|
||||
|
||||
// Database connection strings - PostgreSQL
|
||||
postgresPasswordPattern = regexp.MustCompile(`(postgresql?://[^:]+:)[^@]+(@)`)
|
||||
|
||||
// Generic connection strings with passwords
|
||||
connStringPasswordPattern = regexp.MustCompile(`(password[=:])[^\s&;]+`)
|
||||
|
||||
// HTTP Authorization headers
|
||||
authHeaderPattern = regexp.MustCompile(`(Authorization:\s*(?:Bearer|Basic)\s+)[^\s]+`)
|
||||
|
||||
// Private keys
|
||||
privateKeyPattern = regexp.MustCompile(`(-----BEGIN (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----)[\s\S]*?(-----END (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----)`)
|
||||
)
|
||||
|
||||
// SanitizeLogMessage removes sensitive data from log messages
|
||||
|
|
@ -43,6 +55,18 @@ func SanitizeLogMessage(message string) string {
|
|||
// Redact Redis passwords from URLs
|
||||
message = redisPasswordPattern.ReplaceAllString(message, "redis://:[REDACTED]@")
|
||||
|
||||
// Redact PostgreSQL connection strings
|
||||
message = postgresPasswordPattern.ReplaceAllString(message, "${1}[REDACTED]${2}")
|
||||
|
||||
// Redact generic connection string passwords
|
||||
message = connStringPasswordPattern.ReplaceAllString(message, "${1}[REDACTED]")
|
||||
|
||||
// Redact HTTP Authorization headers
|
||||
message = authHeaderPattern.ReplaceAllString(message, "${1}[REDACTED]")
|
||||
|
||||
// Redact private keys
|
||||
message = privateKeyPattern.ReplaceAllString(message, "[REDACTED-PRIVATE-KEY]")
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -12,6 +13,23 @@ import (
|
|||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||
)
|
||||
|
||||
// SanitizeConnectionString removes passwords from connection strings for safe logging
|
||||
func SanitizeConnectionString(connStr string) string {
|
||||
if connStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Pattern for postgresql://user:pass@host format
|
||||
postgresPattern := regexp.MustCompile(`(postgresql?://[^:]+:)[^@]+(@)`)
|
||||
sanitized := postgresPattern.ReplaceAllString(connStr, "${1}[REDACTED]${2}")
|
||||
|
||||
// Pattern for password=xxxxx in connection string
|
||||
passwordPattern := regexp.MustCompile(`(password=)[^\s]*`)
|
||||
sanitized = passwordPattern.ReplaceAllString(sanitized, "${1}[REDACTED]")
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// DBConfig holds database connection configuration.
|
||||
type DBConfig struct {
|
||||
Type string
|
||||
|
|
|
|||
Loading…
Reference in a new issue