From d22c7826fe63ab58e44988597f3df260192507c8 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 9 Feb 2025 16:14:29 -0500 Subject: [PATCH] Add config files --- install/captcha.html | 338 ++++++++++++++++ install/crowdsec/install.go | 376 ++++++++++++++++++ install/fs/crowdsec/acquis.yaml | 18 + install/fs/crowdsec/config.yaml | 12 + .../fs/crowdsec/local_api_credentials.yaml | 2 + install/fs/crowdsec/profiles.yaml | 25 ++ 6 files changed, 771 insertions(+) create mode 100644 install/captcha.html create mode 100644 install/crowdsec/install.go create mode 100644 install/fs/crowdsec/acquis.yaml create mode 100644 install/fs/crowdsec/config.yaml create mode 100644 install/fs/crowdsec/local_api_credentials.yaml create mode 100644 install/fs/crowdsec/profiles.yaml diff --git a/install/captcha.html b/install/captcha.html new file mode 100644 index 00000000..a40d37a3 --- /dev/null +++ b/install/captcha.html @@ -0,0 +1,338 @@ + + + + + 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/install.go new file mode 100644 index 00000000..84f3089b --- /dev/null +++ b/install/crowdsec/install.go @@ -0,0 +1,376 @@ +package crowdsec + +import ( + "bytes" + "embed" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "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 +} + +// DockerContainer represents a Docker container +type DockerContainer struct { + NetworkSettings struct { + Networks map[string]struct { + IPAddress string `json:"IPAddress"` + } `json:"Networks"` + } `json:"NetworkSettings"` +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + // Create configuration + config := &Config{} + + // Run installation steps + if err := backupConfig(); err != nil { + 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 { + return fmt.Errorf("config file creation failed: %v", err) + } + + if err := retrieveIPs(config); err != nil { + return fmt.Errorf("IP retrieval failed: %v", err) + } + + 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) + } + + if err := verifyDeployment(); err != nil { + return fmt.Errorf("verification failed: %v", err) + } + + printInstructions() + return nil +} + +func backupConfig() error { + // Backup docker-compose.yml + if _, err := os.Stat("docker-compose.yml"); err == nil { + if err := copyFile("docker-compose.yml", "docker-compose.yml.backup"); err != nil { + return fmt.Errorf("failed to backup docker-compose.yml: %v", err) + } + } + + // Backup config directory + if _, err := os.Stat("config"); err == nil { + cmd := exec.Command("tar", "-czvf", "config.tar.gz", "config") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to backup config directory: %v", err) + } + } + + 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") + if err != nil { + return fmt.Errorf("failed to read docker-compose.yml: %v", err) + } + + // Verify required services exist + requiredServices := []string{"services:", "pangolin:", "gerbil:", "traefik:"} + for _, service := range requiredServices { + if !bytes.Contains(content, []byte(service)) { + return fmt.Errorf("required service %s not found in docker-compose.yml", service) + } + } + + // Add crowdsec service + modified := addCrowdsecService(string(content)) + + // Write modified content + if err := os.WriteFile("docker-compose.yml", []byte(modified), 0644); err != nil { + return fmt.Errorf("failed to write modified docker-compose.yml: %v", err) + } + + 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") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to start crowdsec: %v", err) + } + defer exec.Command("docker", "compose", "down").Run() + + // Wait for container to start + time.Sleep(10 * time.Second) + + // Get bouncer key + output, err := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer").Output() + if err != nil { + return fmt.Errorf("failed to get bouncer key: %v", err) + } + + // Parse key from output + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "key:") { + config.TraefikBouncerKey = strings.TrimSpace(strings.Split(line, ":")[1]) + break + } + } + + 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 { + return fmt.Errorf("failed to deploy stack: %v", err) + } + + fmt.Println("Stack deployed. Waiting 2 minutes for services to initialize...") + time.Sleep(2 * time.Minute) + return nil +} + +func verifyDeployment() error { + resp, err := exec.Command("curl", "-s", "http://localhost:6060/metrics").Output() + if err != nil { + return fmt.Errorf("failed to get metrics: %v", err) + } + + if !bytes.Contains(resp, []byte("appsec")) { + return fmt.Errorf("appsec metrics not found in response") + } + + 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 { + return err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return err + } + defer destination.Close() + + _, err = io.Copy(destination, source) + return err +} + +func getContainerIP(containerName string) (string, error) { + output, err := exec.Command("docker", "inspect", containerName).Output() + if 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") +} + +func addCrowdsecService(content string) string { + // Implementation of adding crowdsec service to docker-compose.yml + // This would involve string manipulation or template rendering + // The actual implementation would depend on how you want to structure the docker-compose modifications + return content + ` + crowdsec: + image: crowdsecurity/crowdsec:latest + container_name: crowdsec + environment: + GID: "${GID-1000}" + 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: + - gerbil + labels: + - "traefik.enable=false" + volumes: + - ./config/crowdsec:/etc/crowdsec + - ./config/crowdsec/db:/var/lib/crowdsec/data + - ./config/crowdsec_logs/auth.log:/var/log/auth.log:ro + - ./config/crowdsec_logs/syslog:/var/log/syslog:ro + - ./config/crowdsec_logs:/var/log + - ./config/traefik/logs:/var/log/traefik + ports: + - 9090:9090 + - 6060:6060 + expose: + - 9090 + - 6060 + - 7422 + restart: unless-stopped + command: -t` +} diff --git a/install/fs/crowdsec/acquis.yaml b/install/fs/crowdsec/acquis.yaml new file mode 100644 index 00000000..74d8fd1c --- /dev/null +++ b/install/fs/crowdsec/acquis.yaml @@ -0,0 +1,18 @@ +filenames: + - /var/log/auth.log + - /var/log/syslog +labels: + type: syslog +--- +poll_without_inotify: false +filenames: + - /var/log/traefik/*.log +labels: + type: traefik +--- +listen_addr: 0.0.0.0:7422 +appsec_config: crowdsecurity/appsec-default +name: myAppSecComponent +source: appsec +labels: + type: appsec \ No newline at end of file diff --git a/install/fs/crowdsec/config.yaml b/install/fs/crowdsec/config.yaml new file mode 100644 index 00000000..0acf4635 --- /dev/null +++ b/install/fs/crowdsec/config.yaml @@ -0,0 +1,12 @@ +api: + client: + insecure_skip_verify: false + credentials_path: /etc/crowdsec/local_api_credentials.yaml + server: + log_level: info + listen_uri: 0.0.0.0:9090 + profiles_path: /etc/crowdsec/profiles.yaml + trusted_ips: + - 0.0.0.0/0 + - 127.0.0.1 + - ::1 \ No newline at end of file diff --git a/install/fs/crowdsec/local_api_credentials.yaml b/install/fs/crowdsec/local_api_credentials.yaml new file mode 100644 index 00000000..8776e4fd --- /dev/null +++ b/install/fs/crowdsec/local_api_credentials.yaml @@ -0,0 +1,2 @@ +url: http://0.0.0.0:9090 +login: localhost \ No newline at end of file diff --git a/install/fs/crowdsec/profiles.yaml b/install/fs/crowdsec/profiles.yaml new file mode 100644 index 00000000..3796b47f --- /dev/null +++ b/install/fs/crowdsec/profiles.yaml @@ -0,0 +1,25 @@ +name: captcha_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http" +decisions: + - type: captcha + duration: 4h +on_success: break + +--- +name: default_ip_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" +decisions: + - type: ban + duration: 4h +on_success: break + +--- +name: default_range_remediation +filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" +decisions: + - type: ban + duration: 4h +on_success: break \ No newline at end of file