mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-19 08:37:48 +01:00
376 lines
9.5 KiB
Go
376 lines
9.5 KiB
Go
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`
|
|
}
|