Skip to content

Security Findings ​

A defensive review of weaknesses in the platform, first written from the code at commit daeb43f, re-checked against main at 220e6d3, and re-checked again at 6971373. Each has a severity (the reviewer's judgment), the evidence, the impact and a recommended fix. There is no exploit code here. The Security Architecture page lists the controls that are already in place.

Read before hosting this documentation publicly

This page names real weaknesses in a running system. The repository is public, but a tidy, indexed weakness list next to deployment details is far more useful to an attacker than source code alone. If you publish this site at cyberonline.site:

  • Put the whole site behind Cloudflare Access (or HTTP auth), so only your team and the panel can read it. The hosting guide shows how.
  • Or drop this page and the threat model from the public build and keep them internal.
  • Fix or accept the High items below before the live host is reachable by students.
  • Nothing here is a secret value: no passwords, tokens or flags are printed. The risk is the collected roadmap, not any single line.

Severity summary ​

SeverityCountItems
High (verify on live host)2F1, F2
Medium9F3 to F10, F19 (F6 narrowed; F9 partly addressed)
Low6 open, 1 partly fixed, 1 fixedF11 to F13, F15 to F17 open; F14 partly fixed; F18 fixed

Several depend on live configuration the repository cannot show; those say verify.

Changes since the 220e6d3 review (re-checked at 6971373)

  • F6 narrowed by pull requests 308 and 309: every lab network now has an egress ACL that rejects all outbound traffic except the lab's own subnet and the Wazuh manager on TCP 1514/1515, and the reaper re-applies it. Pull request 303 adds a script that binds the Wazuh API to loopback inside its container and rotates the built-in accounts. Enrollment itself is still passwordless.
  • F9 partly addressed further: pull request 269 type-checks the portal on every pull request; pull request 306 repaired the failing portal tests. CI still runs no pytest or jest.
  • F14 partly fixed by pull requests 313 and 314: the portal now sends framing, nosniff and Referrer-Policy headers and no longer advertises Next.js. There is still no HSTS or full Content Security Policy.
  • New: F19, open advisories against the portal's Next.js version, with interim controls from pull requests 312 and 315. Resolved on main by pull request 316 (Next.js 15.5.28).
  • New controls that are not findings: lab provisioning now fails closed when the SIEM agents cannot connect or a stale agent name is still registered (pull requests 195, 197, 211, 275); an internal CA for the Wazuh API that a release must verify (pull request 275); Admin user creation no longer emails passwords (pull request 198). See Security Architecture.
Changes at the 220e6d3 review
  • F18 fixed by pull request 146: logout now sends id_token_hint.
  • F9 partly addressed by pull request 143: CI now fails on an undeclared Python dependency, but still runs no test suite.
  • New controls that close gaps the first review did not list as findings: required TOTP MFA for every user (SEC-03), a password policy with a common-password blocklist and temporary brute-force lockout (SEC-04). See Login, Sessions & Tokens.
  • F2 matters more now: MFA enrolment, the password policy and the login theme are stored in the same un-exported Keycloak database.

High ​

F1. Keycloak admin and master-realm routes exposed through the gateway ​

  • Evidence: the repository nginx config proxies ~ ^/admin/(serverinfo|realms|master) and ~ ^/realms/master to Keycloak, added (per manual Issue 12) so the admin console works over the tunnel. The network appendix says the admin console should be reachable by SSH tunnel only.
  • Impact: the Keycloak admin surface and master realm would be reachable on the public hostname, a large increase in attack surface for the identity provider.
  • Fix: do not expose admin or master-realm paths through the public origin; reach the console over SSH. If remote console access is required, put it behind Cloudflare Access. Verify the live nginx config, which may differ.
  • Source: cyberrange/deploy/guacamole/nginx-ampere.conf, manual/appendix-network-map.md, manual/09-known-issues.md. Discrepancy D3.

F2. Keycloak running in development mode ​

  • Evidence: the unit inside the gateway container runs kc.sh start-dev with no KC_DB configured, which uses the embedded H2 dev-file database and development defaults.
  • Impact: start-dev is not for production; the H2 file database and dev settings are not sized or hardened for real use, and identity data has no separate managed store.
  • Fix: run kc.sh start in production mode with an external database (or an explicitly accepted, backed-up H2), behind HTTPS with production settings. Verify the live host. Until then, export the realm after any realm change: since SEC-03 and SEC-04 a lost Keycloak database also loses every user's enrolled authenticator, the password policy and the login theme.
  • Source: cyberrange/deploy/keycloak/keycloak.service. Open question 3.

Medium ​

F3. Access token in the terminal WebSocket URL ​

  • Evidence: the terminal connects to /api/ssh-websocket?token=<access token>&pod_id=...; the gateway nginx sets no custom access_log, so its default access log would record the query string.
  • Impact: access tokens can land in proxy or Cloudflare logs and browser history.
  • Fix: pass the token in a subprotocol or a short-lived one-time ticket instead of the query string; disable or scrub access logging for this location. Verify whether gateway access logs are on.
  • Source: cyberrange/portal/src/components/terminal/XtermView.tsx, bridge-src/bridge.py, nginx-ampere.conf.

F4. Access token exposed to browser JavaScript ​

  • Evidence: the NextAuth session callback puts the access token on the session, so useSession() exposes it to page scripts (needed for the terminal WebSocket URL).
  • Impact: any XSS in the portal could read a valid 12-hour token.
  • Fix: keep the token server-side; mint a narrow, short-lived credential just for the terminal instead of surfacing the full access token.
  • Source: cyberrange/portal/src/lib/auth.ts.

F5. Direct access grants enabled on the portal client ​

  • Evidence: create_keycloak_realm.sh creates the portal client with directAccessGrantsEnabled=true (the resource-owner password grant).
  • Impact: allows username/password token requests that bypass the browser login flow and its protections.
  • Fix: disable direct access grants unless a specific flow needs them. Verify the live realm. The MFA change (pull request 146) left this flow unchanged on purpose; an account that has not enrolled an authenticator can still get a token by password grant. The verification scripts moved to a separate cyberrange-verify client, which removes their need for it on portal.
  • Source: cyberrange/deploy/keycloak/create_keycloak_realm.sh. Open question 6.

F6. Passwordless Wazuh enrollment reachable from every lab Narrowed ​

  • Evidence: agents auto-enroll without a password (<use_password>no</use_password>); every lab network is peered to the monitoring network so it can reach the manager. Since pull request 308 the lab's egress ACL allows only TCP 1514/1515 to the manager (plus the lab's own subnet) and rejects everything else, including the Wazuh API port 55000 and the uplink DNS resolver. harden_wazuh_api.sh (pull request 303) binds the API to 127.0.0.1 inside the manager container and rotates the built-in wazuh and wazuh-wui accounts.
  • Impact: a student with root in a lab can still reach the enrollment port, so could register spurious agents or send noise to the shared SIEM. The API itself is no longer reachable from a lab once the ACL is in place and the hardening script has run.
  • Fix: use an enrollment password or per-agent keys; restrict enrollment to provisioning. Verify live use_password, and that harden_wazuh_api.sh has been run on each host: its evidence record (WAZUH-API-286-loopback-and-rotation.md) is still marked Pending.
  • Source: manual/06-scoring.md, pod_net.py (pod_acl_rules), deploy/host/harden_wazuh_api.sh, cyberrange/docs/evidence/WAZUH-API-286-loopback-and-rotation.md. Open question 7.

F7. Admin token can open any lab's terminal ​

  • Evidence: the bridge authorizes a connection by calling /pods/{id}/status, which allows the owner or an admin.
  • Impact: an admin token can open a shell into any student's lab by crafting the URL; no admin page does this, but the capability exists. Instructors cannot.
  • Fix: decide whether admins should have lab shell access; if not, require ownership in the bridge. Document it if intended.
  • Source: cyberrange/bridge-src/bridge.py, pods_router.py require_owner_or_admin.

F8. Host operator SSH key installed in every lab container ​

  • Evidence: provisioning installs /home/llms_admin/.ssh/id_rsa.pub into root's authorized_keys in all three containers, although scoring uses the LXD control plane, not SSH.
  • Impact: an unused standing access path from the host operator key into every lab's root account.
  • Fix: remove the key installation if nothing uses it; otherwise document why it is needed.
  • Source: cyberrange/src/provisioning/provision.py.

F9. No automated tests in CI Partly addressed ​

  • Evidence: the release workflow builds and packages but runs no pytest or jest; tests run only on developer machines. Since pull request 143 it does run check_runtime_deps.py, which fails the build on an undeclared Python dependency. Since pull request 269 a second workflow, portal-typecheck.yml, runs npx tsc --noEmit on every pull request (added after a type error that Jest did not catch broke every release build). Pull request 306 repaired the portal's Lab History and middleware Jest expectations.
  • Impact: a regression (including a security regression, such as a broken route guard) can still be released without a test gate. A type check catches type errors only. At 220e6d3, main had 4 failing backend tests and 1 failing portal test and nothing blocked the merge; nothing in CI would block a new failure either.
  • Fix: run the backend and portal suites in CI and block release on failure; make portal-typecheck a required status check.
  • Source: .github/workflows/ampere-release.yml, .github/workflows/portal-typecheck.yml.

F10. Unbounded scoring-table growth ​

  • Evidence: the poller inserts a milestone_verification row and an audit_log row for every check, including FAIL, every 3 seconds per unpassed milestone of each active lab; there is no retention job.
  • Impact: SQLite grows continuously during labs; over time this affects size and query cost, and GET /pods/{id}/milestones returns the full history.
  • Fix: record only status changes or thin old FAIL rows; add a retention job and periodic VACUUM. Team decision.
  • Source: score_poller.py, scoring.py. Pull request 268's source column (schema v15) labels each row but still writes one per check.

F19. Portal framework security advisories (resolved on main) ​

  • Status: resolved after this snapshot. Pull request 316 (merged into main as ba1e1e5, 2026-10-23) upgraded the portal to next 15.5.28 and React 19, pinned postcss 8.5.31, and set poweredByHeader: false with path-scoped framing headers. npm audit --omit=dev --package-lock-only on ba1e1e5 reports 0 vulnerabilities.
  • Before the fix: at 6971373 the portal pinned next ^14.2.35, and npm audit --omit=dev reported 2 production advisories: next (critical, including remote code execution in the Image Optimization API, cache poisoning and middleware bypasses) and its bundled postcss (high). Pull requests 312 and 315 added interim controls: the Image Optimization API is disabled (images: { unoptimized: true }, still set), next-auth and axios were bumped, and an npm run audit:prod script was added.
  • Remaining: keep npm run audit:prod in CI so new advisories are caught.
  • Source: cyberrange/portal/package.json, package-lock.json, next.config.mjs at ba1e1e5.

Low ​

#FindingImpactFixSource
F11scenario_id is not validated and is interpolated into Kali cloud-init YAMLYAML/cloud-init injection into the student's own container (already has sudo); low impactValidate against the catalog IDs (01,06,09,11)models.py, provision.py
F12Legacy Guacamole API routes still reachableDead attack surface (/api/guac-launch, /api/guac-websocket; guacd not deployed)Remove the unused routes and dependencyportal/src/app/api/guac-*
F13/capacity is publicLeaks free RAM, active pod count and limits without authRequire a role, or reduce the detailpods_router.py
F14Partly fixed Few security headers on the portal originWas: no headers at all. Since pull requests 313 and 314 Next.js sends X-Frame-Options: DENY and frame-ancestors 'none' (relaxed to same-origin only for /lab/dvwa/*), nosniff and Referrer-Policy, and drops X-Powered-By. Still no HSTS and no Content Security Policy beyond frame-ancestors on portal pagesAdd HSTS and a script/style CSP (at nginx or in next.config.mjs)portal/next.config.mjs, nginx-ampere.conf
F15Service logs in /tmpLogs lost on reboot; readability depends on umaskMove to a persistent, access-controlled location; ship off-hostsystemd units
F16urllib3 InsecureRequestWarning suppressed globallyHides genuine TLS-verification warnings elsewhereScope the suppression to the intended callprovision_api_fastapi.py
F17BFF path parameters interpolated unencodedLow risk given upstream is trusted and IDs are typed; still worth hardeningencodeURIComponent path paramsapiProxy.ts
F18Fixed id_token_hint never sent on logoutWas: federated logout might not fully end the Keycloak sessionFixed in pull request 146: id_token kept server-side and sent as the hintauth.ts, federated-logout route

Notes on scope ​

  • Severities are the reviewer's judgment from the code, not a formal assessment or a live test.
  • "Verify" items depend on the live host and are collected in Open Questions.
  • No secret values (passwords, tokens, flags) appear anywhere in this documentation. Lab default credentials are mentioned only as the subject of a scenario's vulnerability.

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