chore: clean .gitignore cache
This commit is contained in:
@@ -1,264 +0,0 @@
|
||||
/**
|
||||
* Emergency Server E2E Tests (Tier 2 Break Glass)
|
||||
*
|
||||
* Tests the separate emergency server running on port 2019.
|
||||
* This server provides failsafe access when the main application
|
||||
* security is blocking access.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Emergency server enabled in docker-compose.e2e.yml
|
||||
* - Port 2019 accessible from test environment
|
||||
* - Basic Auth credentials configured
|
||||
*
|
||||
* Reference: docs/plans/break_glass_protocol_redesign.md - Phase 3.2
|
||||
*/
|
||||
|
||||
import { test, expect, request as playwrightRequest } from '@playwright/test';
|
||||
import { EMERGENCY_TOKEN, EMERGENCY_SERVER, enableSecurity } from '../fixtures/security';
|
||||
import { TestDataManager } from '../utils/TestDataManager';
|
||||
|
||||
test.describe('Emergency Server (Tier 2 Break Glass)', () => {
|
||||
test('Test 1: Emergency server health endpoint', async () => {
|
||||
console.log('🧪 Testing emergency server health endpoint...');
|
||||
|
||||
// Create a new request context for emergency server
|
||||
const emergencyRequest = await playwrightRequest.newContext({
|
||||
baseURL: EMERGENCY_SERVER.baseURL,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await emergencyRequest.get('/health');
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const body = await response.json();
|
||||
expect(body.status).toBe('ok');
|
||||
expect(body.server).toBe('emergency');
|
||||
|
||||
console.log(' ✓ Health endpoint responded successfully');
|
||||
console.log(` ✓ Server type: ${body.server}`);
|
||||
console.log('✅ Test 1 passed: Emergency server health endpoint works');
|
||||
} finally {
|
||||
await emergencyRequest.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('Test 2: Emergency server requires Basic Auth', async () => {
|
||||
console.log('🧪 Testing emergency server Basic Auth requirement...');
|
||||
|
||||
const emergencyRequest = await playwrightRequest.newContext({
|
||||
baseURL: EMERGENCY_SERVER.baseURL,
|
||||
});
|
||||
|
||||
try {
|
||||
// Test 2a: Request WITHOUT Basic Auth should fail
|
||||
const noAuthResponse = await emergencyRequest.post('/emergency/security-reset', {
|
||||
headers: {
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
expect(noAuthResponse.status()).toBe(401);
|
||||
console.log(' ✓ Request without auth properly rejected (401)');
|
||||
|
||||
// Test 2b: Request WITH Basic Auth should succeed
|
||||
const authHeader =
|
||||
'Basic ' +
|
||||
Buffer.from(`${EMERGENCY_SERVER.username}:${EMERGENCY_SERVER.password}`).toString(
|
||||
'base64'
|
||||
);
|
||||
|
||||
const authResponse = await emergencyRequest.post('/emergency/security-reset', {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
expect(authResponse.ok()).toBeTruthy();
|
||||
expect(authResponse.status()).toBe(200);
|
||||
|
||||
const body = await authResponse.json();
|
||||
expect(body.success).toBe(true);
|
||||
|
||||
console.log(' ✓ Request with valid auth succeeded');
|
||||
console.log('✅ Test 2 passed: Basic Auth properly enforced');
|
||||
} finally {
|
||||
await emergencyRequest.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('Test 3: Emergency server bypasses main app security', async ({ request }) => {
|
||||
console.log('🧪 Testing emergency server security bypass...');
|
||||
|
||||
const testData = new TestDataManager(request, 'emergency-server-bypass');
|
||||
|
||||
try {
|
||||
// Step 1: Enable security on main app (port 8080)
|
||||
await request.post('/api/v1/settings', {
|
||||
data: { key: 'feature.cerberus.enabled', value: 'true' },
|
||||
});
|
||||
|
||||
// Create restrictive ACL on main app
|
||||
const { id: aclId } = await testData.createAccessList({
|
||||
name: 'test-emergency-server-acl',
|
||||
type: 'whitelist',
|
||||
ipRules: [{ cidr: '192.168.99.0/24', description: 'Unreachable network' }],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
await request.post('/api/v1/settings', {
|
||||
data: { key: 'security.acl.enabled', value: 'true' },
|
||||
});
|
||||
|
||||
// Wait for settings to propagate
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Step 2: Verify main app blocks requests (403)
|
||||
const mainAppResponse = await request.get('/api/v1/proxy-hosts');
|
||||
expect(mainAppResponse.status()).toBe(403);
|
||||
console.log(' ✓ Main app (port 8080) blocking requests with ACL');
|
||||
|
||||
// Step 3: Use emergency server (port 2019) to reset security
|
||||
const emergencyRequest = await playwrightRequest.newContext({
|
||||
baseURL: EMERGENCY_SERVER.baseURL,
|
||||
});
|
||||
|
||||
const authHeader =
|
||||
'Basic ' +
|
||||
Buffer.from(`${EMERGENCY_SERVER.username}:${EMERGENCY_SERVER.password}`).toString(
|
||||
'base64'
|
||||
);
|
||||
|
||||
const emergencyResponse = await emergencyRequest.post('/emergency/security-reset', {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
await emergencyRequest.dispose();
|
||||
|
||||
expect(emergencyResponse.ok()).toBeTruthy();
|
||||
expect(emergencyResponse.status()).toBe(200);
|
||||
console.log(' ✓ Emergency server (port 2019) succeeded despite ACL');
|
||||
|
||||
// Wait for settings to propagate
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Step 4: Verify main app now accessible
|
||||
const allowedResponse = await request.get('/api/v1/proxy-hosts');
|
||||
expect(allowedResponse.ok()).toBeTruthy();
|
||||
console.log(' ✓ Main app now accessible after emergency reset');
|
||||
|
||||
console.log('✅ Test 3 passed: Emergency server bypasses main app security');
|
||||
} finally {
|
||||
await testData.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('Test 4: Emergency server security reset works', async ({ request }) => {
|
||||
console.log('🧪 Testing emergency server security reset functionality...');
|
||||
|
||||
// Step 1: Enable all security modules
|
||||
await enableSecurity(request);
|
||||
console.log(' ✓ Security modules enabled');
|
||||
|
||||
// Step 2: Call emergency server endpoint
|
||||
const emergencyRequest = await playwrightRequest.newContext({
|
||||
baseURL: EMERGENCY_SERVER.baseURL,
|
||||
});
|
||||
|
||||
const authHeader =
|
||||
'Basic ' +
|
||||
Buffer.from(`${EMERGENCY_SERVER.username}:${EMERGENCY_SERVER.password}`).toString('base64');
|
||||
|
||||
const resetResponse = await emergencyRequest.post('/emergency/security-reset', {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
await emergencyRequest.dispose();
|
||||
|
||||
expect(resetResponse.ok()).toBeTruthy();
|
||||
const resetBody = await resetResponse.json();
|
||||
expect(resetBody.success).toBe(true);
|
||||
expect(resetBody.disabled_modules).toBeDefined();
|
||||
expect(resetBody.disabled_modules.length).toBeGreaterThan(0);
|
||||
|
||||
console.log(` ✓ Disabled modules: ${resetBody.disabled_modules.join(', ')}`);
|
||||
|
||||
// Wait for settings to propagate
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Step 3: Verify settings are disabled
|
||||
const statusResponse = await request.get('/api/v1/security/status');
|
||||
if (statusResponse.ok()) {
|
||||
const status = await statusResponse.json();
|
||||
|
||||
// At least some security should now be disabled
|
||||
const anyDisabled =
|
||||
!status.acl?.enabled ||
|
||||
!status.waf?.enabled ||
|
||||
!status.rateLimit?.enabled ||
|
||||
!status.cerberus?.enabled;
|
||||
|
||||
expect(anyDisabled).toBe(true);
|
||||
console.log(' ✓ Security status updated - modules disabled');
|
||||
}
|
||||
|
||||
console.log('✅ Test 4 passed: Emergency server security reset functional');
|
||||
});
|
||||
|
||||
test('Test 5: Emergency server minimal middleware (validation)', async () => {
|
||||
console.log('🧪 Testing emergency server minimal middleware...');
|
||||
|
||||
const emergencyRequest = await playwrightRequest.newContext({
|
||||
baseURL: EMERGENCY_SERVER.baseURL,
|
||||
});
|
||||
|
||||
try {
|
||||
const authHeader =
|
||||
'Basic ' +
|
||||
Buffer.from(`${EMERGENCY_SERVER.username}:${EMERGENCY_SERVER.password}`).toString(
|
||||
'base64'
|
||||
);
|
||||
|
||||
const response = await emergencyRequest.post('/emergency/security-reset', {
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
// Verify emergency server responses don't have WAF headers
|
||||
const headers = response.headers();
|
||||
expect(headers['x-waf-status']).toBeUndefined();
|
||||
console.log(' ✓ No WAF headers (bypassed)');
|
||||
|
||||
// Verify no CrowdSec headers
|
||||
expect(headers['x-crowdsec-decision']).toBeUndefined();
|
||||
console.log(' ✓ No CrowdSec headers (bypassed)');
|
||||
|
||||
// Verify no rate limit headers
|
||||
expect(headers['x-ratelimit-limit']).toBeUndefined();
|
||||
console.log(' ✓ No rate limit headers (bypassed)');
|
||||
|
||||
// Emergency server should have minimal middleware:
|
||||
// - Basic Auth (if configured)
|
||||
// - Request logging
|
||||
// - Recovery middleware
|
||||
// NO: WAF, CrowdSec, ACL, Rate Limiting, JWT Auth
|
||||
|
||||
console.log('✅ Test 5 passed: Emergency server uses minimal middleware');
|
||||
console.log(' ℹ️ Emergency server bypasses: WAF, CrowdSec, ACL, Rate Limiting');
|
||||
} finally {
|
||||
await emergencyRequest.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Break Glass - Tier 2 (Emergency Server) Validation Tests
|
||||
*
|
||||
* These tests verify the emergency server (port 2019) 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 (:2019/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.
|
||||
*/
|
||||
|
||||
test.describe('Break Glass - Tier 2 (Emergency Server)', () => {
|
||||
const EMERGENCY_BASE_URL = 'http://localhost:2020';
|
||||
const EMERGENCY_TOKEN = 'test-emergency-token-for-e2e-32chars';
|
||||
const BASIC_AUTH = 'Basic ' + Buffer.from('admin:testpass').toString('base64');
|
||||
|
||||
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();
|
||||
const body = await response.json();
|
||||
expect(body.status).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();
|
||||
const result = await response.json();
|
||||
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();
|
||||
const health = await healthCheck.json();
|
||||
expect(health.status).toBe('ok');
|
||||
});
|
||||
|
||||
test('should enforce Basic Auth on emergency server', async ({ request }) => {
|
||||
// Verify that emergency server still requires authentication
|
||||
|
||||
const response = await request.get(`${EMERGENCY_BASE_URL}/health`, {
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
|
||||
// Should get 401 without 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user