fix: refactor bouncer key path handling and acquisition config retrieval

This commit is contained in:
GitHub Actions
2026-02-17 05:12:20 +00:00
parent 5d5d1b474a
commit 2a355d1c8c
@@ -84,6 +84,71 @@ const (
bouncerName = "caddy-bouncer"
)
func (h *CrowdsecHandler) bouncerKeyPath() string {
if h != nil && strings.TrimSpace(h.DataDir) != "" {
return filepath.Join(h.DataDir, "bouncer_key")
}
if path := strings.TrimSpace(os.Getenv("CHARON_CROWDSEC_BOUNCER_KEY_PATH")); path != "" {
return path
}
return bouncerKeyFile
}
func getAcquisitionConfigPath() string {
if path := strings.TrimSpace(os.Getenv("CHARON_CROWDSEC_ACQUIS_PATH")); path != "" {
return path
}
return "/etc/crowdsec/acquis.yaml"
}
func resolveAcquisitionConfigPath() (string, error) {
rawPath := strings.TrimSpace(getAcquisitionConfigPath())
if rawPath == "" {
return "", errors.New("acquisition config path is empty")
}
if strings.Contains(rawPath, "\x00") {
return "", errors.New("acquisition config path contains null byte")
}
if !filepath.IsAbs(rawPath) {
return "", errors.New("acquisition config path must be absolute")
}
for _, segment := range strings.Split(filepath.ToSlash(rawPath), "/") {
if segment == ".." {
return "", errors.New("acquisition config path must not contain traversal segments")
}
}
return filepath.Clean(rawPath), nil
}
func readAcquisitionConfig(absPath string) ([]byte, error) {
cleanPath := filepath.Clean(absPath)
dirPath := filepath.Dir(cleanPath)
fileName := filepath.Base(cleanPath)
if fileName == "." || fileName == string(filepath.Separator) {
return nil, errors.New("acquisition config filename is invalid")
}
file, err := os.DirFS(dirPath).Open(fileName)
if err != nil {
return nil, fmt.Errorf("open acquisition config: %w", err)
}
defer func() {
_ = file.Close()
}()
content, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("read acquisition config: %w", err)
}
return content, nil
}
// ConfigArchiveValidator validates CrowdSec configuration archives.
type ConfigArchiveValidator struct {
MaxSize int64 // Maximum compressed size (50MB default)
@@ -1717,10 +1782,11 @@ func (h *CrowdsecHandler) testKeyAgainstLAPI(ctx context.Context, apiKey string)
func (h *CrowdsecHandler) GetKeyStatus(c *gin.Context) {
h.registrationMutex.Lock()
defer h.registrationMutex.Unlock()
keyPath := h.bouncerKeyPath()
response := KeyStatusResponse{
BouncerName: bouncerName,
KeyFilePath: bouncerKeyFile,
KeyFilePath: keyPath,
}
// Check for rejected env key first
@@ -1733,7 +1799,7 @@ func (h *CrowdsecHandler) GetKeyStatus(c *gin.Context) {
// Determine current key source and status
envKey := getBouncerAPIKeyFromEnv()
fileKey := readKeyFromFile(bouncerKeyFile)
fileKey := readKeyFromFile(keyPath)
switch {
case envKey != "" && !h.envKeyRejected:
@@ -1773,6 +1839,7 @@ func (h *CrowdsecHandler) GetKeyStatus(c *gin.Context) {
func (h *CrowdsecHandler) ensureBouncerRegistration(ctx context.Context) (string, error) {
h.registrationMutex.Lock()
defer h.registrationMutex.Unlock()
keyPath := h.bouncerKeyPath()
// Priority 1: Check environment variables
envKey := getBouncerAPIKeyFromEnv()
@@ -1796,14 +1863,14 @@ func (h *CrowdsecHandler) ensureBouncerRegistration(ctx context.Context) (string
}
// Priority 2: Check persistent key file
fileKey := readKeyFromFile(bouncerKeyFile)
fileKey := readKeyFromFile(keyPath)
if fileKey != "" {
// Test key against LAPI (not just bouncer name)
if h.testKeyAgainstLAPI(ctx, fileKey) {
logger.Log().WithField("source", "file").WithField("file", bouncerKeyFile).WithField("masked_key", maskAPIKey(fileKey)).Info("CrowdSec bouncer authentication successful")
logger.Log().WithField("source", "file").WithField("file", keyPath).WithField("masked_key", maskAPIKey(fileKey)).Info("CrowdSec bouncer authentication successful")
return "", nil // Key valid
}
logger.Log().WithField("file", bouncerKeyFile).WithField("masked_key", maskAPIKey(fileKey)).Warn("File-stored API key failed LAPI authentication, will re-register")
logger.Log().WithField("file", keyPath).WithField("masked_key", maskAPIKey(fileKey)).Warn("File-stored API key failed LAPI authentication, will re-register")
}
// No valid key found - register new bouncer
@@ -1859,6 +1926,8 @@ func (h *CrowdsecHandler) validateBouncerKey(ctx context.Context) bool {
// registerAndSaveBouncer registers a new bouncer and saves the key to file.
func (h *CrowdsecHandler) registerAndSaveBouncer(ctx context.Context) (string, error) {
keyPath := h.bouncerKeyPath()
// Delete existing bouncer if present (stale registration)
deleteCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
_, _ = h.CmdExec.Execute(deleteCtx, "cscli", "bouncers", "delete", bouncerName)
@@ -1879,7 +1948,7 @@ func (h *CrowdsecHandler) registerAndSaveBouncer(ctx context.Context) (string, e
}
// Save key to persistent file
if err := saveKeyToFile(bouncerKeyFile, apiKey); err != nil {
if err := saveKeyToFile(keyPath, apiKey); err != nil {
logger.Log().WithError(err).Warn("Failed to save bouncer key to file")
// Continue - key is still valid for this session
}
@@ -1921,6 +1990,8 @@ func validateAPIKeyFormat(key string) bool {
// logBouncerKeyBanner logs the bouncer key with a formatted banner.
// Security: API key is masked to prevent exposure in logs (CWE-312).
func (h *CrowdsecHandler) logBouncerKeyBanner(apiKey string) {
keyPath := h.bouncerKeyPath()
banner := `
════════════════════════════════════════════════════════════════════
🔐 CrowdSec Bouncer Registered Successfully
@@ -1936,7 +2007,7 @@ Saved To: %s
════════════════════════════════════════════════════════════════════`
// Security: Mask API key to prevent cleartext exposure in logs
maskedKey := maskAPIKey(apiKey)
logger.Log().Infof(banner, bouncerName, maskedKey, bouncerKeyFile)
logger.Log().Infof(banner, bouncerName, maskedKey, keyPath)
}
// getBouncerAPIKeyFromEnv retrieves the bouncer API key from environment variables.
@@ -1999,15 +2070,16 @@ func saveKeyToFile(path string, key string) error {
// GET /api/v1/admin/crowdsec/bouncer
func (h *CrowdsecHandler) GetBouncerInfo(c *gin.Context) {
ctx := c.Request.Context()
keyPath := h.bouncerKeyPath()
info := BouncerInfo{
Name: bouncerName,
FilePath: bouncerKeyFile,
FilePath: keyPath,
}
// Determine key source
envKey := getBouncerAPIKeyFromEnv()
fileKey := readKeyFromFile(bouncerKeyFile)
fileKey := readKeyFromFile(keyPath)
var fullKey string
switch {
@@ -2037,13 +2109,15 @@ func (h *CrowdsecHandler) GetBouncerInfo(c *gin.Context) {
// GetBouncerKey returns the full bouncer key (for copy to clipboard).
// GET /api/v1/admin/crowdsec/bouncer/key
func (h *CrowdsecHandler) GetBouncerKey(c *gin.Context) {
keyPath := h.bouncerKeyPath()
envKey := getBouncerAPIKeyFromEnv()
if envKey != "" {
c.JSON(http.StatusOK, gin.H{"key": envKey, "source": "env_var"})
return
}
fileKey := readKeyFromFile(bouncerKeyFile)
fileKey := readKeyFromFile(keyPath)
if fileKey != "" {
c.JSON(http.StatusOK, gin.H{"key": fileKey, "source": "file"})
return
@@ -2298,11 +2372,16 @@ func (h *CrowdsecHandler) RegisterBouncer(c *gin.Context) {
// GetAcquisitionConfig returns the current CrowdSec acquisition configuration.
// GET /api/v1/admin/crowdsec/acquisition
func (h *CrowdsecHandler) GetAcquisitionConfig(c *gin.Context) {
acquisPath := "/etc/crowdsec/acquis.yaml"
content, err := os.ReadFile(acquisPath)
acquisPath, err := resolveAcquisitionConfigPath()
if err != nil {
if os.IsNotExist(err) {
logger.Log().WithError(err).Warn("Invalid acquisition config path")
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid acquisition config path"})
return
}
content, err := readAcquisitionConfig(acquisPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
c.JSON(http.StatusNotFound, gin.H{"error": "acquisition config not found", "path": acquisPath})
return
}
@@ -2328,7 +2407,12 @@ func (h *CrowdsecHandler) UpdateAcquisitionConfig(c *gin.Context) {
return
}
acquisPath := "/etc/crowdsec/acquis.yaml"
acquisPath, err := resolveAcquisitionConfigPath()
if err != nil {
logger.Log().WithError(err).Warn("Invalid acquisition config path")
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid acquisition config path"})
return
}
// Create backup of existing config if it exists
var backupPath string