Skip to content

Security

In the admin: General, then Security. See the Settings reference.

Read this before exposing Stackyard beyond your own network.

Stackyard serves plain HTTP and does not terminate TLS.

Run it on a trusted network, or behind a reverse proxy that terminates TLS and adds its own access control. See Advanced configuration.

  • Passwords are hashed with scrypt and a per-password salt, stored in PHC string format.
  • Session tokens are HMAC-signed and verified with a constant-time comparison.
  • Sessions expire after an idle lifetime, 12 hours by default. A session in use is reissued past the halfway mark.
  • Login is rate-limited to 5 attempts per IP per 15 minutes. Counters are in memory, so a restart clears them and they are not shared across replicas. Run a single instance behind any proxy.
  • Changing the password rotates the session secret, signing out every other device. Sign out all devices does the same without changing the password.

Authentication is only in force once a password is stored. Until then, the endpoint that sets one accepts the first caller. See First setup.

Stored secrets are stripped from the config before it reaches the browser. A populated field reports as set without returning its value, in config responses and in exports alike.

A secret is restored only for the request it was stored for. If a save changes where the credential would be sent, by editing a badge URL, its non-secret headers, or any non-secret field of a widget’s config, the stored value is not restored and Stackyard names what must be re-entered. Matching on an item’s id alone would let an imported config point an existing credential somewhere new.

Secrets are stored in plain text in apps.json on the data volume. Protect that volume with filesystem permissions and backups.

The server blocks outbound requests to private, loopback, link-local, carrier-grade NAT, multicast and reserved ranges, in IPv4 and IPv6. It resolves the host, checks the address, then pins the resolved IP so the connection cannot be re-pointed after the check.

localhost is refused by name. Dotless hostnames such as Docker container names are trusted, as is the host IP set in General.

ALLOW_PRIVATE_IPS=true disables the guard entirely. Most homelab installs need it.

The guard limits what a compromised widget can reach. It does not protect against an admin, who can already point widgets anywhere.

HostWhy
cdn.jsdelivr.netThe dashboard-icons set.
UnsplashWallpaper, only when the wallpaper source is Unsplash.
api.github.comThe update check, when About is opened, at most once an hour.

Nothing else leaves your network, and no usage data is collected.

Icons load through the server, which caches each one for 24 hours, so the CDN does not learn which services your dashboard shows. Set the wallpaper to an image or a solid colour and Unsplash is never contacted.

The provided Compose file drops all capabilities, adds back only what is needed, sets no-new-privileges, and bounds memory, process count and log size.

Inside, supervisord runs as root to bind port 80 and start both processes. It drops the API to the unprivileged node user, and nginx drops its workers to nginx. The API, which parses untrusted input, never runs as root.

Every released image is signed with cosign using keyless signing, bound to the GitHub Actions workflow that built it and recorded in Sigstore’s transparency log.

cosign verify ghcr.io/sandobserver/stackyard:1.5.0 \
--certificate-identity-regexp '^https://github.com/SandObserver/stackyard/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

Both flags matter. Without them cosign accepts a signature from any identity, which proves only that something signed the image.

Each release build also scans the image with Trivy and fails on a HIGH or CRITICAL finding that has a fix, so a flagged image never reaches a registry. An SPDX SBOM is attached to the build.

See SECURITY.md. The full security notes are in docs/security.md.