Skip to content

Security review

What protects a Stronghold box's management layer (strongholdd's API, the standalone UI, the Cockpit plugin, the CLI, the Ansible collection), as reviewed on 2026-10-19 against the OWASP Top 10 (2021), with what that review changed and what is left. The box's own confinement is selinux.md; secrets are secrets.md.

Tools

  • govulncheck (v1.1.4): no vulnerability reachable from the code. One in a required module (golang.org/x/sys, GO-2026-5024) is in its Windows code only.
  • gosec (v2.22.8): reviewed by hand. False positives: the backups' AES-GCM nonce (random, crypto/rand), SHA-1 for NetworkManager profiles' UUIDs (UUID version 5's layout, not a security use), TLS clients without MinVersion (Go's client default is TLS 1.2), "hard coded credentials" (key names, not keys), file inclusion and subprocesses with variables (paths and arguments built by strongholdd from checked values). Fixed: the agent's HTTP server had no header timeout.

OWASP Top 10

Risk Stronghold
A01 Broken access control Every route has a least role (viewer, operator, admin; x-role in openapi.yaml, checked against the served routes by a test); the Unix socket is root's only (0600) and is admin. Names that become paths (tokens, secrets, backup entries) are checked: no .., no / where none belongs, regular files only.
A02 Cryptographic failures HTTPS only for the network API (TLS 1.2+, Go's defaults); tokens and sessions are 256 random bits, tokens stored as SHA-256 hashes and compared in constant time; backup secrets sealed with AES-256-GCM, key from PBKDF2-SHA256 (≥100,000 rounds, random salt). API answers carry Cache-Control: no-store (WireGuard configs, the DDNS key).
A03 Injection No shell anywhere: programs run with argument lists. The tools build fixed command lines from checked values (host names can't start with -, so no option injection). The UI builds the DOM with text nodes only (no innerHTML); a link from a coordination server (Tailscale's login URL) is a link only when it is https:.
A04 Insecure design Changes roll back unless confirmed (60 s); the WAN is closed from first boot; secrets never enter the config's git history.
A05 Security misconfiguration The standalone UI sends a strict CSP (no inline script, frame-ancestors 'none'), X-Frame-Options: DENY, nosniff, Referrer-Policy: no-referrer; only the UI's own flat files are served. No HSTS on purpose: the box's certificate is self-signed until it is given one, and HSTS would make its warning impossible to pass.
A06 Vulnerable components Fedora's packages, updated with the image; Go modules vendored and checked with govulncheck.
A07 Identification and authentication failures Password logins through PAM, only for the role groups; sessions expire (an hour idle, twelve hours at most) and live in memory; single sign-on uses the code flow with PKCE, state and nonce, pending logins capped. Failed logins are slowed (a second each) and, since 2026-10-19, limited: ten in five minutes from one address, twenty for one user name, then 429 until the window passes; each failure is logged with its address.
A08 Software and data integrity failures The image is signed and updated as a whole (bootc); a backup's config is checked before it is restored; the config's history is git.
A09 Logging and monitoring failures Every change has its author and message in the history; logins, failed logins and throttling are logged with the client's address; alerts go out for WAN, HA, updates, backups and certificates. No secret values are logged, only whether one is set.
A10 Server-side request forgery strongholdd fetches only what an admin configured (blocklists, OIDC issuer, OpenBao, alert webhooks); none of it from a request's input.

Also

  • HTTP servers: header timeout 10 s everywhere; the network API also limits a request's arrival (5 minutes, for a restore's upload) and idle connections (2 minutes). No write timeout: the tools stream output.
  • Request bodies are limited (maxBody, larger for a restore).
  • CSRF doesn't apply: the UI sends its session in the Authorization header, never a cookie.

Left

  • Sessions are bearer strings in the tab's sessionStorage: an XSS would read them. The CSP and text-only DOM are what keep XSS out.
  • The token file directory is read on every request with a token: fine for a handful of tokens, not for thousands.
  • strongholdd runs as root (it manages the system), confined by SELinux (stronghold_t) rather than by systemd's sandboxing.