fix(crowdsec): resolve LAPI "access forbidden" authentication failures
Replace name-based bouncer validation with actual LAPI authentication testing. The previous implementation checked if a bouncer NAME existed but never validated if the API KEY was accepted by CrowdSec LAPI. Key changes: - Add testKeyAgainstLAPI() with real HTTP authentication against /v1/decisions/stream endpoint - Implement exponential backoff retry (500ms → 5s cap) for transient connection errors while failing fast on 403 authentication failures - Add mutex protection to prevent concurrent registration race conditions - Use atomic file writes (temp → rename) for key persistence - Mask API keys in all log output (CWE-312 compliance) Breaking behavior: Invalid env var keys now auto-recover by registering a new bouncer instead of failing silently with stale credentials. Includes temporary acceptance of 7 Debian HIGH CVEs with documented mitigation plan (Alpine migration in progress - issue #631).
This commit is contained in:
784
docs/issues/crowdsec_auth_regression.md
Normal file
784
docs/issues/crowdsec_auth_regression.md
Normal file
@@ -0,0 +1,784 @@
|
||||
# CrowdSec Authentication Regression - Bug Investigation Report
|
||||
|
||||
**Status**: Investigation Complete - Ready for Fix Implementation
|
||||
**Priority**: P0 (Critical Production Bug)
|
||||
**Created**: 2026-02-04
|
||||
**Reporter**: User via Production Environment
|
||||
**Affected Version**: Post Auto-Registration Feature
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The CrowdSec integration suffers from **three distinct but related bugs** introduced by the auto-registration feature implementation. While the feature was designed to eliminate manual key management, it contains a critical flaw in key validation logic that causes "access forbidden" errors when users provide environment variable keys. Additionally, there are two UI bugs affecting the bouncer key display component.
|
||||
|
||||
**Impact**:
|
||||
- **High**: Users with `CHARON_SECURITY_CROWDSEC_API_KEY` set experience continuous LAPI connection failures
|
||||
- **Medium**: Confusing UI showing translation codes instead of human-readable text
|
||||
- **Low**: Bouncer key card appearing on wrong page in the interface
|
||||
|
||||
---
|
||||
|
||||
## Bug #1: Flawed Key Validation Logic (CRITICAL)
|
||||
|
||||
### The Core Issue
|
||||
|
||||
The `ensureBouncerRegistration()` method contains a **logical fallacy** in its validation approach:
|
||||
|
||||
```go
|
||||
// From: backend/internal/api/handlers/crowdsec_handler.go:1545-1570
|
||||
|
||||
func (h *CrowdsecHandler) ensureBouncerRegistration(ctx context.Context) (string, error) {
|
||||
// Priority 1: Check environment variables
|
||||
envKey := getBouncerAPIKeyFromEnv()
|
||||
if envKey != "" {
|
||||
if h.validateBouncerKey(ctx) { // ❌ BUG: Validates BOUNCER NAME, not KEY VALUE
|
||||
logger.Log().Info("Using CrowdSec API key from environment variable")
|
||||
return "", nil // Key valid, nothing new to report
|
||||
}
|
||||
logger.Log().Warn("Env-provided CrowdSec API key is invalid or bouncer not registered, will re-register")
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### What `validateBouncerKey()` Actually Does
|
||||
|
||||
```go
|
||||
// From: backend/internal/api/handlers/crowdsec_handler.go:1573-1598
|
||||
|
||||
func (h *CrowdsecHandler) validateBouncerKey(ctx context.Context) bool {
|
||||
// ...
|
||||
output, err := h.CmdExec.Execute(checkCtx, "cscli", "bouncers", "list", "-o", "json")
|
||||
// ...
|
||||
|
||||
for _, b := range bouncers {
|
||||
if b.Name == bouncerName { // ❌ Checks if NAME exists, not if API KEY is correct
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
### The Failure Scenario
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Bug #1: Authentication Flow Analysis │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Step 1: User sets docker-compose.yml │
|
||||
│ CHARON_SECURITY_CROWDSEC_API_KEY=myinventedkey123 │
|
||||
│ │
|
||||
│ Step 2: CrowdSec starts, bouncer gets registered │
|
||||
│ Result: Bouncer "caddy-bouncer" exists with valid key "xyz789abc..." │
|
||||
│ │
|
||||
│ Step 3: User enables CrowdSec via GUI │
|
||||
│ → ensureBouncerRegistration() is called │
|
||||
│ → envKey = "myinventedkey123" (from env var) │
|
||||
│ → validateBouncerKey() is called │
|
||||
│ → Checks: Does bouncer named "caddy-bouncer" exist? │
|
||||
│ → Returns: TRUE (bouncer exists, regardless of key value) │
|
||||
│ → Conclusion: "Key is valid" ✓ (WRONG!) │
|
||||
│ → Returns empty string (no new key to report) │
|
||||
│ │
|
||||
│ Step 4: Caddy config is generated │
|
||||
│ → getCrowdSecAPIKey() returns "myinventedkey123" │
|
||||
│ → CrowdSecApp { APIKey: "myinventedkey123", APIUrl: "http://127.0.0.1:8085" } │
|
||||
│ │
|
||||
│ Step 5: Caddy bouncer attempts LAPI connection │
|
||||
│ → Sends HTTP request with header: X-Api-Key: myinventedkey123 │
|
||||
│ → LAPI checks if "myinventedkey123" is registered │
|
||||
│ → LAPI responds: 403 Forbidden ("access forbidden") │
|
||||
│ → Caddy logs error and retries every 10s indefinitely │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Root Cause Explained
|
||||
|
||||
**What Was Intended**:
|
||||
- Check if the bouncer exists in CrowdSec's registry
|
||||
- If it doesn't exist, register a new one
|
||||
- If it does exist, use the key from the environment or file
|
||||
|
||||
**What Actually Happens**:
|
||||
- Check if a bouncer with name "caddy-bouncer" exists
|
||||
- If it exists, **assume the env var key is valid** (incorrect assumption)
|
||||
- Never validate that the env var key **matches** the registered bouncer's key
|
||||
- Never test the key against LAPI before committing to it
|
||||
|
||||
### Why This Broke Working Connections
|
||||
|
||||
**Before the Auto-Registration Feature**:
|
||||
- If user set an invalid key, CrowdSec wouldn't start
|
||||
- Error was obvious and immediate
|
||||
- No ambiguous state
|
||||
|
||||
**After the Auto-Registration Feature**:
|
||||
- System auto-registers a valid bouncer on startup
|
||||
- User's invalid env var key is "validated" by checking bouncer name existence
|
||||
- Invalid key gets used because validation passed
|
||||
- Connection fails with cryptic "access forbidden" error
|
||||
- User sees bouncer as "registered" in UI but connection still fails
|
||||
|
||||
---
|
||||
|
||||
## Bug #2: UI Translation Codes Displayed (MEDIUM)
|
||||
|
||||
### The Symptom
|
||||
|
||||
Users report seeing:
|
||||
```
|
||||
security.crowdsec.bouncerApiKey
|
||||
```
|
||||
|
||||
Instead of:
|
||||
```
|
||||
Bouncer API Key
|
||||
```
|
||||
|
||||
### Investigation Findings
|
||||
|
||||
**Translation Key Exists**:
|
||||
```json
|
||||
// frontend/src/locales/en/translation.json:272
|
||||
{
|
||||
"security": {
|
||||
"crowdsec": {
|
||||
"bouncerApiKey": "Bouncer API Key",
|
||||
"keyCopied": "API key copied to clipboard",
|
||||
"copyFailed": "Failed to copy API key",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Component Uses Translation Correctly**:
|
||||
```tsx
|
||||
// frontend/src/components/CrowdSecBouncerKeyDisplay.tsx:72-75
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Key className="h-4 w-4" />
|
||||
{t('security.crowdsec.bouncerApiKey')}
|
||||
</CardTitle>
|
||||
```
|
||||
|
||||
### Possible Causes
|
||||
|
||||
1. **Translation Context Not Loaded**: The `useTranslation()` hook might not have access to the full translation namespace when the component renders
|
||||
2. **Import Order Issue**: Translation provider might be initialized after component mount
|
||||
3. **Build Cache**: Stale build artifacts from webpack/vite cache
|
||||
|
||||
### Evidence Supporting Cache Theory
|
||||
|
||||
From test files:
|
||||
```typescript
|
||||
// frontend/src/components/__tests__/CrowdSecBouncerKeyDisplay.test.tsx:33
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'security.crowdsec.bouncerApiKey': 'Bouncer API Key',
|
||||
// Mock translations work correctly in tests
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tests pass with mocked translations, suggesting the issue is runtime-specific, not code-level.
|
||||
|
||||
---
|
||||
|
||||
## Bug #3: Component Rendered on Wrong Page (LOW)
|
||||
|
||||
### The Symptom
|
||||
|
||||
The `CrowdSecBouncerKeyDisplay` component appears on the **Security Dashboard** page instead of (or in addition to) the **CrowdSec Config** page.
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
```
|
||||
Security Dashboard (/security)
|
||||
├─ Cerberus Status Card
|
||||
├─ Admin Whitelist Card
|
||||
├─ Security Layer Cards (CrowdSec, ACL, WAF, Rate Limit)
|
||||
└─ [NO BOUNCER KEY CARD]
|
||||
|
||||
CrowdSec Config Page (/security/crowdsec)
|
||||
├─ CrowdSec Status & Controls
|
||||
├─ Console Enrollment Card
|
||||
├─ Hub Management
|
||||
├─ Decisions List
|
||||
└─ [BOUNCER KEY CARD HERE] ✅
|
||||
```
|
||||
|
||||
### Current (Buggy) Behavior
|
||||
|
||||
The component appears on the Security Dashboard page.
|
||||
|
||||
### Code Evidence
|
||||
|
||||
**Correct Import Location**:
|
||||
```tsx
|
||||
// frontend/src/pages/CrowdSecConfig.tsx:16
|
||||
import { CrowdSecBouncerKeyDisplay } from '../components/CrowdSecBouncerKeyDisplay'
|
||||
|
||||
// frontend/src/pages/CrowdSecConfig.tsx:543-545
|
||||
{/* CrowdSec Bouncer API Key - moved from Security Dashboard */}
|
||||
{status.cerberus?.enabled && status.crowdsec.enabled && (
|
||||
<CrowdSecBouncerKeyDisplay />
|
||||
)}
|
||||
```
|
||||
|
||||
**Migration Evidence**:
|
||||
```typescript
|
||||
// frontend/src/pages/__tests__/Security.functional.test.tsx:102
|
||||
// NOTE: CrowdSecBouncerKeyDisplay mock removed (moved to CrowdSecConfig page)
|
||||
|
||||
// frontend/src/pages/__tests__/Security.functional.test.tsx:404-405
|
||||
// NOTE: CrowdSec Bouncer Key Display moved to CrowdSecConfig page (Sprint 3)
|
||||
// Tests for bouncer key display are now in CrowdSecConfig tests
|
||||
```
|
||||
|
||||
### Hypothesis
|
||||
|
||||
**Most Likely**: The component is **still imported** in `Security.tsx` despite the migration comments. The test mock was removed but the actual component import wasn't.
|
||||
|
||||
**File to Check**:
|
||||
```tsx
|
||||
// frontend/src/pages/Security.tsx
|
||||
// Search for: CrowdSecBouncerKeyDisplay import or usage
|
||||
```
|
||||
|
||||
The Security.tsx file is 618 lines long, and the migration might not have been completed.
|
||||
|
||||
---
|
||||
|
||||
## How CrowdSec Bouncer Keys Actually Work
|
||||
|
||||
Understanding the authentication mechanism is critical to fixing Bug #1.
|
||||
|
||||
### CrowdSec Bouncer Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ CrowdSec Bouncer Flow │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Component 1: CrowdSec Agent (LAPI Server) │
|
||||
│ • Runs on port 8085 (Charon default) │
|
||||
│ • Maintains SQLite database of registered bouncers │
|
||||
│ • Database: /var/lib/crowdsec/data/crowdsec.db │
|
||||
│ • Table: bouncers (columns: name, api_key, ip_address, ...) │
|
||||
│ • Authenticates API requests via X-Api-Key header │
|
||||
│ │
|
||||
│ Component 2: Bouncer Client (Caddy Plugin) │
|
||||
│ • Embedded in Caddy via github.com/hslatman/caddy-crowdsec-bouncer │
|
||||
│ • Makes HTTP requests to LAPI (GET /v1/decisions/stream) │
|
||||
│ • Includes X-Api-Key header in every request │
|
||||
│ • Key must match a registered bouncer in LAPI database │
|
||||
│ │
|
||||
│ Component 3: Registration (cscli) │
|
||||
│ • Command: cscli bouncers add <name> │
|
||||
│ • Generates random API key (e.g., "a1b2c3d4e5f6...") │
|
||||
│ • Stores key in database (hashed? TBD) │
|
||||
│ • Returns plaintext key to caller (one-time show) │
|
||||
│ • Key must be provided to bouncer client for authentication │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```
|
||||
1. Bouncer Registration:
|
||||
$ cscli bouncers add caddy-bouncer
|
||||
→ Generates: "abc123xyz789def456ghi789"
|
||||
→ Stores hash in: /var/lib/crowdsec/data/crowdsec.db (bouncers table)
|
||||
→ Returns plaintext: "abc123xyz789def456ghi789"
|
||||
|
||||
2. Bouncer Configuration:
|
||||
Caddy config:
|
||||
{
|
||||
"apps": {
|
||||
"crowdsec": {
|
||||
"api_key": "abc123xyz789def456ghi789",
|
||||
"api_url": "http://127.0.0.1:8085"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3. Bouncer Authentication Request:
|
||||
GET /v1/decisions/stream HTTP/1.1
|
||||
Host: 127.0.0.1:8085
|
||||
X-Api-Key: abc123xyz789def456ghi789
|
||||
|
||||
4. LAPI Validation:
|
||||
• Extract X-Api-Key header
|
||||
• Hash the key value
|
||||
• Compare hash against bouncers table
|
||||
• If match: return decisions (200 OK)
|
||||
• If no match: return 403 Forbidden
|
||||
```
|
||||
|
||||
### Why Keys Cannot Be "Invented"
|
||||
|
||||
**User Misconception**:
|
||||
> "I'll just set `CHARON_SECURITY_CROWDSEC_API_KEY=mySecurePassword123` in docker-compose.yml"
|
||||
|
||||
**Reality**:
|
||||
- The API key is **not a password you choose**
|
||||
- It's a **randomly generated token** by CrowdSec
|
||||
- Only keys generated via `cscli bouncers add` are stored in the database
|
||||
- LAPI has no record of "mySecurePassword123" → rejects it
|
||||
|
||||
**Analogy**:
|
||||
Setting an invented API key is like showing a fake ID at a checkpoint. The guard doesn't care if the ID looks official—they check their list. If you're not on the list, you're denied.
|
||||
|
||||
### Do Keys Need Hashing?
|
||||
|
||||
**For Storage**: Yes, likely hashed in the database (CWE-312 mitigation)
|
||||
|
||||
**For Transmission**: **No**, must be plaintext in the `X-Api-Key` header
|
||||
|
||||
**For Display in UI**: **Partial masking** is recommended (first 4 + last 3 chars)
|
||||
|
||||
```go
|
||||
// backend/internal/api/handlers/crowdsec_handler.go:1757-1763
|
||||
if fullKey != "" && len(fullKey) > 7 {
|
||||
info.KeyPreview = fullKey[:4] + "..." + fullKey[len(fullKey)-3:]
|
||||
} else if fullKey != "" {
|
||||
info.KeyPreview = "***"
|
||||
}
|
||||
```
|
||||
|
||||
**Security Note**: The full key must be retrievable for the "Copy to Clipboard" feature, so it's stored in plaintext in the file `/app/data/crowdsec/bouncer_key` with `chmod 600` permissions.
|
||||
|
||||
---
|
||||
|
||||
## File Locations & Architecture
|
||||
|
||||
### Backend Files
|
||||
|
||||
| File | Purpose | Lines of Interest |
|
||||
|------|---------|-------------------|
|
||||
| `backend/internal/api/handlers/crowdsec_handler.go` | Main CrowdSec handler | Lines 482, 1543-1625 (buggy validation) |
|
||||
| `backend/internal/caddy/config.go` | Caddy config generation | Lines 65, 1129-1160 (key retrieval) |
|
||||
| `backend/internal/crowdsec/registration.go` | Bouncer registration utilities | Lines 96-122, 257-336 (helper functions) |
|
||||
| `.docker/docker-entrypoint.sh` | Container startup script | Lines 223-252 (CrowdSec initialization) |
|
||||
| `configs/crowdsec/register_bouncer.sh` | Bouncer registration script | Lines 1-43 (manual registration) |
|
||||
|
||||
### Frontend Files
|
||||
|
||||
| File | Purpose | Lines of Interest |
|
||||
|------|---------|-------------------|
|
||||
| `frontend/src/components/CrowdSecBouncerKeyDisplay.tsx` | Key display component | Lines 35-148 (entire component) |
|
||||
| `frontend/src/pages/CrowdSecConfig.tsx` | CrowdSec config page | Lines 16, 543-545 (component usage) |
|
||||
| `frontend/src/pages/Security.tsx` | Security dashboard | Lines 1-618 (check for stale imports) |
|
||||
| `frontend/src/locales/en/translation.json` | English translations | Lines 272-278 (translation keys) |
|
||||
|
||||
### Key Storage Locations
|
||||
|
||||
| Path | Description | Permissions | Persists? |
|
||||
|------|-------------|-------------|-----------|
|
||||
| `/app/data/crowdsec/bouncer_key` | Primary key storage (NEW) | 600 | ✅ Yes (Docker volume) |
|
||||
| `/etc/crowdsec/bouncers/caddy-bouncer.key` | Legacy location | 600 | ❌ No (ephemeral) |
|
||||
| `CHARON_SECURITY_CROWDSEC_API_KEY` env var | User override | N/A | ✅ Yes (compose file) |
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Fix Plan
|
||||
|
||||
### Fix #1: Correct Key Validation Logic (P0 - CRITICAL)
|
||||
|
||||
**File**: `backend/internal/api/handlers/crowdsec_handler.go`
|
||||
|
||||
**Current Code** (Lines 1545-1570):
|
||||
```go
|
||||
func (h *CrowdsecHandler) ensureBouncerRegistration(ctx context.Context) (string, error) {
|
||||
envKey := getBouncerAPIKeyFromEnv()
|
||||
if envKey != "" {
|
||||
if h.validateBouncerKey(ctx) { // ❌ Validates name, not key value
|
||||
logger.Log().Info("Using CrowdSec API key from environment variable")
|
||||
return "", nil
|
||||
}
|
||||
logger.Log().Warn("Env-provided CrowdSec API key is invalid or bouncer not registered, will re-register")
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Proposed Fix**:
|
||||
```go
|
||||
func (h *CrowdsecHandler) ensureBouncerRegistration(ctx context.Context) (string, error) {
|
||||
envKey := getBouncerAPIKeyFromEnv()
|
||||
if envKey != "" {
|
||||
// TEST KEY AGAINST LAPI, NOT JUST BOUNCER NAME
|
||||
if h.testKeyAgainstLAPI(ctx, envKey) {
|
||||
logger.Log().Info("Using CrowdSec API key from environment variable (verified)")
|
||||
return "", nil
|
||||
}
|
||||
logger.Log().Warn("Env-provided CrowdSec API key failed LAPI authentication, will re-register")
|
||||
}
|
||||
|
||||
fileKey := readKeyFromFile(bouncerKeyFile)
|
||||
if fileKey != "" {
|
||||
if h.testKeyAgainstLAPI(ctx, fileKey) {
|
||||
logger.Log().WithField("file", bouncerKeyFile).Info("Using CrowdSec API key from file (verified)")
|
||||
return "", nil
|
||||
}
|
||||
logger.Log().WithField("file", bouncerKeyFile).Warn("File API key failed LAPI authentication, will re-register")
|
||||
}
|
||||
|
||||
return h.registerAndSaveBouncer(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
**New Method to Add**:
|
||||
```go
|
||||
// testKeyAgainstLAPI validates an API key by making an authenticated request to LAPI.
|
||||
// Returns true if the key is accepted (200 OK), false otherwise.
|
||||
func (h *CrowdsecHandler) testKeyAgainstLAPI(ctx context.Context, apiKey string) bool {
|
||||
if apiKey == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get LAPI URL
|
||||
lapiURL := "http://127.0.0.1:8085"
|
||||
if h.Security != nil {
|
||||
cfg, err := h.Security.Get()
|
||||
if err == nil && cfg != nil && cfg.CrowdSecAPIURL != "" {
|
||||
lapiURL = cfg.CrowdSecAPIURL
|
||||
}
|
||||
}
|
||||
|
||||
// Construct heartbeat endpoint URL
|
||||
endpoint := fmt.Sprintf("%s/v1/heartbeat", strings.TrimRight(lapiURL, "/"))
|
||||
|
||||
// Create request with timeout
|
||||
testCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(testCtx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
logger.Log().WithError(err).Debug("Failed to create LAPI test request")
|
||||
return false
|
||||
}
|
||||
|
||||
// Set API key header
|
||||
req.Header.Set("X-Api-Key", apiKey)
|
||||
|
||||
// Execute request
|
||||
client := network.NewInternalServiceHTTPClient(5 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Log().WithError(err).Debug("Failed to connect to LAPI for key validation")
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
logger.Log().Debug("API key validated successfully against LAPI")
|
||||
return true
|
||||
}
|
||||
|
||||
logger.Log().WithField("status", resp.StatusCode).Debug("API key rejected by LAPI")
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
- Tests the key against the **actual LAPI endpoint** (`/v1/heartbeat`)
|
||||
- Uses the same authentication header (`X-Api-Key`) that Caddy bouncer will use
|
||||
- Returns true only if LAPI accepts the key (200 OK)
|
||||
- Fails safely if LAPI is unreachable (returns false, triggers re-registration)
|
||||
|
||||
### Fix #2: Remove Stale Component Import from Security Dashboard (P2)
|
||||
|
||||
**File**: `frontend/src/pages/Security.tsx`
|
||||
|
||||
**Task**:
|
||||
1. Search for any remaining import of `CrowdSecBouncerKeyDisplay`
|
||||
2. Search for any JSX usage of `<CrowdSecBouncerKeyDisplay />`
|
||||
3. Remove both if found
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
# Search for imports
|
||||
grep -n "CrowdSecBouncerKeyDisplay" frontend/src/pages/Security.tsx
|
||||
|
||||
# Search for JSX usage
|
||||
grep -n "<CrowdSecBouncerKeyDisplay" frontend/src/pages/Security.tsx
|
||||
```
|
||||
|
||||
**Expected Result**: No matches found (component fully migrated to CrowdSecConfig.tsx)
|
||||
|
||||
### Fix #3: Resolve Translation Display Issue (P2)
|
||||
|
||||
**Option A: Clear Build Cache** (Try First)
|
||||
```bash
|
||||
cd frontend
|
||||
rm -rf node_modules/.vite
|
||||
rm -rf dist
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Option B: Verify i18n Provider Wraps Component** (If Cache Clear Fails)
|
||||
|
||||
Check that `CrowdSecBouncerKeyDisplay` is used within the i18n context:
|
||||
|
||||
```tsx
|
||||
// Verify in: frontend/src/App.tsx or root component
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import i18n from './i18n'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{/* All components here have translation access */}
|
||||
<RouterProvider router={router} />
|
||||
</I18nextProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Option C: Dynamic Import with Suspense** (If Issue Persists)
|
||||
|
||||
Wrap the component in a Suspense boundary to ensure translations load:
|
||||
|
||||
```tsx
|
||||
// frontend/src/pages/CrowdSecConfig.tsx
|
||||
import { Suspense } from 'react'
|
||||
|
||||
{status.cerberus?.enabled && status.crowdsec.enabled && (
|
||||
<Suspense fallback={<Skeleton className="h-32 w-full" />}>
|
||||
<CrowdSecBouncerKeyDisplay />
|
||||
</Suspense>
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Test Case 1: Env Var with Invalid Key (Primary Bug)
|
||||
|
||||
**Setup**:
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
environment:
|
||||
- CHARON_SECURITY_CROWDSEC_API_KEY=thisisinvalid
|
||||
```
|
||||
|
||||
**Expected Before Fix**:
|
||||
- ❌ System validates bouncer name, uses invalid key
|
||||
- ❌ LAPI returns 403 Forbidden continuously
|
||||
- ❌ Logs show "Using CrowdSec API key from environment variable"
|
||||
|
||||
**Expected After Fix**:
|
||||
- ✅ System tests key against LAPI, validation fails
|
||||
- ✅ System auto-generates new valid key
|
||||
- ✅ Logs show "Env-provided CrowdSec API key failed LAPI authentication, will re-register"
|
||||
- ✅ LAPI connection succeeds with new key
|
||||
|
||||
### Test Case 2: Env Var with Valid Key
|
||||
|
||||
**Setup**:
|
||||
```bash
|
||||
# Generate a real key first
|
||||
docker exec charon cscli bouncers add test-bouncer
|
||||
|
||||
# Copy key to docker-compose.yml
|
||||
environment:
|
||||
- CHARON_SECURITY_CROWDSEC_API_KEY=<generated-key>
|
||||
```
|
||||
|
||||
**Expected After Fix**:
|
||||
- ✅ System tests key against LAPI, validation succeeds
|
||||
- ✅ System uses provided key (no new key generated)
|
||||
- ✅ Logs show "Using CrowdSec API key from environment variable (verified)"
|
||||
- ✅ LAPI connection succeeds
|
||||
|
||||
### Test Case 3: No Env Var, File Key Exists
|
||||
|
||||
**Setup**:
|
||||
```bash
|
||||
# docker-compose.yml has no CHARON_SECURITY_CROWDSEC_API_KEY
|
||||
|
||||
# File exists from previous run
|
||||
cat /app/data/crowdsec/bouncer_key
|
||||
# Outputs: abc123xyz789...
|
||||
```
|
||||
|
||||
**Expected After Fix**:
|
||||
- ✅ System reads key from file
|
||||
- ✅ System tests key against LAPI, validation succeeds
|
||||
- ✅ System uses file key
|
||||
- ✅ Logs show "Using CrowdSec API key from file (verified)"
|
||||
|
||||
### Test Case 4: No Key Anywhere (Fresh Install)
|
||||
|
||||
**Setup**:
|
||||
```bash
|
||||
# No env var set
|
||||
# No file exists
|
||||
# Bouncer never registered
|
||||
```
|
||||
|
||||
**Expected After Fix**:
|
||||
- ✅ System registers new bouncer
|
||||
- ✅ System saves key to `/app/data/crowdsec/bouncer_key`
|
||||
- ✅ System logs key banner with masked preview
|
||||
- ✅ LAPI connection succeeds
|
||||
|
||||
### Test Case 5: UI Component Location
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
# Navigate to Security Dashboard
|
||||
# URL: http://localhost:8080/security
|
||||
|
||||
# Expected:
|
||||
# - CrowdSec card with toggle and "Configure" button
|
||||
# - NO bouncer key card visible
|
||||
|
||||
# Navigate to CrowdSec Config
|
||||
# URL: http://localhost:8080/security/crowdsec
|
||||
|
||||
# Expected:
|
||||
# - Bouncer key card visible (if CrowdSec enabled)
|
||||
# - Card shows: key preview, registered badge, source badge
|
||||
# - Copy button works
|
||||
```
|
||||
|
||||
### Test Case 6: UI Translation Display
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
# Navigate to CrowdSec Config
|
||||
# Enable CrowdSec if not enabled
|
||||
|
||||
# Check bouncer key card:
|
||||
# - Card title shows "Bouncer API Key" (not "security.crowdsec.bouncerApiKey")
|
||||
# - Badge shows "Registered" (not "security.crowdsec.registered")
|
||||
# - Badge shows "Environment Variable" or "File" (not raw keys)
|
||||
# - Path label shows "Key stored at:" (not "security.crowdsec.keyStoredAt")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If fixes cause regressions:
|
||||
|
||||
1. **Revert `testKeyAgainstLAPI()` Addition**:
|
||||
```bash
|
||||
git revert <commit-hash>
|
||||
```
|
||||
|
||||
2. **Emergency Workaround for Users**:
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
# Remove any CHARON_SECURITY_CROWDSEC_API_KEY line
|
||||
# Let system auto-generate key
|
||||
```
|
||||
|
||||
3. **Manual Key Registration**:
|
||||
```bash
|
||||
docker exec charon cscli bouncers add caddy-bouncer
|
||||
# Copy output to docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Long-Term Recommendations
|
||||
|
||||
### 1. Add LAPI Health Check to Startup
|
||||
|
||||
**File**: `.docker/docker-entrypoint.sh`
|
||||
|
||||
Add after machine registration:
|
||||
```bash
|
||||
# Wait for LAPI to be ready before proceeding
|
||||
echo "Waiting for CrowdSec LAPI to be ready..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s -f http://127.0.0.1:8085/v1/heartbeat > /dev/null 2>&1; then
|
||||
echo "✓ LAPI is ready"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "✗ LAPI failed to start within 30 seconds"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
|
||||
### 2. Add Bouncer Key Rotation Feature
|
||||
|
||||
**UI Button**: "Rotate Bouncer Key"
|
||||
|
||||
**Behavior**:
|
||||
1. Delete current bouncer (`cscli bouncers delete caddy-bouncer`)
|
||||
2. Register new bouncer (`cscli bouncers add caddy-bouncer`)
|
||||
3. Save new key to file
|
||||
4. Reload Caddy config
|
||||
5. Show new key in UI banner
|
||||
|
||||
### 3. Add LAPI Connection Status Indicator
|
||||
|
||||
**UI Enhancement**: Real-time status badge
|
||||
|
||||
```tsx
|
||||
<Badge variant={lapiConnected ? 'success' : 'error'}>
|
||||
{lapiConnected ? 'LAPI Connected' : 'LAPI Connection Failed'}
|
||||
</Badge>
|
||||
```
|
||||
|
||||
**Backend**: WebSocket or polling endpoint to check LAPI status every 10s
|
||||
|
||||
### 4. Documentation Updates
|
||||
|
||||
**Files to Update**:
|
||||
- `docs/guides/crowdsec-setup.md` - Add troubleshooting section for "access forbidden"
|
||||
- `README.md` - Clarify that bouncer keys are auto-generated
|
||||
- `docker-compose.yml.example` - Remove `CHARON_SECURITY_CROWDSEC_API_KEY` or add warning comment
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Related Issues & PRs
|
||||
- Original Working State: Before auto-registration feature
|
||||
- Auto-Registration Feature Plan: `docs/plans/crowdsec_bouncer_auto_registration.md`
|
||||
- LAPI Auth Fix Plan: `docs/plans/crowdsec_lapi_auth_fix.md`
|
||||
|
||||
### External Documentation
|
||||
- [CrowdSec Bouncer API Documentation](https://doc.crowdsec.net/docs/next/local_api/bouncers/)
|
||||
- [CrowdSec cscli Bouncers Commands](https://doc.crowdsec.net/docs/next/cscli/cscli_bouncers/)
|
||||
- [Caddy CrowdSec Bouncer Plugin](https://github.com/hslatman/caddy-crowdsec-bouncer)
|
||||
|
||||
### Code Comments & Markers
|
||||
- `// ❌ BUG:` markers added to problematic validation logic
|
||||
- `// TODO:` markers for future enhancements
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This bug regression stems from a **logical flaw** in the key validation implementation. The auto-registration feature was designed to eliminate user error, but ironically introduced a validation shortcut that causes the exact problem it was meant to solve.
|
||||
|
||||
**The Fix**: Replace name-based validation with actual LAPI authentication testing.
|
||||
|
||||
**Estimated Fix Time**: 2-4 hours (implementation + testing)
|
||||
|
||||
**Risk Level**: Low (new validation is strictly more correct than old)
|
||||
|
||||
**User Impact After Fix**: Immediate resolution - invalid keys rejected, valid keys used correctly, "access forbidden" errors eliminated.
|
||||
|
||||
---
|
||||
|
||||
**Investigation Status**: ✅ Complete
|
||||
**Next Step**: Implement fixes per step-by-step plan above
|
||||
**Assignee**: [Development Team]
|
||||
**Target Resolution**: [Date]
|
||||
Reference in New Issue
Block a user