Security
Logins and sessions
Section titled “Logins and sessions”- Passwords are stored as Argon2id hashes. After 10 failed logins within 15 minutes for a user or from an address, further attempts are refused for 15 minutes; wrong two-factor codes count as failed logins, and after 5 wrong codes the half-finished sign-in has to start again with the password.
- Two-factor authentication (TOTP, any authenticator app) under Profile. Turn it on for every admin. Each code works once (a code someone watched you type, or caught with a phishing proxy, is already used up).
- Changing your own password asks for the current one and signs out all your sessions.
- The session cookie is
HttpOnly,SameSite=Strictand, with anhttps://TRIXOS_PUBLIC_URL,Secure. Changes and live connections (job logs, events) with it are accepted only from the panel’s own pages. - API tokens are stored hashed, can be limited to scopes, addresses and an expiry date (Users, roles & API tokens).
The browser
Section titled “The browser”The web container sends a Content-Security-Policy (no inline or third-party scripts, connections to the panel
only), X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy. The web UI ships
without source maps.
The panel’s own API docs (/api/docs, the Swagger UI and the spec) list every route and field, so only signed-in
admins see them; TRIXOS_API_DOCS=public opens them, off turns them off. The public reference is
api.trixos.sh.
Client addresses
Section titled “Client addresses”Login throttling, console links (bound to the address that opened them), PXE source checks and the audit log rely
on the client’s address. The web container takes it from the connection, and from X-Forwarded-For only from the
peers named in TRIXOS_REAL_IP_FROM (.env):
| Value | When |
|---|---|
none |
Clients reach the panel’s port directly (installed without --domain). Nobody can fake an address. |
local |
A reverse proxy on the same host, e.g. the built-in HTTPS. Only together with WEB_BIND_ADDRESS=127.0.0.1, so the port isn’t reachable from outside. |
| addresses / CIDRs | A reverse proxy on another machine, e.g. TRIXOS_REAL_IP_FROM=10.0.0.5; firewall the port for everyone else. |
| unset | 127.0.0.1 ::1 172.16.0.0/12 (the behaviour of 0.8.0, for installations from before 0.8.1) |
Only the last X-Forwarded-For entry counts (what your proxy saw), and the API gets exactly one address. The API
itself believes that address only from the web container (TRUST_PROXY: web in the compose file, resolved by name),
not from a console viewer or any other container on a shared Docker network. An
installation from before 0.8.1 whose port 8088 is reachable from the internet without a proxy should set
TRIXOS_REAL_IP_FROM=none and run docker compose up -d.
Outgoing requests
Section titled “Outgoing requests”Some addresses that admins enter make TrixOS itself connect somewhere: webhooks, custom ISO downloads and the boot
files of OS profiles. These connections only go to public addresses: loopback, private networks (RFC 1918, ULA),
link-local (including cloud metadata at 169.254.169.254), CGNAT, multicast and IPv6 addresses with such an IPv4
address inside (::ffff:10.0.0.1, NAT64, 6to4) are refused. The address is checked when the connection is made,
not only when the URL is saved: the host name is resolved once and TrixOS connects to exactly the address it
checked, so a name that answers with an internal address the second time (DNS rebinding) gets nowhere. Every
redirect is checked the same way. Only http and https, no user name or password in the URL.
Need an internal target (a webhook receiver or ISO mirror in your own network)? Set
TRIXOS_ALLOW_PRIVATE_URLS=true; that lifts the check for all of these. BMCs, switches, PDUs and site agents are
internal by nature and are not affected.
Install with --domain panel.example.com: Caddy (compose profile https) gets a Let’s Encrypt certificate, redirects
HTTP to HTTPS, sends HSTS, and the panel’s own port listens on 127.0.0.1 only
(Installation → HTTPS). It runs on the host network, so it sees the real client
addresses, and has no admin API. With your own reverse proxy, bind the port to localhost (WEB_BIND_ADDRESS) or
firewall it, and set TRIXOS_REAL_IP_FROM. With Cloudflare in front use the SSL mode Full (strict). Without any of
this the panel speaks plain HTTP on port 8088, and the installer warns about it.
Containers
Section titled “Containers”All containers run with no-new-privileges; the API, worker, poller, asset and viewer services run as an
unprivileged user without any Linux capabilities. Postgres and Redis listen on 127.0.0.1 only. /metrics has no
authentication and is never published.
The Docker socket (root on the host) is held only by the docker-proxy service: no network, no secrets, read-only
file system. It offers the viewer service a socket of its own in a volume only the viewer mounts, and lets through
exactly what starting a console needs: creating containers from the kvm-viewer image on the viewers network, without
privileges, added capabilities, mounts, devices or host namespaces, and starting, stopping, inspecting and removing
those containers. Everything else (exec, other images, volumes, builds, other containers) is refused with 403 and
logged (docker compose logs docker-proxy). The viewer service is reachable only inside the compose network and
every call needs VIEWER_SECRET.
Updates
Section titled “Updates”Releases are signed (Ed25519); the installer, TrixOS and the host updater install only releases signed with the key
built into them, never an older version (the installer only with --allow-downgrade), and the updater rolls back
automatically when the new version does not start (Platform Updates). install --download-only DIR verifies a release without installing it. The installer script itself is fetched over HTTPS from trixos.sh: read
it before you run it if you want to be sure. Rebuild the base images regularly for their security fixes.
Your checklist
Section titled “Your checklist”- HTTPS (
--domain, or your own proxy),TRIXOS_PUBLIC_URLwithhttps://, port 8088 not reachable from the internet,TRIXOS_REAL_IP_FROMmatching your setup. - Two-factor authentication for every admin; API tokens with the smallest scopes and an expiry.
.env(master key!) in a password manager; nightly database backups (Backups & Restore).- Firewall: only 80/443 (and the agents’ WireGuard port) open; SSH with keys.
- Keep the host updated (
unattended-upgrades) and update TrixOS when a release is out.