Skip to content

Security: b5463/systems

Security

docs/SECURITY.md

SYSTEMS. — Security

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.

Controls in place

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

Hard rules

  • 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_PROXY stays 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.

Not yet built

  • Env var masking in the UI with explicit reveal + audit on reveal.

Change safety

Schema, Docker, proxy, and server-level changes are dangerous, so they go in as dedicated, reviewable steps rather than alongside unrelated work.


Windows Firewall posture

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 5432 not externally reachable
  • Docker API not listening publicly (no 0.0.0.0:2375)
  • Caddy admin 2019 not public
  • only 80/443 open
  • systems.acronym.sk behind admin auth
  • private/internal systems have no public Caddy route
  • wildcard DNS does not imply every subdomain serves something

Resource & exposure hardening

  • 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.

Optional features — security review (per feature)

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.

There aren't any published security advisories