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 withoutMinVersion(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
Authorizationheader, 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.