# Production Bug Investigation: LAPI Auth & Translation Keys **Date**: 2025-01-20 **Status**: Investigation Complete **Priority**: High (Both bugs affect production stability and UX) --- ## Executive Summary This document details the investigation of two production bugs in Charon: 1. **Bug 1**: CrowdSec LAPI "access forbidden" error repeating every 10 seconds 2. **Bug 2**: WebUI displaying raw translation keys like `translation.security.crowdsec.title` Both issues have been traced to their root causes with proposed fix approaches. --- ## Bug 1: CrowdSec LAPI "access forbidden" Error ### Symptoms - Error in logs: `"msg":"API request failed","error":"making request: performing request: API error: access forbidden"` - Error repeats continuously every 10 seconds - CrowdSec bouncer cannot authenticate with Local API (LAPI) ### Files Investigated | File | Purpose | Key Findings | |------|---------|--------------| | [backend/internal/caddy/config.go](../../backend/internal/caddy/config.go#L1129) | Generates Caddy JSON config | `getCrowdSecAPIKey()` only reads env vars | | [backend/internal/crowdsec/registration.go](../../backend/internal/crowdsec/registration.go) | Bouncer registration utilities | Has key validation but not called at startup | | [backend/internal/api/handlers/crowdsec_handler.go](../../backend/internal/api/handlers/crowdsec_handler.go) | HTTP handlers for CrowdSec | `Start()` doesn't call bouncer registration | | [configs/crowdsec/register_bouncer.sh](../../configs/crowdsec/register_bouncer.sh) | Shell script for bouncer registration | Manual registration script exists | | [.docker/docker-entrypoint.sh](../../.docker/docker-entrypoint.sh) | Container startup | Only registers machine, NOT bouncer | ### Root Cause Analysis The root cause has been identified through code analysis: #### 1. Invalid Static API Key User configured static bouncer key in docker-compose: ```yaml environment: CHARON_SECURITY_CROWDSEC_API_KEY: "charonbouncerkey2024" ``` This key was **never registered** with CrowdSec LAPI via `cscli bouncers add`. #### 2. getCrowdSecAPIKey() Only Reads Environment **File**: [backend/internal/caddy/config.go](../../backend/internal/caddy/config.go#L1129) ```go func getCrowdSecAPIKey() string { key := os.Getenv("CHARON_SECURITY_CROWDSEC_API_KEY") if key == "" { key = os.Getenv("CROWDSEC_API_KEY") } // BUG: No fallback to /app/data/crowdsec/bouncer_key file // BUG: No validation that key is registered with LAPI return key } ``` **Problem**: Function returns env var without: - Checking if key exists in bouncer_key file - Validating key against LAPI before use - Auto-registering bouncer if key is invalid #### 3. Missing Auto-Registration at Startup **File**: [.docker/docker-entrypoint.sh](../../.docker/docker-entrypoint.sh) ```bash # Current: Only machine registration cscli machines add local-api --password "$machine_pwd" --force # Missing: Bouncer registration # cscli bouncers add caddy-bouncer -k "$bouncer_key" ``` #### 4. Wrong LAPI Port in Environment User environment may have: ```yaml CROWDSEC_LAPI_URL: "http://localhost:8080" # Wrong: Charon uses 8080 ``` Should be: ```yaml CROWDSEC_LAPI_URL: "http://localhost:8085" # Correct: CrowdSec LAPI port ``` ### Data Flow Diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ CURRENT (BROKEN) FLOW │ └─────────────────────────────────────────────────────────────────┘ User docker-compose.yml Backend Config Caddy Bouncer │ │ │ │ CHARON_SECURITY_ │ │ │ CROWDSEC_API_KEY= │ │ │ "charonbouncerkey2024" │ │ │ │ │ ▼ ▼ │ ┌─────────┐ ┌─────────────┐ │ │ Env Var │ ─────────────▶ │getCrowdSec │ │ │ (static)│ │APIKey() │ │ └─────────┘ └──────┬──────┘ │ │ │ │ Returns unvalidated key │ ▼ │ ┌─────────────┐ │ │ Caddy JSON │──────────────────▶│ │ Config │ Uses invalid key │ └─────────────┘ │ ▼ ┌──────────────┐ │ CrowdSec LAPI│ │ Port 8085 │ └──────┬───────┘ │ │ Key not in │ bouncer list ▼ ┌──────────────┐ │ 403 FORBIDDEN│ │ Every 10 sec │ └──────────────┘ ``` ### Proposed Fix Approach The fix is already designed in [crowdsec_lapi_auth_fix.md](crowdsec_lapi_auth_fix.md). Key changes: #### Phase 1: Backend Changes 1. **Update `getCrowdSecAPIKey()`** to: - First check `/app/data/crowdsec/bouncer_key` file - Fall back to env vars if file doesn't exist - Log source of key for debugging 2. **Add `validateBouncerKey()`** function: - Test key against LAPI before use - Return boolean validity status 3. **Update CrowdSec `Start()` handler**: - After LAPI ready, call `ensureBouncerRegistration()` - If env key invalid, auto-register new bouncer - Store generated key in bouncer_key file - Regenerate Caddy config with valid key #### Phase 2: Docker Entrypoint 1. Add bouncer registration after machine registration 2. Store generated key in persistent volume #### Phase 3: Caddy Config Regeneration 1. After bouncer registration, trigger config reload 2. Ensure new key propagates to Caddy bouncer plugin #### Phase 4: UX Notification for Key Rejection **Critical UX Requirement**: When the user's env var key (`CHARON_SECURITY_CROWDSEC_API_KEY`) is rejected by LAPI and a new key is auto-generated, **the user MUST be notified** so they can update their docker-compose file. Without this: 1. Container starts → reads bad env key 2. Key rejected → generates new key → bouncer works 3. Container restarts → reads bad env key again (env var overrides file) 4. Key rejected → generates ANOTHER new key 5. **Endless loop of re-registration** **Implementation**: 1. **Backend API Endpoint**: Add `/api/v1/crowdsec/key-status` that returns: ```json { "keySource": "env" | "file" | "auto-generated", "envKeyRejected": true | false, "currentKey": "cs-abc123...", // Masked for display "message": "Your environment variable key was rejected. Update your docker-compose with the new key below." } ``` 2. **Frontend Notification Banner**: In Security page CrowdSec section: - Show warning banner if `envKeyRejected: true` - Display the new valid key (copyable) - Provide instructions to update docker-compose.yml - Persist warning until user dismisses or env var is fixed 3. **Log Warning**: On startup, log at WARN level: ``` CROWDSEC: Environment variable key rejected by LAPI. Auto-generated new key. Update your docker-compose.yml: CHARON_SECURITY_CROWDSEC_API_KEY= ``` ### Acceptance Criteria - [ ] CrowdSec bouncer authenticates successfully with LAPI - [ ] No "access forbidden" errors in logs after fix - [ ] Auto-registration works for new deployments - [ ] Existing deployments with invalid keys get auto-fixed - [ ] Key source (env vs file) logged for debugging - [ ] **UX: Warning banner shown in Security page when env key rejected** - [ ] **UX: New valid key displayed and copyable for docker-compose update** - [ ] **UX: Log warning includes the new key for CLI users** --- ## Bug 2: WebUI Displaying Raw Translation Keys ### Symptoms - WebUI shows literal text: `translation.security.crowdsec.title` - Expected behavior: Should show "CrowdSec" - Affects multiple translation keys in Security page ### Files Investigated | File | Purpose | Key Findings | |------|---------|--------------| | [frontend/src/i18n.ts](../../frontend/src/i18n.ts) | i18next initialization | Uses static imports, default namespace is `translation` | | [frontend/src/main.tsx](../../frontend/src/main.tsx) | App entry point | Imports `./i18n` before rendering | | [frontend/src/pages/Security.tsx](../../frontend/src/pages/Security.tsx) | Security dashboard | Uses `useTranslation()` hook correctly | | [frontend/src/locales/en/translation.json](../../frontend/src/locales/en/translation.json) | English translations | All keys exist and are properly nested | | [frontend/src/context/LanguageContext.tsx](../../frontend/src/context/LanguageContext.tsx) | Language context | Wraps app with language state | ### Root Cause Analysis #### Verified Working Elements 1. **Translation Keys Exist**: ```json // frontend/src/locales/en/translation.json (lines 245-281) "security": { "title": "Security", "crowdsec": { "title": "CrowdSec", "subtitle": "IP Reputation & Threat Intelligence", ... } } ``` 2. **i18n Initialization Uses Static Imports**: ```typescript // frontend/src/i18n.ts import enTranslation from './locales/en/translation.json' const resources = { en: { translation: enTranslation }, ... } ``` 3. **Components Use Correct Hook**: ```tsx // frontend/src/pages/Security.tsx const { t } = useTranslation() ... {t('security.crowdsec.title')} ``` #### Probable Root Cause: Namespace Prefix Bug The symptom `translation.security.crowdsec.title` contains the namespace prefix `translation.` which should **never** appear in output. **i18next Namespace Behavior**: - Default namespace: `translation` - When calling `t('security.crowdsec.title')`, i18next looks for `translation:security.crowdsec.title` - If found: Returns value ("CrowdSec") - If NOT found: Returns key only (`security.crowdsec.title`) **The Bug**: The output contains `translation.` prefix, suggesting one of: 1. **Initialization Race Condition**: - i18n module imported but not initialized before first render - Suspense fallback showing raw key with namespace 2. **Production Build Issue**: - Vite bundler not properly including JSON files - Tree-shaking removing translation resources 3. **Browser Cache with Stale Bundle**: - Old JS bundle cached that has broken i18n 4. **KeyPrefix Misconfiguration** (less likely): - Some code may be prepending `translation.` to keys ### Investigation Required To confirm the exact cause, the following debugging is needed: #### 1. Check Browser Console ```javascript // Run in browser DevTools console console.log(i18next.isInitialized) // Should be true console.log(i18next.language) // Should be 'en' or detected language console.log(i18next.t('security.crowdsec.title')) // Should return "CrowdSec" console.log(i18next.getResourceBundle('en', 'translation')) // Should show all translations ``` #### 2. Check Network Tab - Verify no 404 for translation JSON files - Verify main.js bundle includes translations (search for "CrowdSec" in bundle) #### 3. Check React DevTools - Find component using translation - Verify `t` function is from i18next, not a mock ### Proposed Fix Approach #### Hypothesis A: Initialization Race **Fix**: Ensure i18n is fully initialized before React renders **File**: [frontend/src/main.tsx](../../frontend/src/main.tsx) ```tsx // Current import './i18n' ReactDOM.createRoot(...).render(...) // Fixed - Wait for initialization import i18n from './i18n' i18n.on('initialized', () => { ReactDOM.createRoot(document.getElementById('root')!).render( ... ) }) ``` #### Hypothesis B: Production Build Missing Resources **Fix**: Verify Vite config includes JSON files **File**: [frontend/vite.config.ts](../../frontend/vite.config.ts) ```typescript export default defineConfig({ // Ensure JSON imported as modules json: { stringify: true // Keeps JSON as-is }, build: { rollupOptions: { // Ensure locale files not tree-shaken external: [], } } }) ``` #### Hypothesis C: Enable Debug Mode **File**: [frontend/src/i18n.ts](../../frontend/src/i18n.ts) ```typescript .init({ ... debug: true, // Enable to see why key resolution fails ... }) ``` ### Testing Plan 1. **Local Development**: - Clear browser cache and hard reload - Open DevTools console, check for i18n debug output - Verify translations load 2. **Production Build**: - Run `npm run build` - Inspect dist/assets/*.js for translation strings - Verify "CrowdSec" appears in bundle 3. **Docker Environment**: - Rebuild container: `docker build --no-cache` - Test with fresh browser/incognito mode ### Acceptance Criteria - [ ] Security page shows "CrowdSec" not `translation.security.crowdsec.title` - [ ] All translation keys resolve to values - [ ] Works in both dev and production builds - [ ] Works after browser cache clear - [ ] i18next console shows successful initialization --- ## Implementation Priority | Bug | Severity | Effort | Priority | |-----|----------|--------|----------| | Bug 1: LAPI Auth | High (security feature broken) | Medium | P1 | | Bug 2: Translations | Medium (UX issue) | Low | P2 | ### Recommended Order 1. **Bug 1 First**: CrowdSec is a core security feature; broken auth defeats its purpose 2. **Bug 2 Second**: Translation issue is visual/UX, doesn't affect functionality --- ## Related Documentation - [CrowdSec LAPI Auth Fix Design](crowdsec_lapi_auth_fix.md) - Detailed fix design for Bug 1 - [CrowdSec Integration Guide](../crowdsec-integration.md) - Overall CrowdSec architecture - [i18n Setup](../../frontend/src/i18n.ts) - Translation configuration --- ## Appendix: Key Code Sections ### A1: getCrowdSecAPIKey() - Current Implementation **File**: `backend/internal/caddy/config.go` (line ~1129) ```go func getCrowdSecAPIKey() string { key := os.Getenv("CHARON_SECURITY_CROWDSEC_API_KEY") if key == "" { key = os.Getenv("CROWDSEC_API_KEY") } return key } ``` ### A2: i18next Initialization **File**: `frontend/src/i18n.ts` ```typescript const resources = { en: { translation: enTranslation }, es: { translation: esTranslation }, fr: { translation: frTranslation }, de: { translation: deTranslation }, zh: { translation: zhTranslation }, } i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources, fallbackLng: 'en', debug: false, interpolation: { escapeValue: false }, detection: { order: ['localStorage', 'navigator'], caches: ['localStorage'], lookupLocalStorage: 'charon-language', }, }) ``` ### A3: Security Page Translation Usage **File**: `frontend/src/pages/Security.tsx` ```tsx export default function Security() { const { t } = useTranslation() // ... return ( {/* CrowdSec Card */} {t('security.crowdsec.title')} {t('security.crowdsec.subtitle')} {/* ... */} ) } ```