import { test, expect, request as playwrightRequest } from '@playwright/test'; import { EMERGENCY_TOKEN, EMERGENCY_SERVER } from '../../fixtures/security'; /** * Break Glass - Tier 2 (Emergency Server) Validation Tests * * These tests verify the emergency server (port 2020) works independently of the main application, * proving defense in depth for the break glass protocol. * * Architecture: * - Tier 1: Main app endpoint (/api/v1/emergency/security-reset) - goes through Caddy/CrowdSec * - Tier 2: Emergency server (:2020/emergency/*) - bypasses all security layers (sidecar door) * * Why this matters: If Tier 1 is blocked by ACL/WAF/CrowdSec, Tier 2 provides an independent recovery path. */ // Store health status in a way that persists correctly across hooks const testState = { emergencyServerHealthy: undefined as boolean | undefined, healthCheckComplete: false, }; async function checkEmergencyServerHealth(): Promise { const BASIC_AUTH = 'Basic ' + Buffer.from(`${EMERGENCY_SERVER.username}:${EMERGENCY_SERVER.password}`).toString('base64'); const emergencyRequest = await playwrightRequest.newContext({ baseURL: EMERGENCY_SERVER.baseURL, }); try { const response = await emergencyRequest.get('/health', { headers: { 'Authorization': BASIC_AUTH }, timeout: 3000, }); return response.ok(); } catch { return false; } finally { await emergencyRequest.dispose(); } } async function ensureHealthChecked(): Promise { if (!testState.healthCheckComplete) { console.log('🔍 Checking tier-2 server health before tests...'); testState.emergencyServerHealthy = await checkEmergencyServerHealth(); testState.healthCheckComplete = true; if (!testState.emergencyServerHealthy) { console.log('⚠️ Tier-2 server is unavailable - tests will be skipped'); } else { console.log('✅ Tier-2 server is healthy'); } } return testState.emergencyServerHealthy ?? false; } test.describe('Break Glass - Tier 2 (Emergency Server)', () => { const EMERGENCY_BASE_URL = EMERGENCY_SERVER.baseURL; const BASIC_AUTH = 'Basic ' + Buffer.from(`${EMERGENCY_SERVER.username}:${EMERGENCY_SERVER.password}`).toString('base64'); // Skip individual tests if emergency server is not healthy test.beforeEach(async ({}, testInfo) => { const isHealthy = await ensureHealthChecked(); if (!isHealthy) { console.log('⚠️ Emergency server not accessible from test environment - continuing test anyway'); // Changed from testInfo.skip() to allow test to run and identify root cause // testInfo.skip(true, 'Emergency server not accessible from test environment'); } }); test('should access emergency server health endpoint without ACL blocking', async ({ request }) => { // This tests the "sidecar door" - completely bypasses main app security const response = await request.get(`${EMERGENCY_BASE_URL}/health`, { headers: { 'Authorization': BASIC_AUTH, }, }); expect(response.ok()).toBeTruthy(); let body; try { body = await response.json(); } catch (e) { // Note: Can't get text after json() fails because body is consumed console.error(`❌ JSON parse failed: ${String(e)}`); body = { _parseError: String(e) }; } expect(body.status, `Expected 'ok' but got '${body.status}'. Parse error: ${body._parseError || 'none'}`).toBe('ok'); expect(body.server).toBe('emergency'); }); test('should reset security via emergency server (bypasses Caddy layer)', async ({ request }) => { // Use Tier 2 endpoint - proves we can bypass if Tier 1 is blocked const response = await request.post(`${EMERGENCY_BASE_URL}/emergency/security-reset`, { headers: { 'X-Emergency-Token': EMERGENCY_TOKEN, 'Authorization': BASIC_AUTH, }, }); expect(response.ok()).toBeTruthy(); let result; try { result = await response.json(); } catch { result = { success: false, disabled_modules: [] }; } expect(result.success).toBe(true); expect(result.disabled_modules).toContain('security.acl.enabled'); expect(result.disabled_modules).toContain('security.waf.enabled'); expect(result.disabled_modules).toContain('security.rate_limit.enabled'); }); test('should validate defense in depth - both tiers work independently', async ({ request }) => { // First, ensure security is enabled by resetting via Tier 2 const resetResponse = await request.post(`${EMERGENCY_BASE_URL}/emergency/security-reset`, { headers: { 'X-Emergency-Token': EMERGENCY_TOKEN, 'Authorization': BASIC_AUTH, }, }); expect(resetResponse.ok()).toBeTruthy(); // Wait for propagation await new Promise(resolve => setTimeout(resolve, 2000)); // Verify Tier 2 still accessible even after reset const healthCheck = await request.get(`${EMERGENCY_BASE_URL}/health`, { headers: { 'Authorization': BASIC_AUTH, }, }); expect(healthCheck.ok()).toBeTruthy(); let health; try { health = await healthCheck.json(); } catch (e) { // Note: Can't get text after json() fails because body is consumed console.error(`❌ JSON parse failed: ${String(e)}`); health = { status: 'unknown', _parseError: String(e) }; } expect(health.status, `Expected 'ok' but got '${health.status}'. Parse error: ${health._parseError || 'none'}`).toBe('ok'); }); test('should enforce Basic Auth on emergency server', async ({ request }) => { // /health is intentionally unauthenticated for monitoring probes // Protected endpoints like /emergency/security-reset require Basic Auth const response = await request.post(`${EMERGENCY_BASE_URL}/emergency/security-reset`, { headers: { 'X-Emergency-Token': EMERGENCY_TOKEN, // Deliberately omitting Authorization header to test auth enforcement }, failOnStatusCode: false, }); // Should get 401 without Basic Auth credentials expect(response.status()).toBe(401); }); test('should reject invalid emergency token on Tier 2', async ({ request }) => { // Even Tier 2 validates the emergency token const response = await request.post(`${EMERGENCY_BASE_URL}/emergency/security-reset`, { headers: { 'X-Emergency-Token': 'invalid-token-12345678901234567890', 'Authorization': BASIC_AUTH, }, failOnStatusCode: false, }); expect(response.status()).toBe(401); const result = await response.json(); expect(result.error).toBe('unauthorized'); }); test('should rate limit emergency server requests (lenient in test mode)', async ({ request }) => { // Test that rate limiting works but is lenient (50 attempts vs 5 in production) // Make multiple requests rapidly const requests = Array.from({ length: 10 }, () => request.post(`${EMERGENCY_BASE_URL}/emergency/security-reset`, { headers: { 'X-Emergency-Token': EMERGENCY_TOKEN, 'Authorization': BASIC_AUTH, }, }) ); const responses = await Promise.all(requests); // All should succeed in test environment (50 attempts allowed) for (const response of responses) { expect(response.ok()).toBeTruthy(); } }); test('should provide independent access even when main app is blocking', async ({ request }) => { // Scenario: Main app (:8080) might be blocked by ACL/WAF // Emergency server (:2019) should still work // Test emergency server is accessible const emergencyHealth = await request.get(`${EMERGENCY_BASE_URL}/health`, { headers: { 'Authorization': BASIC_AUTH, }, }); expect(emergencyHealth.ok()).toBeTruthy(); // Test main app is also accessible (in E2E environment both work) const mainHealth = await request.get('http://localhost:8080/api/v1/health'); expect(mainHealth.ok()).toBeTruthy(); // Key point: Emergency server provides alternative path if main is blocked const mainHealthData = await mainHealth.json(); const emergencyHealthData = await emergencyHealth.json(); expect(mainHealthData.status).toBe('ok'); expect(emergencyHealthData.server).toBe('emergency'); }); });