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
135 lines
3.7 KiB
Go
135 lines
3.7 KiB
Go
// Package main implements the fetch_ml API server
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/api"
|
|
)
|
|
|
|
// Build variables injected at build time
|
|
var (
|
|
BuildHash = "unknown"
|
|
BuildTime = "unknown"
|
|
)
|
|
|
|
func main() {
|
|
configFile := flag.String("config", "configs/api/dev.yaml", "Configuration file path")
|
|
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
|
|
if *showVersion {
|
|
fmt.Printf("fetch_ml API Server\n")
|
|
fmt.Printf(" Build Hash: %s\n", BuildHash)
|
|
fmt.Printf(" Build Time: %s\n", BuildTime)
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Handle build verification (placeholder - always true for now)
|
|
if *verifyBuild {
|
|
fmt.Printf("Build verification: OK\n")
|
|
fmt.Printf(" Build Hash: %s\n", BuildHash)
|
|
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 {
|
|
log.Fatalf("Failed to create server: %v", err)
|
|
}
|
|
|
|
if err := server.Start(); err != nil {
|
|
log.Fatalf("Failed to start server: %v", err)
|
|
}
|
|
|
|
// Wait for shutdown
|
|
server.WaitForShutdown()
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|