mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-12 15:04:53 +02:00
commit
b83dadb14b
15 changed files with 1014 additions and 63 deletions
353
install/config.go
Normal file
353
install/config.go
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TraefikConfig represents the structure of the main Traefik configuration
|
||||||
|
type TraefikConfig struct {
|
||||||
|
Experimental struct {
|
||||||
|
Plugins struct {
|
||||||
|
Badger struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
} `yaml:"badger"`
|
||||||
|
} `yaml:"plugins"`
|
||||||
|
} `yaml:"experimental"`
|
||||||
|
CertificatesResolvers struct {
|
||||||
|
LetsEncrypt struct {
|
||||||
|
Acme struct {
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
} `yaml:"acme"`
|
||||||
|
} `yaml:"letsencrypt"`
|
||||||
|
} `yaml:"certificatesResolvers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicConfig represents the structure of the dynamic configuration
|
||||||
|
type DynamicConfig struct {
|
||||||
|
HTTP struct {
|
||||||
|
Routers map[string]struct {
|
||||||
|
Rule string `yaml:"rule"`
|
||||||
|
} `yaml:"routers"`
|
||||||
|
} `yaml:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigValues holds the extracted configuration values
|
||||||
|
type ConfigValues struct {
|
||||||
|
DashboardDomain string
|
||||||
|
LetsEncryptEmail string
|
||||||
|
BadgerVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadTraefikConfig reads and extracts values from Traefik configuration files
|
||||||
|
func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) {
|
||||||
|
// Read main config file
|
||||||
|
mainConfigData, err := os.ReadFile(mainConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading main config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainConfig TraefikConfig
|
||||||
|
if err := yaml.Unmarshal(mainConfigData, &mainConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing main config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read dynamic config file
|
||||||
|
dynamicConfigData, err := os.ReadFile(dynamicConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading dynamic config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicConfig DynamicConfig
|
||||||
|
if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing dynamic config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract values
|
||||||
|
values := &ConfigValues{
|
||||||
|
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
||||||
|
LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract DashboardDomain from router rules
|
||||||
|
// Look for it in the main router rules
|
||||||
|
for _, router := range dynamicConfig.HTTP.Routers {
|
||||||
|
if router.Rule != "" {
|
||||||
|
// Extract domain from Host(`mydomain.com`)
|
||||||
|
if domain := extractDomainFromRule(router.Rule); domain != "" {
|
||||||
|
values.DashboardDomain = domain
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractDomainFromRule extracts the domain from a router rule
|
||||||
|
func extractDomainFromRule(rule string) string {
|
||||||
|
// Look for the Host(`mydomain.com`) pattern
|
||||||
|
if start := findPattern(rule, "Host(`"); start != -1 {
|
||||||
|
end := findPattern(rule[start:], "`)")
|
||||||
|
if end != -1 {
|
||||||
|
return rule[start+6 : start+end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// findPattern finds the start of a pattern in a string
|
||||||
|
func findPattern(s, pattern string) int {
|
||||||
|
return bytes.Index([]byte(s), []byte(pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyDockerService(sourceFile, destFile, serviceName string) error {
|
||||||
|
// Read source file
|
||||||
|
sourceData, err := os.ReadFile(sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading source file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read destination file
|
||||||
|
destData, err := os.ReadFile(destFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading destination file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse source Docker Compose YAML
|
||||||
|
var sourceCompose map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
||||||
|
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse destination Docker Compose YAML
|
||||||
|
var destCompose map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
||||||
|
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get services section from source
|
||||||
|
sourceServices, ok := sourceCompose["services"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("services section not found in source file or has invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the specific service configuration
|
||||||
|
serviceConfig, ok := sourceServices[serviceName]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("service '%s' not found in source file", serviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create services section in destination
|
||||||
|
destServices, ok := destCompose["services"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
// If services section doesn't exist, create it
|
||||||
|
destServices = make(map[string]interface{})
|
||||||
|
destCompose["services"] = destServices
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update service in destination
|
||||||
|
destServices[serviceName] = serviceConfig
|
||||||
|
|
||||||
|
// Marshal updated destination YAML
|
||||||
|
// Use yaml.v3 encoder to preserve formatting and comments
|
||||||
|
// updatedData, err := yaml.Marshal(destCompose)
|
||||||
|
updatedData, err := MarshalYAMLWithIndent(destCompose, 2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling updated Docker Compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write updated YAML back to destination file
|
||||||
|
if err := os.WriteFile(destFile, updatedData, 0644); err != nil {
|
||||||
|
return fmt.Errorf("error writing to destination file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := yaml.NewEncoder(buffer)
|
||||||
|
encoder.SetIndent(indent)
|
||||||
|
|
||||||
|
err := encoder.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer encoder.Close()
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceInFile(filepath, oldStr, newStr string) error {
|
||||||
|
// Read the file content
|
||||||
|
content, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the string
|
||||||
|
newContent := strings.Replace(string(content), oldStr, newStr, -1)
|
||||||
|
|
||||||
|
// Write the modified content back to the file
|
||||||
|
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error writing file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckAndAddTraefikLogVolume(composePath string) error {
|
||||||
|
// Read the docker-compose.yml file
|
||||||
|
data, err := os.ReadFile(composePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse YAML into a generic map
|
||||||
|
var compose map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get services section
|
||||||
|
services, ok := compose["services"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("services section not found or invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get traefik service
|
||||||
|
traefik, ok := services["traefik"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check volumes
|
||||||
|
logVolume := "./config/traefik/logs:/var/log/traefik"
|
||||||
|
var volumes []interface{}
|
||||||
|
|
||||||
|
if existingVolumes, ok := traefik["volumes"].([]interface{}); ok {
|
||||||
|
// Check if volume already exists
|
||||||
|
for _, v := range existingVolumes {
|
||||||
|
if v.(string) == logVolume {
|
||||||
|
fmt.Println("Traefik log volume is already configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
volumes = existingVolumes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new volume
|
||||||
|
volumes = append(volumes, logVolume)
|
||||||
|
traefik["volumes"] = volumes
|
||||||
|
|
||||||
|
// Write updated config back to file
|
||||||
|
newData, err := MarshalYAMLWithIndent(compose, 2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling updated compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(composePath, newData, 0644); err != nil {
|
||||||
|
return fmt.Errorf("error writing updated compose file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Added traefik log volume and created logs directory")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeYAML merges two YAML files, where the contents of the second file
|
||||||
|
// are merged into the first file. In case of conflicts, values from the
|
||||||
|
// second file take precedence.
|
||||||
|
func MergeYAML(baseFile, overlayFile string) error {
|
||||||
|
// Read the base YAML file
|
||||||
|
baseContent, err := os.ReadFile(baseFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading base file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the overlay YAML file
|
||||||
|
overlayContent, err := os.ReadFile(overlayFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading overlay file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse base YAML into a map
|
||||||
|
var baseMap map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
||||||
|
return fmt.Errorf("error parsing base YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse overlay YAML into a map
|
||||||
|
var overlayMap map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
||||||
|
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the overlay into the base
|
||||||
|
merged := mergeMap(baseMap, overlayMap)
|
||||||
|
|
||||||
|
// Marshal the merged result back to YAML
|
||||||
|
mergedContent, err := MarshalYAMLWithIndent(merged, 2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling merged YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the merged content back to the base file
|
||||||
|
if err := os.WriteFile(baseFile, mergedContent, 0644); err != nil {
|
||||||
|
return fmt.Errorf("error writing merged YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeMap recursively merges two maps
|
||||||
|
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
|
||||||
|
// Copy all key-values from base map
|
||||||
|
for k, v := range base {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge overlay values
|
||||||
|
for k, v := range overlay {
|
||||||
|
// If both maps have the same key and both values are maps, merge recursively
|
||||||
|
if baseVal, ok := base[k]; ok {
|
||||||
|
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
|
||||||
|
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
|
||||||
|
result[k] = mergeMap(baseMap, overlayMap)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, overlay value takes precedence
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
18
install/config/crowdsec/acquis.yaml
Normal file
18
install/config/crowdsec/acquis.yaml
Normal file
|
@ -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
|
35
install/config/crowdsec/docker-compose.yml
Normal file
35
install/config/crowdsec/docker-compose.yml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
services:
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:latest
|
||||||
|
container_name: crowdsec
|
||||||
|
environment:
|
||||||
|
GID: "1000"
|
||||||
|
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||||
|
ENROLL_INSTANCE_NAME: "pangolin-crowdsec"
|
||||||
|
PARSERS: crowdsecurity/whitelists
|
||||||
|
ACQUIRE_FILES: "/var/log/traefik/*.log"
|
||||||
|
ENROLL_TAGS: docker
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "cscli", "capi", "status"]
|
||||||
|
depends_on:
|
||||||
|
- gerbil # Wait for gerbil to be healthy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false" # Disable traefik for crowdsec
|
||||||
|
volumes:
|
||||||
|
# crowdsec container data
|
||||||
|
- ./config/crowdsec:/etc/crowdsec # crowdsec config
|
||||||
|
- ./config/crowdsec/db:/var/lib/crowdsec/data # crowdsec db
|
||||||
|
# log bind mounts into crowdsec
|
||||||
|
- ./config/crowdsec_logs/auth.log:/var/log/auth.log:ro # auth.log
|
||||||
|
- ./config/crowdsec_logs/syslog:/var/log/syslog:ro # syslog
|
||||||
|
- ./config/crowdsec_logs:/var/log # crowdsec logs
|
||||||
|
- ./config/traefik/logs:/var/log/traefik # traefik logs
|
||||||
|
ports:
|
||||||
|
- 9090:9090 # port mapping for local firewall bouncers
|
||||||
|
- 6060:6060 # metrics endpoint for prometheus
|
||||||
|
expose:
|
||||||
|
- 9090 # http api for bouncers
|
||||||
|
- 6060 # metrics endpoint for prometheus
|
||||||
|
- 7422 # appsec waf endpoint
|
||||||
|
restart: unless-stopped
|
||||||
|
command: -t # Add test config flag to verify configuration
|
108
install/config/crowdsec/dynamic_config.yml
Normal file
108
install/config/crowdsec/dynamic_config.yml
Normal file
|
@ -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: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later
|
||||||
|
crowdsecLapiHost: crowdsec:8080 # 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(`{{.DashboardDomain}}`)" # 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(`{{.DashboardDomain}}`) && !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(`{{.DashboardDomain}}`) && 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(`{{.DashboardDomain}}`)" # 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
|
25
install/config/crowdsec/profiles.yaml
Normal file
25
install/config/crowdsec/profiles.yaml
Normal file
|
@ -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
|
87
install/config/crowdsec/traefik_config.yml
Normal file
87
install/config/crowdsec/traefik_config.yml
Normal file
|
@ -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:
|
||||||
|
- crowdsec@file
|
||||||
|
|
||||||
|
serversTransport:
|
||||||
|
insecureSkipVerify: true
|
|
@ -11,7 +11,6 @@ services:
|
||||||
interval: "3s"
|
interval: "3s"
|
||||||
timeout: "3s"
|
timeout: "3s"
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
gerbil:
|
gerbil:
|
||||||
image: fosrl/gerbil:{{.GerbilVersion}}
|
image: fosrl/gerbil:{{.GerbilVersion}}
|
||||||
|
@ -35,15 +34,13 @@ services:
|
||||||
- 443:443 # Port for traefik because of the network_mode
|
- 443:443 # Port for traefik because of the network_mode
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.3.3
|
image: traefik:v3.3.3
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
{{end}}
|
{{end}}{{if not .InstallGerbil}}
|
||||||
{{if not .InstallGerbil}}
|
|
||||||
ports:
|
ports:
|
||||||
- 443:443
|
- 443:443
|
||||||
- 80:80
|
- 80:80
|
||||||
|
@ -56,6 +53,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
|
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
121
install/crowdsec.go
Normal file
121
install/crowdsec.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func installCrowdsec(config Config) error {
|
||||||
|
|
||||||
|
if err := stopContainers(); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run installation steps
|
||||||
|
if err := backupConfig(); err != nil {
|
||||||
|
return fmt.Errorf("backup failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createConfigFiles(config); err != nil {
|
||||||
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.MkdirAll("config/crowdsec/db", 0755)
|
||||||
|
os.MkdirAll("config/crowdsec_logs/syslog", 0755)
|
||||||
|
os.MkdirAll("config/traefik/logs", 0755)
|
||||||
|
|
||||||
|
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
||||||
|
fmt.Printf("Error copying docker service: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := MergeYAML("config/traefik/traefik_config.yml", "config/crowdsec/traefik_config.yml"); err != nil {
|
||||||
|
fmt.Printf("Error copying entry points: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// delete the 2nd file
|
||||||
|
if err := os.Remove("config/crowdsec/traefik_config.yml"); err != nil {
|
||||||
|
fmt.Printf("Error removing file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := MergeYAML("config/traefik/dynamic_config.yml", "config/crowdsec/dynamic_config.yml"); err != nil {
|
||||||
|
fmt.Printf("Error copying entry points: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// delete the 2nd file
|
||||||
|
if err := os.Remove("config/crowdsec/dynamic_config.yml"); err != nil {
|
||||||
|
fmt.Printf("Error removing file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove("config/crowdsec/docker-compose.yml"); err != nil {
|
||||||
|
fmt.Printf("Error removing file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CheckAndAddTraefikLogVolume("docker-compose.yml"); err != nil {
|
||||||
|
fmt.Printf("Error checking and adding Traefik log volume: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := startContainers(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get API key
|
||||||
|
apiKey, err := GetCrowdSecAPIKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get API key: %v", err)
|
||||||
|
}
|
||||||
|
config.TraefikBouncerKey = apiKey
|
||||||
|
|
||||||
|
if err := replaceInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK", config.TraefikBouncerKey); err != nil {
|
||||||
|
return fmt.Errorf("failed to replace bouncer key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := restartContainer("traefik"); err != nil {
|
||||||
|
return fmt.Errorf("failed to restart containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIsCrowdsecInstalledInCompose() bool {
|
||||||
|
// Read docker-compose.yml
|
||||||
|
content, err := os.ReadFile("docker-compose.yml")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for crowdsec service
|
||||||
|
return bytes.Contains(content, []byte("crowdsec:"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCrowdSecAPIKey() (string, error) {
|
||||||
|
// First, ensure the container is running
|
||||||
|
if err := waitForContainer("crowdsec"); err != nil {
|
||||||
|
return "", fmt.Errorf("waiting for container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the command to get the API key
|
||||||
|
cmd := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", fmt.Errorf("executing command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim any whitespace from the output
|
||||||
|
apiKey := strings.TrimSpace(out.String())
|
||||||
|
if apiKey == "" {
|
||||||
|
return "", fmt.Errorf("empty API key returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey, nil
|
||||||
|
}
|
|
@ -5,4 +5,5 @@ go 1.23.0
|
||||||
require (
|
require (
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/term v0.28.0 // indirect
|
golang.org/x/term v0.28.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,3 +2,6 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
12
install/input.txt
Normal file
12
install/input.txt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
example.com
|
||||||
|
pangolin.example.com
|
||||||
|
admin@example.com
|
||||||
|
yes
|
||||||
|
admin@example.com
|
||||||
|
Password123!
|
||||||
|
Password123!
|
||||||
|
yes
|
||||||
|
no
|
||||||
|
no
|
||||||
|
no
|
||||||
|
yes
|
308
install/main.go
308
install/main.go
|
@ -4,13 +4,16 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"bytes"
|
||||||
"text/template"
|
"text/template"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
@ -24,7 +27,7 @@ func loadVersions(config *Config) {
|
||||||
config.BadgerVersion = "replaceme"
|
config.BadgerVersion = "replaceme"
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed fs/*
|
//go:embed config/*
|
||||||
var configFiles embed.FS
|
var configFiles embed.FS
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -45,6 +48,8 @@ type Config struct {
|
||||||
EmailSMTPPass string
|
EmailSMTPPass string
|
||||||
EmailNoReply string
|
EmailNoReply string
|
||||||
InstallGerbil bool
|
InstallGerbil bool
|
||||||
|
TraefikBouncerKey string
|
||||||
|
DoCrowdsecInstall bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -56,9 +61,12 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
config.DoCrowdsecInstall = false
|
||||||
|
|
||||||
// check if there is already a config file
|
// check if there is already a config file
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
config := collectUserInput(reader)
|
config = collectUserInput(reader)
|
||||||
|
|
||||||
loadVersions(&config)
|
loadVersions(&config)
|
||||||
|
|
||||||
|
@ -67,18 +75,53 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
installDocker()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
|
if isDockerInstalled() {
|
||||||
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
|
pullAndStartContainers()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Config file already exists... skipping configuration")
|
fmt.Println("Looks like you already installed, so I am going to do the setup...")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isDockerInstalled() {
|
if !checkIsCrowdsecInstalledInCompose() {
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
fmt.Println("\n=== Crowdsec Install ===")
|
||||||
pullAndStartContainers()
|
// check if crowdsec is installed
|
||||||
|
if readBool(reader, "Would you like to install Crowdsec?", true) {
|
||||||
|
|
||||||
|
if config.DashboardDomain == "" {
|
||||||
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading config: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.DashboardDomain = traefikConfig.DashboardDomain
|
||||||
|
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
||||||
|
config.BadgerVersion = traefikConfig.BadgerVersion
|
||||||
|
|
||||||
|
// print the values and check if they are right
|
||||||
|
fmt.Println("Detected values:")
|
||||||
|
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
|
||||||
|
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
||||||
|
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
||||||
|
|
||||||
|
if !readBool(reader, "Are these values correct?", true) {
|
||||||
|
config = collectUserInput(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.DoCrowdsecInstall = true
|
||||||
|
installCrowdsec(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,22 +142,24 @@ func readString(reader *bufio.Reader, prompt string, defaultValue string) string
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPassword(prompt string) string {
|
func readPassword(prompt string, reader *bufio.Reader) string {
|
||||||
fmt.Print(prompt + ": ")
|
if term.IsTerminal(int(syscall.Stdin)) {
|
||||||
|
fmt.Print(prompt + ": ")
|
||||||
// Read password without echo
|
// Read password without echo if we're in a terminal
|
||||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
||||||
|
if err != nil {
|
||||||
if err != nil {
|
return ""
|
||||||
return ""
|
}
|
||||||
}
|
input := strings.TrimSpace(string(password))
|
||||||
|
if input == "" {
|
||||||
input := strings.TrimSpace(string(password))
|
return readPassword(prompt, reader)
|
||||||
if input == "" {
|
}
|
||||||
return readPassword(prompt)
|
return input
|
||||||
}
|
} else {
|
||||||
return input
|
// Fallback to reading from stdin if not in a terminal
|
||||||
|
return readString(reader, prompt, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
||||||
|
@ -150,8 +195,8 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
fmt.Println("\n=== Admin User Configuration ===")
|
fmt.Println("\n=== Admin User Configuration ===")
|
||||||
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
|
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
|
||||||
for {
|
for {
|
||||||
pass1 := readPassword("Create admin user password")
|
pass1 := readPassword("Create admin user password", reader)
|
||||||
pass2 := readPassword("Confirm admin user password")
|
pass2 := readPassword("Confirm admin user password", reader)
|
||||||
|
|
||||||
if pass1 != pass2 {
|
if pass1 != pass2 {
|
||||||
fmt.Println("Passwords do not match")
|
fmt.Println("Passwords do not match")
|
||||||
|
@ -261,31 +306,33 @@ func createConfigFiles(config Config) error {
|
||||||
os.MkdirAll("config/logs", 0755)
|
os.MkdirAll("config/logs", 0755)
|
||||||
|
|
||||||
// Walk through all embedded files
|
// Walk through all embedded files
|
||||||
err := fs.WalkDir(configFiles, "fs", func(path string, d fs.DirEntry, err error) error {
|
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip the root fs directory itself
|
// Skip the root fs directory itself
|
||||||
if path == "fs" {
|
if path == "config" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the relative path by removing the "fs/" prefix
|
if !config.DoCrowdsecInstall && strings.Contains(path, "crowdsec") {
|
||||||
relPath := strings.TrimPrefix(path, "fs/")
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DoCrowdsecInstall && !strings.Contains(path, "crowdsec") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// skip .DS_Store
|
// skip .DS_Store
|
||||||
if strings.Contains(relPath, ".DS_Store") {
|
if strings.Contains(path, ".DS_Store") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the full output path under "config/"
|
|
||||||
outPath := filepath.Join("config", relPath)
|
|
||||||
|
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
// Create directory
|
// Create directory
|
||||||
if err := os.MkdirAll(outPath, 0755); err != nil {
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create directory %s: %v", outPath, err)
|
return fmt.Errorf("failed to create directory %s: %v", path, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -303,14 +350,14 @@ func createConfigFiles(config Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure parent directory exists
|
// Ensure parent directory exists
|
||||||
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create parent directory for %s: %v", outPath, err)
|
return fmt.Errorf("failed to create parent directory for %s: %v", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create output file
|
// Create output file
|
||||||
outFile, err := os.Create(outPath)
|
outFile, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create %s: %v", outPath, err)
|
return fmt.Errorf("failed to create %s: %v", path, err)
|
||||||
}
|
}
|
||||||
defer outFile.Close()
|
defer outFile.Close()
|
||||||
|
|
||||||
|
@ -326,30 +373,10 @@ func createConfigFiles(config Config) error {
|
||||||
return fmt.Errorf("error walking config files: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func installDocker() error {
|
func installDocker() error {
|
||||||
// Detect Linux distribution
|
// Detect Linux distribution
|
||||||
cmd := exec.Command("cat", "/etc/os-release")
|
cmd := exec.Command("cat", "/etc/os-release")
|
||||||
|
@ -490,3 +517,166 @@ func pullAndStartContainers() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bring containers down
|
||||||
|
func stopContainers() error {
|
||||||
|
fmt.Println("Stopping containers...")
|
||||||
|
|
||||||
|
// Check which docker compose command is available
|
||||||
|
var useNewStyle bool
|
||||||
|
checkCmd := exec.Command("docker", "compose", "version")
|
||||||
|
if err := checkCmd.Run(); err == nil {
|
||||||
|
useNewStyle = true
|
||||||
|
} else {
|
||||||
|
// Check if docker-compose (old style) is available
|
||||||
|
checkCmd = exec.Command("docker-compose", "version")
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to execute docker compose commands
|
||||||
|
executeCommand := func(args ...string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if useNewStyle {
|
||||||
|
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("docker-compose", args...)
|
||||||
|
}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executeCommand("-f", "docker-compose.yml", "down"); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// just start containers
|
||||||
|
func startContainers() error {
|
||||||
|
fmt.Println("Starting containers...")
|
||||||
|
|
||||||
|
// Check which docker compose command is available
|
||||||
|
var useNewStyle bool
|
||||||
|
checkCmd := exec.Command("docker", "compose", "version")
|
||||||
|
if err := checkCmd.Run(); err == nil {
|
||||||
|
useNewStyle = true
|
||||||
|
} else {
|
||||||
|
// Check if docker-compose (old style) is available
|
||||||
|
checkCmd = exec.Command("docker-compose", "version")
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to execute docker compose commands
|
||||||
|
executeCommand := func(args ...string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if useNewStyle {
|
||||||
|
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("docker-compose", args...)
|
||||||
|
}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executeCommand("-f", "docker-compose.yml", "up", "-d"); err != nil {
|
||||||
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartContainer(container string) error {
|
||||||
|
fmt.Printf("Restarting %s container...\n", container)
|
||||||
|
|
||||||
|
// Check which docker compose command is available
|
||||||
|
var useNewStyle bool
|
||||||
|
checkCmd := exec.Command("docker", "compose", "version")
|
||||||
|
if err := checkCmd.Run(); err == nil {
|
||||||
|
useNewStyle = true
|
||||||
|
} else {
|
||||||
|
// Check if docker-compose (old style) is available
|
||||||
|
checkCmd = exec.Command("docker-compose", "version")
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to execute docker compose commands
|
||||||
|
executeCommand := func(args ...string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if useNewStyle {
|
||||||
|
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("docker-compose", args...)
|
||||||
|
}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executeCommand("-f", "docker-compose.yml", "restart", container); err != nil {
|
||||||
|
return fmt.Errorf("failed to restart %s container: %v", container, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 moveFile(src, dst string) error {
|
||||||
|
if err := copyFile(src, dst); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Remove(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForContainer(containerName string) error {
|
||||||
|
maxAttempts := 30
|
||||||
|
retryInterval := time.Second * 2
|
||||||
|
|
||||||
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||||
|
// Check if container is running
|
||||||
|
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
// If the container doesn't exist or there's another error, wait and retry
|
||||||
|
time.Sleep(retryInterval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning := strings.TrimSpace(out.String()) == "true"
|
||||||
|
if isRunning {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container exists but isn't running yet, wait and retry
|
||||||
|
time.Sleep(retryInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue