Skip to content

Security Architecture ​

This section is a defensive review of the platform: the controls that are in place, the trust boundaries, the threats considered, and an honest list of weaknesses with recommendations. It documents the team's own capstone so the panel and future maintainers can reason about it. It contains no exploit code.

  • Threat Model: trust boundaries, assets, and a STRIDE-style threat table.
  • Findings: the weaknesses found, by severity, with evidence and fixes, plus the caution about hosting this publicly.

Design in one line ​

The platform runs many deliberately vulnerable machines on one host, so the whole design assumes the lab machines are hostile and keeps them boxed in: each lab is its own isolated network of unprivileged containers, reachable by students only through an authenticated portal, with the host's own services bound to private interfaces and the only inbound path an outbound Cloudflare Tunnel.

Defense in depth ​

Controls that are in place ​

These are verified in the code and are the platform's real strengths.

AreaControlWhere
AuthenticationOAuth2 authorization code with PKCE and state; confidential client; 12-hour session in an encrypted cookieportal/src/lib/auth.ts
AuthenticationTOTP multi-factor authentication required for every account at browser sign-in; Admin-only, audited Reset MFA that deletes only OTP credentialsdeploy/host/enable_keycloak_mfa.sh, users_router.py
AuthenticationPassword policy (8 to 128 characters, not username or email, last 3 not reused, 2,112-entry common-password blocklist) and temporary brute-force lockout (10 failures, 1 to 15 minutes)deploy/host/enable_keycloak_password_policy.sh
Session endLogout sends id_token_hint and clears the session cookie, so the Keycloak session really endsportal/src/app/api/auth/federated-logout/route.ts
AuthorizationEvery request re-validated by Keycloak token introspection (cache at most 60 s, invalidated on disable or role change)auth.py, introspect_cache.py
AuthorizationRouter-level require_app_role, plus require_role, require_owner (returns 404 to hide existence) and require_owner_or_admin; a test classifies every routeauth.py, test_sec01_rbac_matrix.py
Fail-closed startupMissing auth secret or introspection URL aborts startup; AUTH_ENABLED=false logs a CRITICAL warningauth.py validate_auth_config
Concurrency safetyAdmission runs in BEGIN IMMEDIATE with a unique partial index (one live lab per student); RAM and storage gatespods_router.py, schema/v2.sql, capacity.py
Network isolationOne OVN network per lab; labs peered only to the monitoring network, never to each other; peering via a restricted sudo wrapperpod_net.py
Lab egressEach lab network is created with its own ACL attached and a default egress action of reject: only the lab's own subnet and the Wazuh manager on TCP 1514/1515 are allowed; the uplink DNS resolver and the lab's gateway address are rejected explicitly. The reaper re-applies the rules and detaches any foreign ACL on every sweeppod_net.py (create_pod_network, ensure_pod_acl, reconcile_pod_networks)
Container isolationUnprivileged containers with CPU and memory caps; students have sudo only inside their own labmanual/02-gateway-and-wazuh.md, provision.py
Surface reductionHost services bind 10.115.77.1/127.0.0.1; no inbound web ports on OCI; public access is an outbound Cloudflare Tunnelmanual/appendix-network-map.md, config.py
Data accessThe API is the only SQLite client; all queries are parameterized; the portal reaches data only through the BFFdb.py, apiProxy.ts
Token exposureThe BFF injects the access token server-side; the browser calls same-origin API routesapiProxy.ts
SecretsLoaded from environment or a secrets file; a half-configured Vault fails closed; the audit log is written to avoid secretssecrets_loader.py, audit_router.py
Evidence uploadsImages decoded and re-encoded, with size and pixel caps and a sandbox CSP; 5 per review, 5 MB eachevidence_images.py
Least privilege (SIEM)Wazuh API reached over a loopback proxy with a pinned certificate; separate read-only scoring and agent-management users. setup_wazuh_provisioner.sh creates the provisioner user with only agent:read and agent:delete and a deny on the manager (agent 000); harden_wazuh_api.sh binds the API to 127.0.0.1 inside the container and rotates the built-in wazuh and wazuh-wui accounts. Both scripts keep passwords off the command line and never print themconfig.py, wazuh_client.py, deploy/host/setup_wazuh_provisioner.sh, harden_wazuh_api.sh
SIEM fail-closedA lab is not marked ACTIVE if a stale agent with the student's name is still registered (WAZUH_STALE_AGENT), if its agents are not active within WAZUH_AGENT_CONNECT_TIMEOUT_S (default 90 s), or on a TLS or 401/403 error from the Wazuh API; it is rolled back insteadwazuh_client.py (finish_enroll, abort_if_names_remain, wait_for_agents_active), provision.py
Portal response headersX-Frame-Options: DENY and frame-ancestors 'none' on every portal path (same-origin only for the embedded DVWA), X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, no X-Powered-By; the Image Optimization API is disabledportal/next.config.mjs
User managementNo self-signup; strict input schemas; self- and last-admin protection; every write audited; cached tokens revoked on change. New passwords are never emailed: the Admin page shows them once in a handoff dialog that closes itself after 2 minutes, and validation errors on the password routes never echo the rejected valueusers_router.py, provision_api_fastapi.py, portal/src/components/admin/CredentialHandoffModal.tsx
Supply chainRelease pipeline verifies the lockfile hash, health-gates the apply with crash-loop detection, rolls back on failure and records why; CI fails on an undeclared Python dependency; every pull request is type-checked (tsc --noEmit); the Wazuh agent signing key is checked against a pinned fingerprint when the golden image is bakeddeploy/host/pull_release.sh, deploy/host/check_runtime_deps.py, .github/workflows/portal-typecheck.yml, deploy/golden/bake_wazuh_agent.sh
Integrity of the SIEM pinThe host's pinned Wazuh API certificate is preserved across releases and only replaced from the live manager when its fingerprint and DNS:localhost SAN match. Once issue_wazuh_internal_ca.py --install has placed certs/wazuh-api-ca.crt, the live certificate must chain to that CA for localhost and the scoring user must authenticate, or the release is rolled backdeploy/host/release_sync.py, sync_wazuh_api_cert.py, issue_wazuh_internal_ca.py, pull_release.sh

The weaknesses are on the Findings page, kept separate so this page can stand as the control inventory.

Sources

cyberrange/src/provisioning/auth.py, introspect_cache.py, cyberrange/deploy/host/enable_keycloak_mfa.sh, enable_keycloak_password_policy.sh, sync_wazuh_api_cert.py, issue_wazuh_internal_ca.py, harden_wazuh_api.sh, setup_wazuh_provisioner.sh, check_runtime_deps.py, pods_router.py, capacity.py, pod_net.py, provision.py, db.py, secrets_loader.py, evidence_images.py, users_router.py, wazuh_client.py, test_sec01_rbac_matrix.py, cyberrange/portal/src/lib/auth.ts, apiProxy.ts, next.config.mjs, components/admin/CredentialHandoffModal.tsx, cyberrange/deploy/host/pull_release.sh, cyberrange/deploy/golden/bake_wazuh_agent.sh, .github/workflows/portal-typecheck.yml, manual/02-gateway-and-wazuh.md, manual/06-scoring.md, manual/appendix-network-map.md.

Cyber Range capstone documentation (Capstone 2 · Group 2 · A4102). Documents main at commit 6971373.