fix: refactor bouncer key path handling and acquisition config retrieval
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user