feat: break-glass security reset
Implement dual-registry container publishing to both GHCR and Docker Hub
for maximum distribution reach. Add emergency security reset endpoint
("break-glass" mechanism) to recover from ACL lockout situations.
Key changes:
Docker Hub + GHCR dual publishing with Cosign signing and SBOM
Emergency reset endpoint POST /api/v1/emergency/security-reset
Token-based authentication bypasses Cerberus middleware
Rate limited (5/hour) with audit logging
30 new security enforcement E2E tests covering ACL, WAF, CrowdSec,
Rate Limiting, Security Headers, and Combined scenarios
Fixed container startup permission issue (tmpfs directory ownership)
Playwright config updated with testIgnore for browser projects
Security: Token via CHARON_EMERGENCY_TOKEN env var (32+ chars recommended)
Tests: 689 passed, 86% backend coverage, 85% frontend coverage
This commit is contained in:
116
tests/security-teardown.setup.ts
Normal file
116
tests/security-teardown.setup.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Security Teardown Setup
|
||||
*
|
||||
* This file runs AFTER all security-tests complete.
|
||||
* It disables all security modules to ensure browser tests run without blocking.
|
||||
*
|
||||
* Uses a two-strategy approach:
|
||||
* 1. Try normal API with authentication
|
||||
* 2. Fall back to emergency reset endpoint if API is blocked by ACL/security
|
||||
*
|
||||
* Uses continue-on-error pattern - individual module disable failures won't
|
||||
* prevent other modules from being disabled.
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Security Module Testing Plan
|
||||
*/
|
||||
|
||||
import { test as teardown } from '@bgotink/playwright-coverage';
|
||||
import { request } from '@playwright/test';
|
||||
|
||||
teardown('disable-all-security-modules', async () => {
|
||||
console.log('\n🔒 Security Teardown: Disabling all security modules...');
|
||||
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080';
|
||||
const emergencyToken = process.env.CHARON_EMERGENCY_TOKEN;
|
||||
|
||||
const modules = [
|
||||
{ key: 'security.acl.enabled', value: 'false' },
|
||||
{ key: 'security.waf.enabled', value: 'false' },
|
||||
{ key: 'security.crowdsec.enabled', value: 'false' },
|
||||
{ key: 'security.rate_limit.enabled', value: 'false' },
|
||||
{ key: 'feature.cerberus.enabled', value: 'false' },
|
||||
];
|
||||
|
||||
// Strategy 1: Try normal API with auth
|
||||
const requestContext = await request.newContext({
|
||||
baseURL,
|
||||
storageState: 'playwright/.auth/user.json',
|
||||
});
|
||||
|
||||
const errors: string[] = [];
|
||||
let apiBlocked = false;
|
||||
|
||||
for (const { key, value } of modules) {
|
||||
try {
|
||||
const response = await requestContext.post('/api/v1/settings', {
|
||||
data: { key, value },
|
||||
});
|
||||
if (response.status() === 403) {
|
||||
apiBlocked = true;
|
||||
console.warn(` ⚠ API blocked (403) while disabling ${key}`);
|
||||
break;
|
||||
}
|
||||
console.log(` ✓ Disabled via API: ${key}`);
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to disable ${key}: ${e}`;
|
||||
errors.push(errorMsg);
|
||||
console.warn(` ⚠ ${errorMsg}`);
|
||||
apiBlocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await requestContext.dispose();
|
||||
|
||||
// Strategy 2: If API is blocked, use emergency reset endpoint
|
||||
if (apiBlocked && emergencyToken) {
|
||||
console.log(' ⚠ API blocked - using emergency reset endpoint...');
|
||||
|
||||
try {
|
||||
const emergencyContext = await request.newContext({ baseURL });
|
||||
const response = await emergencyContext.post(
|
||||
'/api/v1/emergency/security-reset',
|
||||
{
|
||||
headers: {
|
||||
'X-Emergency-Token': emergencyToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { reason: 'Playwright teardown - API was blocked' },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok()) {
|
||||
const body = await response.json();
|
||||
console.log(
|
||||
` ✓ Emergency reset successful: ${body.disabled.join(', ')}`
|
||||
);
|
||||
// Clear errors since emergency reset succeeded
|
||||
errors.length = 0;
|
||||
} else {
|
||||
console.error(` ✗ Emergency reset failed: ${response.status()}`);
|
||||
errors.push(`Emergency reset failed with status ${response.status()}`);
|
||||
}
|
||||
await emergencyContext.dispose();
|
||||
} catch (e) {
|
||||
console.error(' ✗ Emergency reset error:', e);
|
||||
errors.push(`Emergency reset error: ${e}`);
|
||||
}
|
||||
} else if (apiBlocked && !emergencyToken) {
|
||||
console.error(' ✗ API blocked but CHARON_EMERGENCY_TOKEN not set!');
|
||||
errors.push('API blocked and no emergency token available');
|
||||
}
|
||||
|
||||
// Stabilization delay - wait for Caddy config reload
|
||||
console.log(' ⏳ Waiting for Caddy config reload...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(
|
||||
'\n⚠️ Security teardown had errors (continuing anyway):',
|
||||
errors.join('\n ')
|
||||
);
|
||||
// Don't throw - let other tests run even if teardown partially failed
|
||||
} else {
|
||||
console.log('✅ Security teardown complete: All modules disabled\n');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user