From 9e03c64d2a0e7b0c966a04de3ee556e76ff361e5 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 12 Feb 2025 21:56:13 -0500 Subject: [PATCH] Complete bash migration --- install/captcha.html | 338 ------------------ install/{crowdsec/install.go => crowdsec.go} | 271 ++++++-------- install/{fs => }/crowdsec/acquis.yaml | 0 install/{fs => }/crowdsec/config.yaml | 0 install/crowdsec/dynamic_config.yml | 108 ++++++ .../crowdsec/local_api_credentials.yaml | 0 install/{fs => }/crowdsec/profiles.yaml | 0 install/crowdsec/traefik_config.yml | 87 +++++ install/main.go | 1 + 9 files changed, 301 insertions(+), 504 deletions(-) delete mode 100644 install/captcha.html rename install/{crowdsec/install.go => crowdsec.go} (55%) rename install/{fs => }/crowdsec/acquis.yaml (100%) rename install/{fs => }/crowdsec/config.yaml (100%) create mode 100644 install/crowdsec/dynamic_config.yml rename install/{fs => }/crowdsec/local_api_credentials.yaml (100%) rename install/{fs => }/crowdsec/profiles.yaml (100%) create mode 100644 install/crowdsec/traefik_config.yml diff --git a/install/captcha.html b/install/captcha.html deleted file mode 100644 index a40d37a3..00000000 --- a/install/captcha.html +++ /dev/null @@ -1,338 +0,0 @@ - - - - - CrowdSec Captcha - - - - - - - -
-
-
- -

CrowdSec Captcha

-
-
-
-
-
-
-

This security check has been powered by

- - - - - - - - - - - - - - - - - - - - - CrowdSec - -
-
-
- - - \ No newline at end of file diff --git a/install/crowdsec/install.go b/install/crowdsec.go similarity index 55% rename from install/crowdsec/install.go rename to install/crowdsec.go index 84f3089b..187fea88 100644 --- a/install/crowdsec/install.go +++ b/install/crowdsec.go @@ -1,31 +1,21 @@ -package crowdsec +package main import ( "bytes" "embed" - "encoding/json" "fmt" + "html/template" "io" + "io/fs" "os" "os/exec" + "path/filepath" "strings" "time" ) -//go:embed fs/* -var configFiles embed.FS - -// Config holds all configuration values -type Config struct { - DomainName string - EnrollmentKey string - TurnstileSiteKey string - TurnstileSecretKey string - GID string - CrowdsecIP string - TraefikBouncerKey string - PangolinIP string -} +//go:embed crowdsec/* +var configCrowdsecFiles embed.FS // DockerContainer represents a Docker container type DockerContainer struct { @@ -36,14 +26,7 @@ type DockerContainer struct { } `json:"NetworkSettings"` } -func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} - -func run() error { +func installCrowdsec() error { // Create configuration config := &Config{} @@ -52,30 +35,21 @@ func run() error { return fmt.Errorf("backup failed: %v", err) } - if err := createPangolinNetwork(); err != nil { - return fmt.Errorf("network creation failed: %v", err) - } - if err := modifyDockerCompose(); err != nil { return fmt.Errorf("docker-compose modification failed: %v", err) } - if err := createConfigFiles(*config); err != nil { + if err := createCrowdsecFiles(*config); err != nil { return fmt.Errorf("config file creation failed: %v", err) } - if err := retrieveIPs(config); err != nil { - return fmt.Errorf("IP retrieval failed: %v", err) - } + moveFile("config/crowdsec/traefik_config.yaml", "config/traefik/traefik_config.yaml") + moveFile("config/crowdsec/dynamic.yaml", "config/traefik/dynamic.yaml") if err := retrieveBouncerKey(config); err != nil { return fmt.Errorf("bouncer key retrieval failed: %v", err) } - if err := replacePlaceholders(config); err != nil { - return fmt.Errorf("placeholder replacement failed: %v", err) - } - if err := deployStack(); err != nil { return fmt.Errorf("deployment failed: %v", err) } @@ -84,7 +58,6 @@ func run() error { return fmt.Errorf("verification failed: %v", err) } - printInstructions() return nil } @@ -107,23 +80,6 @@ func backupConfig() error { return nil } -func createPangolinNetwork() error { - // Check if network exists - cmd := exec.Command("docker", "network", "inspect", "pangolin") - if err := cmd.Run(); err == nil { - fmt.Println("pangolin network already exists") - return nil - } - - // Create network - cmd = exec.Command("docker", "network", "create", "pangolin") - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to create pangolin network: %v", err) - } - - return nil -} - func modifyDockerCompose() error { // Read existing docker-compose.yml content, err := os.ReadFile("docker-compose.yml") @@ -150,34 +106,6 @@ func modifyDockerCompose() error { return nil } -func retrieveIPs(config *Config) error { - // Start required containers - cmd := exec.Command("docker", "compose", "up", "-d", "pangolin", "crowdsec") - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to start containers: %v", err) - } - defer exec.Command("docker", "compose", "down").Run() - - // Wait for containers to start - time.Sleep(10 * time.Second) - - // Get Pangolin IP - pangolinIP, err := getContainerIP("pangolin") - if err != nil { - return fmt.Errorf("failed to get pangolin IP: %v", err) - } - config.PangolinIP = pangolinIP - - // Get CrowdSec IP - crowdsecIP, err := getContainerIP("crowdsec") - if err != nil { - return fmt.Errorf("failed to get crowdsec IP: %v", err) - } - config.CrowdsecIP = crowdsecIP - - return nil -} - func retrieveBouncerKey(config *Config) error { // Start crowdsec container cmd := exec.Command("docker", "compose", "up", "-d", "crowdsec") @@ -207,32 +135,6 @@ func retrieveBouncerKey(config *Config) error { return nil } -func replacePlaceholders(config *Config) error { - // Get user input - fmt.Print("Enter your Domain Name (e.g., pangolin.example.com): ") - fmt.Scanln(&config.DomainName) - - fmt.Print("Enter your CrowdSec Enrollment Key: ") - fmt.Scanln(&config.EnrollmentKey) - - fmt.Print("Enter your Cloudflare Turnstile Site Key: ") - fmt.Scanln(&config.TurnstileSiteKey) - - fmt.Print("Enter your Cloudflare Turnstile Secret Key: ") - fmt.Scanln(&config.TurnstileSecretKey) - - fmt.Print("Enter your GID (or leave empty for default 1000): ") - gid := "" - fmt.Scanln(&gid) - if gid == "" { - config.GID = "1000" - } else { - config.GID = gid - } - - return nil -} - func deployStack() error { cmd := exec.Command("docker", "compose", "up", "-d") if err := cmd.Run(); err != nil { @@ -257,43 +159,6 @@ func verifyDeployment() error { return nil } -func printInstructions() { - fmt.Println(` ---- Testing Instructions --- -1. Test Captcha Implementation: - docker exec crowdsec cscli decisions add --ip YOUR_IP --type captcha -d 1h - (Replace YOUR_IP with your actual IP address) - -2. Verify decisions: - docker exec -it crowdsec cscli decisions list - -3. Test security by accessing DOMAIN_NAME/.env (should return 403) - (Replace DOMAIN_NAME with the domain you entered) - ---- Troubleshooting --- -1. If encountering 403 errors: - - Check Traefik logs: docker compose logs traefik -f - - Verify CrowdSec logs: docker compose logs crowdsec - -2. For plugin errors: - - Verify http notifications are commented out in profiles.yaml - - Restart services: docker compose restart traefik crowdsec - -3. For Captcha issues: - - Ensure Turnstile is configured in non-interactive mode - - Verify captcha.html configuration - - Check container network connectivity - -Useful Commands: -- View Traefik logs: docker compose logs traefik -f -- View CrowdSec logs: docker compose logs crowdsec -- List decisions: docker exec -it crowdsec cscli decisions list -- Check metrics: curl http://localhost:6060/metrics | grep appsec -`) -} - -// Helper functions - func copyFile(src, dst string) error { source, err := os.Open(src) if err != nil { @@ -311,26 +176,12 @@ func copyFile(src, dst string) error { return err } -func getContainerIP(containerName string) (string, error) { - output, err := exec.Command("docker", "inspect", containerName).Output() - if err != nil { - return "", err +func moveFile(src, dst string) error { + if err := copyFile(src, dst); err != nil { + return err } - var containers []DockerContainer - if err := json.Unmarshal(output, &containers); err != nil { - return "", err - } - - if len(containers) == 0 { - return "", fmt.Errorf("no container found") - } - - for _, network := range containers[0].NetworkSettings.Networks { - return network.IPAddress, nil - } - - return "", fmt.Errorf("no IP address found") + return os.Remove(src) } func addCrowdsecService(content string) string { @@ -346,11 +197,8 @@ func addCrowdsecService(content string) string { COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules ENROLL_INSTANCE_NAME: "pangolin-crowdsec" PARSERS: crowdsecurity/whitelists - ENROLL_KEY: ${ENROLLMENT_KEY} ACQUIRE_FILES: "/var/log/traefik/*.log" ENROLL_TAGS: docker - networks: - - pangolin healthcheck: test: ["CMD", "cscli", "capi", "status"] depends_on: @@ -374,3 +222,94 @@ func addCrowdsecService(content string) string { restart: unless-stopped command: -t` } + +func createCrowdsecFiles(config Config) error { + // Walk through all embedded files + err := fs.WalkDir(configCrowdsecFiles, "crowdsec", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip the root fs directory itself + if path == "fs" { + return nil + } + + // Get the relative path by removing the "fs/" prefix + relPath := strings.TrimPrefix(path, "fs/") + + // skip .DS_Store + if strings.Contains(relPath, ".DS_Store") { + return nil + } + + // Create the full output path under "config/" + outPath := filepath.Join("config", relPath) + + if d.IsDir() { + // Create directory + if err := os.MkdirAll(outPath, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %v", outPath, err) + } + return nil + } + + // Read the template file + content, err := configFiles.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read %s: %v", path, err) + } + + // Parse template + tmpl, err := template.New(d.Name()).Parse(string(content)) + if err != nil { + return fmt.Errorf("failed to parse template %s: %v", path, err) + } + + // Ensure parent directory exists + if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { + return fmt.Errorf("failed to create parent directory for %s: %v", outPath, err) + } + + // Create output file + outFile, err := os.Create(outPath) + if err != nil { + return fmt.Errorf("failed to create %s: %v", outPath, err) + } + defer outFile.Close() + + // Execute template + if err := tmpl.Execute(outFile, config); err != nil { + return fmt.Errorf("failed to execute template %s: %v", path, err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("error walking config files: %v", err) + } + + // get the current directory + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %v", err) + } + + sourcePath := filepath.Join(dir, "config/docker-compose.yml") + destPath := filepath.Join(dir, "docker-compose.yml") + + // Check if source file exists + if _, err := os.Stat(sourcePath); err != nil { + return fmt.Errorf("source docker-compose.yml not found: %v", err) + } + + // Try to move the file + err = os.Rename(sourcePath, destPath) + if err != nil { + return fmt.Errorf("failed to move docker-compose.yml from %s to %s: %v", + sourcePath, destPath, err) + } + + return nil +} diff --git a/install/fs/crowdsec/acquis.yaml b/install/crowdsec/acquis.yaml similarity index 100% rename from install/fs/crowdsec/acquis.yaml rename to install/crowdsec/acquis.yaml diff --git a/install/fs/crowdsec/config.yaml b/install/crowdsec/config.yaml similarity index 100% rename from install/fs/crowdsec/config.yaml rename to install/crowdsec/config.yaml diff --git a/install/crowdsec/dynamic_config.yml b/install/crowdsec/dynamic_config.yml new file mode 100644 index 00000000..9175b143 --- /dev/null +++ b/install/crowdsec/dynamic_config.yml @@ -0,0 +1,108 @@ +http: + middlewares: + redirect-to-https: + redirectScheme: + scheme: https + default-whitelist: # Whitelist middleware for internal IPs + ipWhiteList: # Internal IP addresses + sourceRange: # Internal IP addresses + - "10.0.0.0/8" # Internal IP addresses + - "192.168.0.0/16" # Internal IP addresses + - "172.16.0.0/12" # Internal IP addresses + # Basic security headers + security-headers: + headers: + customResponseHeaders: # Custom response headers + Server: "" # Remove server header + X-Powered-By: "" # Remove powered by header + X-Forwarded-Proto: "https" # Set forwarded proto to https + sslProxyHeaders: # SSL proxy headers + X-Forwarded-Proto: "https" # Set forwarded proto to https + hostsProxyHeaders: # Hosts proxy headers + - "X-Forwarded-Host" # Set forwarded host + contentTypeNosniff: true # Prevent MIME sniffing + customFrameOptionsValue: "SAMEORIGIN" # Set frame options + referrerPolicy: "strict-origin-when-cross-origin" # Set referrer policy + forceSTSHeader: true # Force STS header + stsIncludeSubdomains: true # Include subdomains + stsSeconds: 63072000 # STS seconds + stsPreload: true # Preload STS + # CrowdSec configuration with proper IP forwarding + crowdsec: + plugin: + crowdsec: + enabled: true # Enable CrowdSec plugin + logLevel: INFO # Log level + updateIntervalSeconds: 15 # Update interval + updateMaxFailure: 0 # Update max failure + defaultDecisionSeconds: 15 # Default decision seconds + httpTimeoutSeconds: 10 # HTTP timeout + crowdsecMode: live # CrowdSec mode + crowdsecAppsecEnabled: true # Enable AppSec + crowdsecAppsecHost: crowdsec:7422 # CrowdSec IP address which you noted down later + crowdsecAppsecFailureBlock: true # Block on failure + crowdsecAppsecUnreachableBlock: true # Block on unreachable + crowdsecLapiKey: "{{.TraefikBouncerKey}}" # CrowdSec API key which you noted down later + crowdsecLapiHost: crowdsec:9090 # CrowdSec + crowdsecLapiScheme: http # CrowdSec API scheme + forwardedHeadersTrustedIPs: # Forwarded headers trusted IPs + - "0.0.0.0/0" # All IP addresses are trusted for forwarded headers (CHANGE MADE HERE) + clientTrustedIPs: # Client trusted IPs (CHANGE MADE HERE) + - "10.0.0.0/8" # Internal LAN IP addresses + - "172.16.0.0/12" # Internal LAN IP addresses + - "192.168.0.0/16" # Internal LAN IP addresses + - "100.89.137.0/20" # Internal LAN IP addresses + + routers: + # HTTP to HTTPS redirect router + main-app-router-redirect: + rule: "Host(`{{.DomainName}}`)" # Dynamic Domain Name + service: next-service + entryPoints: + - web + middlewares: + - redirect-to-https + + # Next.js router (handles everything except API and WebSocket paths) + next-router: + rule: "Host(`{{.DomainName}}`) && !PathPrefix(`/api/v1`)" # Dynamic Domain Name + service: next-service + entryPoints: + - websecure + middlewares: + - security-headers # Add security headers middleware + tls: + certResolver: letsencrypt + + # API router (handles /api/v1 paths) + api-router: + rule: "Host(`{{.DomainName}}`) && PathPrefix(`/api/v1`)" # Dynamic Domain Name + service: api-service + entryPoints: + - websecure + middlewares: + - security-headers # Add security headers middleware + tls: + certResolver: letsencrypt + + # WebSocket router + ws-router: + rule: "Host(`{{.DomainName}}`)" # Dynamic Domain Name + service: api-service + entryPoints: + - websecure + middlewares: + - security-headers # Add security headers middleware + tls: + certResolver: letsencrypt + + services: + next-service: + loadBalancer: + servers: + - url: "http://pangolin:3002" # Next.js server + + api-service: + loadBalancer: + servers: + - url: "http://pangolin:3000" # API/WebSocket server \ No newline at end of file diff --git a/install/fs/crowdsec/local_api_credentials.yaml b/install/crowdsec/local_api_credentials.yaml similarity index 100% rename from install/fs/crowdsec/local_api_credentials.yaml rename to install/crowdsec/local_api_credentials.yaml diff --git a/install/fs/crowdsec/profiles.yaml b/install/crowdsec/profiles.yaml similarity index 100% rename from install/fs/crowdsec/profiles.yaml rename to install/crowdsec/profiles.yaml diff --git a/install/crowdsec/traefik_config.yml b/install/crowdsec/traefik_config.yml new file mode 100644 index 00000000..2ac9125c --- /dev/null +++ b/install/crowdsec/traefik_config.yml @@ -0,0 +1,87 @@ +api: + insecure: true + dashboard: true + +providers: + http: + endpoint: "http://pangolin:3001/api/v1/traefik-config" + pollInterval: "5s" + file: + filename: "/etc/traefik/dynamic_config.yml" + +experimental: + plugins: + badger: + moduleName: "github.com/fosrl/badger" + version: "{{.BadgerVersion}}" + crowdsec: # CrowdSec plugin configuration added + moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" + version: "v1.3.5" + +log: + level: "INFO" + format: "json" # Log format changed to json for better parsing + +accessLog: # We enable access logs as json + filePath: "/var/log/traefik/access.log" + format: json + filters: + statusCodes: + - "200-299" # Success codes + - "400-499" # Client errors + - "500-599" # Server errors + retryAttempts: true + minDuration: "100ms" # Increased to focus on slower requests + bufferingSize: 100 # Add buffering for better performance + fields: + defaultMode: drop # Start with dropping all fields + names: + ClientAddr: keep # Keep client address for IP tracking + ClientHost: keep # Keep client host for IP tracking + RequestMethod: keep # Keep request method for tracking + RequestPath: keep # Keep request path for tracking + RequestProtocol: keep # Keep request protocol for tracking + DownstreamStatus: keep # Keep downstream status for tracking + DownstreamContentSize: keep # Keep downstream content size for tracking + Duration: keep # Keep request duration for tracking + ServiceName: keep # Keep service name for tracking + StartUTC: keep # Keep start time for tracking + TLSVersion: keep # Keep TLS version for tracking + TLSCipher: keep # Keep TLS cipher for tracking + RetryAttempts: keep # Keep retry attempts for tracking + headers: + defaultMode: drop # Start with dropping all headers + names: + User-Agent: keep # Keep user agent for tracking + X-Real-Ip: keep # Keep real IP for tracking + X-Forwarded-For: keep # Keep forwarded IP for tracking + X-Forwarded-Proto: keep # Keep forwarded protocol for tracking + Content-Type: keep # Keep content type for tracking + Authorization: redact # Redact sensitive information + Cookie: redact # Redact sensitive information + +certificatesResolvers: + letsencrypt: + acme: + httpChallenge: + entryPoint: web + email: "{{.LetsEncryptEmail}}" + storage: "/letsencrypt/acme.json" + caServer: "https://acme-v02.api.letsencrypt.org/directory" + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + transport: + respondingTimeouts: + readTimeout: "30m" + http: + tls: + certResolver: "letsencrypt" + middlewares: # CHANGE MADE HERE (BOUNCER ENABLED) !!! + - crowdsec@file + +serversTransport: + insecureSkipVerify: true \ No newline at end of file diff --git a/install/main.go b/install/main.go index 4f2deb3a..d5bf5e1e 100644 --- a/install/main.go +++ b/install/main.go @@ -45,6 +45,7 @@ type Config struct { EmailSMTPPass string EmailNoReply string InstallGerbil bool + TraefikBouncerKey string } func main() {