SYSTEMS. is privileged infrastructure. It controls Docker, the reverse proxy, uploaded source, env vars, routes, and logs. It's built to be hardened, private, least-privilege, and admin-only. It is not claimed to be unhackable.
The dashboard and API must be unreachable unless you're authenticated as an admin.
The "not yet" column is the genuinely unbuilt work, not a release timeline.
| Control | In place | Not yet |
|---|---|---|
| Public signup | Disabled (none exists) | — |
| Dashboard access | Admin-only | — |
| API auth | HttpOnly, SameSite=Strict cookie session; Secure and __Host- prefix in production; server-tracked jti plus per-user token_version; bearer/query tokens rejected |
— |
| Two-factor (admin) | Opt-in TOTP, set up on the Admin page | — |
| Session revocation | Per-session revoke; password/2FA changes and "sign out other sessions" rotate the current session and invalidate the rest | — |
| CSRF | Session-bound HMAC token required in X-CSRF-Token for every authenticated mutation, plus strict Origin validation and SameSite defense-in-depth |
— |
| Password hashing | bcrypt (cost 12); new/reset passwords require at least 15 characters | breached-password screening |
| Plaintext passwords | None stored | — |
| Env vars | AES-256-GCM encrypted at rest, values never returned by API | — |
| Secrets in frontend | None (build-time vars are non-secret only) | — |
| Secrets in logs | Avoided | — |
| CORS | Locked to systems.acronym.sk |
— |
| Response headers | CSP default-src 'none' (JSON-only API), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, Permissions-Policy (sensors/media/payment off), Cross-Origin-Opener/Resource-Policy: same-origin, HSTS (2y, includeSubDomains, preload) in production; X-Powered-By removed |
dashboard SPA gets its own CSP at the proxy |
| Login rate limiting | 10/min per IP + escalating lockout/backoff; equal-time unknown-user checks | distributed rate-limit store for multi-node |
| API rate limiting | 100/min global, deploy 5/min | per-route tuning |
| IP denylist | Persistent exact-IP and CIDR-range bans, enforced before auth; add/list/remove admin API; self-ban (incl. a range containing the admin) refused; broad prefixes (< /8 v4, < /32 v6) rejected; check fails open on internal error so a bad row can't 500 every request; actions audited | dashboard controls |
| Proxy trust | Disabled by default; explicit TRUST_PROXY=true required behind a controlled proxy | named/CIDR trust policy for multi-proxy topologies |
| Audit log | All admin actions recorded; tamper-evident SHA-256 hash chain (each row links to the previous), verifiable via GET /api/audit/verify; optional retention (AUDIT_RETENTION_DAYS) |
export, offsite anchoring |
| Destructive actions | Delete keeps history; purge needs the typed slug | — |
| Docker socket | Internal to API container only | — |
| Docker/Caddy admin API | Never public; Caddy admin bound to localhost | — |
| Postgres | Wired; not publicly exposed (SQLite is the local dev file) | — |
| Uploaded code | Treated as untrusted; zip-slip prevented; entry/size caps; build concurrency, CPU, memory, and time ceilings | isolate builds from the Docker daemon |
| Container hardening | CapDrop ALL, no-new-privileges, mem/CPU caps, ICC off |
— |
| Custom Dockerfile | Built, admin-only, off behind ENABLE_DOCKERFILE_MODE |
— |
- The Docker socket and Docker API are never exposed publicly; the frontend never talks to Docker directly. Only the API/deploy worker controls Docker, internally.
- The reverse proxy admin/API is never public.
- Production dashboard/API traffic must use TLS. The session cookie is
Secure,HttpOnly,SameSite=Strict, path-scoped to/, and uses the__Host-prefix in production. TRUST_PROXYstays false unless the API is reachable only through the trusted Caddy/nginx hop; otherwise forwarded client IP headers are attacker-controlled.- The internal database is never public.
- Uploaded code is untrusted. Extraction is zip-slip-guarded with entry-count and per-file size limits.
- Path traversal is prevented on extraction (resolved paths must stay under the destination).
- The reserved route-attestation path is owned by Caddy/nginx, before uploaded application handlers. Generated proxy config carries a per-slug HMAC credential to the private API upstream; it is never injected into the app.
- Attestation payloads use AES-256-GCM authenticated encryption, a random
challenge nonce, a five-second expiry, strict size limits, no redirects, and
Cache-Control: no-store. Modification, cross-system substitution, and replay fail closed. TLS is still required on public routes. - Attestations expose only a hashed deployment fingerprint and minimal observed status. They never contain secrets, environment values, ports, container IDs, logs, or arbitrary app data. A malicious app remains untrusted; application health and Docker metrics are measured independently.
- Env var masking in the UI with explicit reveal + audit on reveal.
Schema, Docker, proxy, and server-level changes are dangerous, so they go in as dedicated, reviewable steps rather than alongside unrelated work.
Open publicly (inbound):
80/tcp,443/tcp- remote admin (RDP/SSH) only if needed, restricted to your IP
Never public:
- Postgres
5432 - Docker API / socket (
2375/2376) - Caddy admin API (
2019) - internal backend/API ports, deployment worker ports
- project internal container ports
- Redis/queues (if added later)
Verify with PowerShell / the helper script:
Get-NetFirewallRule -Direction Inbound -Enabled True | Get-NetFirewallPortFilter | Sort-Object LocalPort
Get-NetTCPConnection -State Listen | Sort-Object LocalPort
.\scripts\check-firewall-windows.ps1 -PublicIp <SERVER_IP>Checklist:
- Postgres
5432not externally reachable - Docker API not listening publicly (no
0.0.0.0:2375) - Caddy admin
2019not public - only
80/443open -
systems.acronym.skbehind admin auth - private/internal systems have no public Caddy route
- wildcard DNS does not imply every subdomain serves something
- Every deployed container gets memory/CPU/PIDs limits, a restart policy, and
log rotation from
DEFAULT_CONTAINER_*(see OPERATIONS). One project can't exhaust the host. - Destructive actions are split: delete stops the container and pulls the public route but keeps history, while purge removes everything and requires typing the system slug to confirm.
- Before a destructive action, the UI shows whether a recent backup exists and warns when none does.
These features are built and off by default. Each one enables only through its
explicit .env flag.
| Feature | Key risks | Mitigations | Status | Safe to enable by default? |
|---|---|---|---|---|
| 2 GB uploads | OOM, disk exhaustion, zip-slip, abandoned temp files | stream to disk (no buffering), server-side cap, disk-fit check, traversal-safe temp path, admin-only, rate-limit, cleanup on cancel | built, off by default, enable after host validation | No — keep the 100 MB cap until validated |
| DB provisioning | credential leak, privilege escalation, SQL injection | per-system DB + least-privilege role, random secret, masked + never returned, parameterized SQL (no concatenation), allowlisted identifiers, typed-confirm destructive | built, off by default, enable after host validation | No (ENABLE_DB_PROVISIONING=false) |
| Dockerfile builds | arbitrary build instructions | disabled by default, explicit opt-in, admin-only, build timeout, resource/PIDs limits, no secrets in logs, audited, rejected silently-never | gate implemented + tested; build host-validated | No (ENABLE_DOCKERFILE_MODE=false) |
| Node API execution | runs uploaded code | hardened container (CapDrop ALL, no-new-privileges), limits, sanitized names/slugs, no shell concatenation, private mode = no route | classification/plan implemented + tested; runtime host-validated | Yes (same trust model as static/Vue) |
| Workers/Bots | long-running uploaded code | no public route by default, limits, logs, restart policy | no-route plan implemented + tested; runtime host-validated | Yes |
| Shell console | container code-exec | disabled by default, admin-only, audited; honest disabled state (no fake terminal) | gate implemented + tested; live use host-validated | No (ENABLE_SHELL_CONSOLE=false) |
| GitHub webhooks | forged payloads, token leak, builds external code | HMAC-SHA256 constant-time verify over raw body, branch filter, admin-only, secret in .env, rate-limit |
built, off by default, enable after host validation — riskiest flag (pulls + builds external code) | No (ENABLE_GITHUB_DEPLOYS=false) |
| Notifications | secret leak, spam | admin-only config, best-effort POST (won't block deploys), failures logged | built, off by default (needs NOTIFY_WEBHOOK_URL) |
Yes once a webhook URL is configured |
Default posture: every high-risk feature ships disabled and stays that way until it's been validated on the real host. None are claimed working live.
See also: HARDENING.md (verification), BACKUPS.md, DISASTER_RECOVERY.md.