diff --git a/README.md b/README.md
index cc51f38..6d25016 100644
--- a/README.md
+++ b/README.md
@@ -147,7 +147,7 @@ services:
# Core Configuration
- PORT=8080
- ENVIRONMENT=production
- - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/dynamic_config.yml
+ - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/rules
- TRAEFIK_CONTAINER_NAME=traefik
- TRAEFIK_STATIC_CONFIG=/etc/traefik/traefik_config.yml
- CROWDSEC_METRICS_URL=http://crowdsec:6060/metrics
@@ -166,6 +166,8 @@ volumes:
tailscale-data:
```
+`TRAEFIK_DYNAMIC_CONFIG` can point to either a single YAML file such as `/etc/traefik/dynamic_config.yml` or a directory of fragments such as `/etc/traefik/rules`. When a directory is used, CrowdSec Manager writes its own overlay to `crowdsec-manager.yml` and leaves shared files like `base.yml` untouched.
+
## Run
```bash
diff --git a/docker-compose.pangolin.yml b/docker-compose.pangolin.yml
index 81abea8..343905d 100644
--- a/docker-compose.pangolin.yml
+++ b/docker-compose.pangolin.yml
@@ -130,7 +130,7 @@ services:
- PANGOLIN_DIR=/app
- CONFIG_DIR=/app/config
- DATABASE_PATH=/app/data/settings.db
- - TRAEFIK_DYNAMIC_CONFIG=/rules/dynamic_config.yml
+ - TRAEFIK_DYNAMIC_CONFIG=/rules
- TRAEFIK_STATIC_CONFIG=/etc/traefik/traefik_config.yml
- TRAEFIK_ACCESS_LOG=/var/log/traefik/access.log
- TRAEFIK_ERROR_LOG=/var/log/traefik/traefik.log
diff --git a/docker-compose.yml b/docker-compose.yml
index 3df6e32..f93381f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -21,7 +21,7 @@ services:
- CONFIG_DIR=/app/config
- DATABASE_PATH=/app/data/settings.db
# Traefik Configuration Paths
- - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/dynamic_config.yml
+ - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/rules
- TRAEFIK_STATIC_CONFIG=/etc/traefik/traefik_config.yml
- TRAEFIK_ACCESS_LOG=/var/log/traefik/access.log
- TRAEFIK_ERROR_LOG=/var/log/traefik/traefik.log
diff --git a/docs/content/docs/configuration/environment.mdx b/docs/content/docs/configuration/environment.mdx
index cee83e7..31bf481 100644
--- a/docs/content/docs/configuration/environment.mdx
+++ b/docs/content/docs/configuration/environment.mdx
@@ -13,7 +13,7 @@ The minimum deployment needs only a small environment set. Add more variables on
| :--- | :--- | :--- |
| `PORT` | `8080` | HTTP port inside the container. |
| `ENVIRONMENT` | `production` | Runtime mode. |
-| `TRAEFIK_DYNAMIC_CONFIG` | `/etc/traefik/dynamic_config.yml` | Traefik dynamic config path used by manager actions. |
+| `TRAEFIK_DYNAMIC_CONFIG` | `/etc/traefik/dynamic_config.yml` | Traefik dynamic config file or directory path used by manager actions. |
| `TRAEFIK_CONTAINER_NAME` | `traefik` | Traefik container name in your stack. |
| `TRAEFIK_STATIC_CONFIG` | `/etc/traefik/traefik_config.yml` | Traefik static config path. |
@@ -29,5 +29,6 @@ The minimum deployment needs only a small environment set. Add more variables on
## Notes
- Keep environment values as in-container paths.
+- `TRAEFIK_DYNAMIC_CONFIG` can point to a directory such as `/etc/traefik/rules`; CrowdSec Manager will then manage `crowdsec-manager.yml` inside that directory.
- Use Docker volume mappings to map host paths to those container paths.
- Multi-proxy support is not available in this release.
diff --git a/docs/content/docs/configuration/settings.mdx b/docs/content/docs/configuration/settings.mdx
index 2ed7e34..0d8bddb 100644
--- a/docs/content/docs/configuration/settings.mdx
+++ b/docs/content/docs/configuration/settings.mdx
@@ -11,9 +11,10 @@ The **Settings** page (labeled Configuration in the UI) allows you to manage cri
## Traefik Configuration Path
-The most important setting is the **Traefik Dynamic Configuration Path**. This tells CrowdSec Manager where to find the `dynamic_config.yml` file inside the Traefik container.
+The most important setting is the **Traefik Dynamic Configuration Path**. This tells CrowdSec Manager where to find the Traefik dynamic config inside the Traefik container.
- **Default Path**: `/etc/traefik/dynamic_config.yml`
+- **Common Directory Path**: `/etc/traefik/rules`
### Why is this important?
@@ -24,5 +25,5 @@ CrowdSec Manager uses this path to:
- Check middleware configurations.
- If your Traefik setup uses a different path or filename for its dynamic configuration, you **must** update it here. Otherwise, features like Captcha and Whitelists will not function correctly.
+ If your Traefik setup uses a different path, filename, or a directory of YAML fragments for its dynamic configuration, you **must** update it here. Otherwise, features like Captcha and Whitelists will not function correctly. In directory mode, CrowdSec Manager writes to `crowdsec-manager.yml` and does not modify shared files such as `base.yml`.
diff --git a/docs/content/docs/features/captcha.mdx b/docs/content/docs/features/captcha.mdx
index 43bf421..42926a9 100644
--- a/docs/content/docs/features/captcha.mdx
+++ b/docs/content/docs/features/captcha.mdx
@@ -36,7 +36,7 @@ The dashboard provides real-time feedback on your captcha configuration:
Enter the **Site Key** (public) and **Secret Key** (private) obtained from your provider's dashboard.
### Apply Configuration
- Click **Configure Captcha**. This will update the `dynamic_config.yml` file and ensure the `captcha.html` template is present.
+ Click **Configure Captcha**. This will update the configured Traefik dynamic config path and ensure the `captcha.html` template is present. If the path is a directory, CrowdSec Manager writes to `crowdsec-manager.yml`.
diff --git a/docs/content/docs/installation.mdx b/docs/content/docs/installation.mdx
index bc45830..e6281f9 100644
--- a/docs/content/docs/installation.mdx
+++ b/docs/content/docs/installation.mdx
@@ -35,7 +35,7 @@ services:
environment:
- PORT=8080
- ENVIRONMENT=production
- - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/dynamic_config.yml
+ - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/rules
- TRAEFIK_CONTAINER_NAME=traefik
- TRAEFIK_STATIC_CONFIG=/etc/traefik/traefik_config.yml
volumes:
@@ -73,3 +73,5 @@ curl http://localhost:8080/health
```
If the endpoint returns healthy status, open the UI on `http://localhost:8080` or behind your existing reverse proxy route.
+
+`TRAEFIK_DYNAMIC_CONFIG` may be a single file path or a directory path. For directory-based Traefik setups, point it at the directory and CrowdSec Manager will manage `crowdsec-manager.yml` inside that directory.
diff --git a/docs/content/docs/quick-start.mdx b/docs/content/docs/quick-start.mdx
index 588abe1..d0737bf 100644
--- a/docs/content/docs/quick-start.mdx
+++ b/docs/content/docs/quick-start.mdx
@@ -26,7 +26,7 @@ services:
environment:
- PORT=8080
- ENVIRONMENT=production
- - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/dynamic_config.yml
+ - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/rules
- TRAEFIK_CONTAINER_NAME=traefik
- TRAEFIK_STATIC_CONFIG=/etc/traefik/traefik_config.yml
volumes:
@@ -50,6 +50,8 @@ docker network create pangolin
docker compose up -d
```
+`TRAEFIK_DYNAMIC_CONFIG` accepts either a single YAML file path or a directory of Traefik config fragments. If you use a directory such as `/etc/traefik/rules`, CrowdSec Manager writes only `crowdsec-manager.yml` inside that directory.
+
## 4. Check health
```bash
diff --git a/docs/src/app/(home)/page.tsx b/docs/src/app/(home)/page.tsx
index 7d47569..323a41f 100644
--- a/docs/src/app/(home)/page.tsx
+++ b/docs/src/app/(home)/page.tsx
@@ -36,7 +36,7 @@ const quickInstall = `services:
environment:
- PORT=8080
- ENVIRONMENT=production
- - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/dynamic_config.yml
+ - TRAEFIK_DYNAMIC_CONFIG=/etc/traefik/rules
- TRAEFIK_CONTAINER_NAME=traefik
- TRAEFIK_STATIC_CONFIG=/etc/traefik/traefik_config.yml
volumes:
diff --git a/internal/api/handlers/captcha.go b/internal/api/handlers/captcha.go
index 2c2e916..bc1bdb7 100644
--- a/internal/api/handlers/captcha.go
+++ b/internal/api/handlers/captcha.go
@@ -13,6 +13,7 @@ import (
"crowdsec-manager/internal/docker"
"crowdsec-manager/internal/logger"
"crowdsec-manager/internal/models"
+ "crowdsec-manager/internal/traefikconfig"
"github.com/gin-gonic/gin"
)
@@ -96,10 +97,9 @@ func SetupCaptcha(dockerClient *docker.Client, cfg *config.Config) gin.HandlerFu
}
captchaHTMLPath := filepath.Join(cfg.ConfigDir, "traefik", "conf", "captcha.html")
- // STEP 2: Update Traefik dynamic_config.yml
+ // STEP 2: Update the CrowdSec-managed Traefik dynamic config path.
logger.Info("Updating Traefik dynamic configuration")
- traefikConfigDir := filepath.Join(cfg.ConfigDir, "traefik")
- if err := updateTraefikCaptchaConfig(dockerClient, cfg, req, traefikConfigDir); err != nil {
+ if err := updateTraefikCaptchaConfig(cfg, req); err != nil {
logger.Error("Failed to update Traefik config", "error", err)
c.JSON(http.StatusInternalServerError, models.Response{
Success: false,
@@ -192,7 +192,7 @@ func GetCaptchaStatus(dockerClient *docker.Client, db *database.Database, cfg *c
}
}
- // Supplementary: check dynamic_config.yml in Traefik container for live state
+ // Supplementary: check the Traefik dynamic config path in the container for live state.
dynamicConfigPath := cfg.TraefikDynamicConfig
if db != nil {
if path, err := db.GetTraefikDynamicConfigPath(); err == nil {
@@ -200,9 +200,8 @@ func GetCaptchaStatus(dockerClient *docker.Client, db *database.Database, cfg *c
}
}
- configContent, err := dockerClient.ExecCommand(cfg.TraefikContainerName, []string{
- "cat", dynamicConfigPath,
- })
+ readResult, err := traefikconfig.ReadContainer(dockerClient, cfg.TraefikContainerName, dynamicConfigPath)
+ configContent := readResult.Content
detectedProvider := ""
hasHTMLPath := false
diff --git a/internal/api/handlers/captcha_config.go b/internal/api/handlers/captcha_config.go
index c64d61b..fc34c08 100644
--- a/internal/api/handlers/captcha_config.go
+++ b/internal/api/handlers/captcha_config.go
@@ -3,7 +3,6 @@ package handlers
import (
"encoding/json"
"net/http"
- "path/filepath"
"strconv"
"crowdsec-manager/internal/config"
@@ -120,8 +119,6 @@ func ApplyCaptchaConfig(dockerClient *docker.Client, db *database.Database, cfg
}
// Define the pipeline of steps.
- traefikConfigDir := filepath.Join(cfg.ConfigDir, "traefik")
-
pipeline := []captchaApplyStep{
{
Num: 1,
@@ -134,7 +131,7 @@ func ApplyCaptchaConfig(dockerClient *docker.Client, db *database.Database, cfg
Num: 2,
Name: "Update Traefik dynamic config",
Run: func(r models.CaptchaSetupRequest) error {
- return updateTraefikCaptchaConfig(dockerClient, cfg, r, traefikConfigDir)
+ return updateTraefikCaptchaConfig(cfg, r)
},
},
{
diff --git a/internal/api/handlers/captcha_detect.go b/internal/api/handlers/captcha_detect.go
index f40be42..3db6a52 100644
--- a/internal/api/handlers/captcha_detect.go
+++ b/internal/api/handlers/captcha_detect.go
@@ -13,6 +13,7 @@ import (
"crowdsec-manager/internal/docker"
"crowdsec-manager/internal/logger"
"crowdsec-manager/internal/models"
+ "crowdsec-manager/internal/traefikconfig"
"github.com/gin-gonic/gin"
)
@@ -31,7 +32,7 @@ func DetectCaptchaConfig(dockerClient *docker.Client, db *database.Database, cfg
"html_file": false,
}
- // 1. Scan Traefik dynamic_config.yml for captcha keys.
+ // 1. Scan the configured Traefik dynamic config path for captcha keys.
traefikValues := detectCaptchaInTraefikConfig(dockerClient, cfg)
if len(traefikValues) > 0 {
sources["traefik_dynamic_config"] = true
@@ -106,21 +107,21 @@ func DetectCaptchaConfig(dockerClient *docker.Client, db *database.Database, cfg
}
}
-// detectCaptchaInTraefikConfig reads Traefik dynamic config and extracts captcha-related values.
-// It first attempts to read from the running container; on failure it falls back to the local file.
+// detectCaptchaInTraefikConfig reads the configured Traefik dynamic config path and extracts captcha-related values.
+// It first attempts to read from the running container; on failure it falls back to the local filesystem.
func detectCaptchaInTraefikConfig(dockerClient *docker.Client, cfg *config.Config) map[string]interface{} {
result := map[string]interface{}{}
- output, err := dockerClient.ExecCommand(cfg.TraefikContainerName, []string{"cat", cfg.TraefikDynamicConfig})
+ readResult, err := traefikconfig.ReadContainer(dockerClient, cfg.TraefikContainerName, cfg.TraefikDynamicConfig)
if err != nil {
- localPath := filepath.Join(cfg.ConfigDir, "traefik", "dynamic_config.yml")
- data, readErr := os.ReadFile(localPath)
+ hostResult, readErr := traefikconfig.ReadHost(cfg, cfg.TraefikDynamicConfig)
if readErr != nil {
logger.Debug("Could not read Traefik dynamic config", "containerErr", err, "localErr", readErr)
return result
}
- output = string(data)
+ readResult = hostResult
}
+ output := readResult.Content
lower := strings.ToLower(output)
if !strings.Contains(lower, "captchaprovider") && !strings.Contains(lower, "captchasitekey") {
diff --git a/internal/api/handlers/captcha_profiles.go b/internal/api/handlers/captcha_profiles.go
index a6d7fa1..f30c8be 100644
--- a/internal/api/handlers/captcha_profiles.go
+++ b/internal/api/handlers/captcha_profiles.go
@@ -7,6 +7,7 @@ import (
"crowdsec-manager/internal/config"
"crowdsec-manager/internal/docker"
"crowdsec-manager/internal/logger"
+ "crowdsec-manager/internal/traefikconfig"
"gopkg.in/yaml.v3"
)
@@ -192,13 +193,12 @@ func verifyCaptchaSetup(dockerClient *docker.Client, cfg *config.Config) bool {
logger.Info("Captcha HTML file verified", "path", cfg.TraefikCaptchaHTMLPath)
// Check 2: Dynamic config contains captcha settings
- configContent, err := dockerClient.ExecCommand(cfg.TraefikContainerName, []string{
- "cat", cfg.TraefikDynamicConfig,
- })
+ readResult, err := traefikconfig.ReadContainer(dockerClient, cfg.TraefikContainerName, cfg.TraefikDynamicConfig)
if err != nil {
logger.Warn("Failed to read dynamic config for verification", "error", err)
return false
}
+ configContent := readResult.Content
if !strings.Contains(strings.ToLower(configContent), "captcha") {
logger.Warn("Captcha not found in dynamic config")
diff --git a/internal/api/handlers/captcha_setup.go b/internal/api/handlers/captcha_setup.go
index e9a744c..e74e771 100644
--- a/internal/api/handlers/captcha_setup.go
+++ b/internal/api/handlers/captcha_setup.go
@@ -8,9 +8,9 @@ import (
"strings"
"crowdsec-manager/internal/config"
- "crowdsec-manager/internal/docker"
"crowdsec-manager/internal/logger"
"crowdsec-manager/internal/models"
+ "crowdsec-manager/internal/traefikconfig"
"gopkg.in/yaml.v3"
)
@@ -78,7 +78,7 @@ const captchaHTMLTemplate = `