Skip to content

Security

vavallee edited this page Apr 15, 2026 · 2 revisions

Security

Bindery is an open-source automation tool that holds API keys, talks to LAN services, and writes to the local filesystem. This page documents the threat model we design against, the controls baked into the application, and how to verify them from the outside.

For the disclosure policy and how to report a vulnerability, see SECURITY.md.


Threat model

What Bindery defends against

  • Unauthenticated access to the API, UI, OPDS feed, or backup endpoints.
  • SSRF via user-supplied URLs (webhooks, indexer base URLs, download-client hostnames).
  • Classic web attacks — XSS, clickjacking, MIME sniffing, CSRF via third-party sites.
  • Privilege escalation inside the container — distroless + nonroot + readonly rootfs + caps dropped.
  • Supply-chain tampering — every image is signed with SLSA provenance against the workflow that built it.
  • Credential leakage — session cookies are signed; the API key lives in a Secret on Kubernetes; the DB file is 0600.

What's explicitly out of scope

  • A nation-state adversary with unlimited budget. Bindery is homelab software.
  • A compromised upstream metadata provider (OpenLibrary / Google Books / Hardcover) returning crafted JSON.
  • Physical access to the host or container runtime.
  • Denial-of-service via resource exhaustion. Rate limiting for unauthenticated login attempts is in place (5 / 15 min / IP); broader abuse is meant to be absorbed by your ingress.
  • Self-XSS — the admin pasting attacker-controlled content into their own UI.

Authentication

Routes and required auth

Route prefix Public Notes
GET /api/v1/auth/status, POST /api/v1/auth/login/logout/setup yes Bootstrap + login surface.
GET /api/v1/health, /system/status yes Health probes.
All other /api/v1/* no Session cookie or API key.
/opds/* no Basic auth, API key, or session.
/* (SPA) yes Static file serving only; API paths are authenticated.

Mechanisms

  • Session cookie — HMAC-signed with a per-install secret (32 random bytes stored in the settings table on first boot).
  • API key — 32 random bytes, hex-encoded. Bootstrapped on first boot or seeded from BINDERY_API_KEY. Rotatable in the UI.
  • Argon2id passwords — user passwords hashed with argon2id using OWASP 2024 parameters (64 MiB memory, 1 iteration, 4 lanes). PHC-formatted strings — nothing reversible is stored.
  • Login rate limit — 5 failed attempts per IP per 15 minutes, matching Sonarr's posture.

Cookie Secure auto-detect

The session cookie sets Secure automatically when TLS is detected either directly on the request (r.TLS != nil) or via the X-Forwarded-Proto: https header from a reverse proxy. Override via:

BINDERY_COOKIE_SECURE=auto    # default
BINDERY_COOKIE_SECURE=always  # proxy doesn't set X-Forwarded-Proto
BINDERY_COOKIE_SECURE=never   # legacy plain-HTTP install, no TLS at all

SSRF controls

internal/httpsec.ValidateOutboundURL is invoked before every outbound request that takes a user-supplied URL. Two policies:

  • PolicyStrict (webhooks) — blocks loopback, link-local, RFC1918, IPv6 ULA, and cloud-metadata endpoints.
  • PolicyLAN (indexers, download clients) — blocks only loopback, link-local, and cloud-metadata. RFC1918 is allowed because this is the homelab pattern.

Blocked by both:

  • 127.0.0.0/8, ::1, ::ffff:127.0.0.1
  • 169.254.0.0/16 (link-local IPv4), fe80::/10 (IPv6 link-local)
  • 169.254.169.254 (AWS/Azure/DO metadata)
  • fd00:ec2::254 (AWS metadata IPv6)
  • metadata.google.internal (hostname match, pre-DNS)

DNS rebinding defense — we resolve A/AAAA records for the hostname and validate every returned address. A rebinding attack that flips the resolver's answer between "example.com" → 1.1.1.1 and a second lookup returning 127.0.0.1 is caught at validation time.

Escape hatch — to allow webhooks to on-LAN targets (ntfy / Home Assistant / Gotify), set:

BINDERY_NOTIFICATIONS_ALLOW_PRIVATE=1

This flips the notification validator from Strict to LAN policy. Loopback and cloud-metadata endpoints remain blocked.


Response headers

Every response carries:

Header Value Purpose
X-Content-Type-Options nosniff Disables MIME sniffing.
X-Frame-Options DENY Clickjacking protection.
Referrer-Policy strict-origin-when-cross-origin Limits referrer leakage.
Content-Security-Policy see below Locks down script/style/frame sources.
Strict-Transport-Security max-age=63072000; includeSubDomains Only when TLS is detected.

The exact CSP

default-src 'self'; img-src 'self' data: https:;
style-src 'self' 'unsafe-inline'; script-src 'self';
connect-src 'self'; frame-ancestors 'none';
base-uri 'self'; form-action 'self'
  • style-src 'unsafe-inline' is present because Tailwind injects inline style blocks at runtime and the React theme bootstrap sets dark-mode class before the main bundle loads. script-src does not allow unsafe-inline or unsafe-eval.
  • img-src https: admits book covers from any HTTPS origin — the metadata providers can return covers from arbitrary CDN hosts.

Container hardening

  • Base image — gcr.io/distroless/static-debian12:nonroot (digest-pinned).
  • User — UID 65532 (nonroot), no passwd entry, no shell.
  • Root filesystem — read-only at runtime; /tmp is an emptyDir.
  • Capabilities — all Linux capabilities dropped.
  • Seccomp — RuntimeDefault profile.
  • Privilege escalation — disallowed at the pod securityContext level.
  • Base image freshness — Dependabot watches the Docker ecosystem and opens PRs weekly when a new digest lands for a pinned tag.

Kubernetes posture

Control Default
Dedicated ServiceAccount with automountServiceAccountToken: false on
API key sourced from Secret via valueFrom.secretKeyRef on when auth.apiKey or auth.existingSecret set
NetworkPolicy (namespace + pod selectors) opt-in, networkPolicy.enabled=true
Pod Security Standard: restricted complies
ArgoCD PostSync smoke test on /api/v1/health opt-in, hooks.postsyncSmoke.enabled=true

The chart's helm-unittest suite under charts/bindery/tests/ asserts the posture so that accidental regressions fail CI.


CI security pipeline

Every push, PR, and weekly cron runs the full scan set. SARIF uploads to the GitHub Security tab; anyone with repo-read access can see findings.

Tool Job What it catches
gosec sast-go Go-specific AST security issues.
govulncheck sast-go CVE-affected imports in the compiled binary.
golangci-lint sast-go Hygiene + security lints (bodyclose, errorlint, noctx, sqlclosecheck, rowserrcheck).
Semgrep sast-frontend Cross-language pattern matching, React rules.
ESLint (plugin-security) sast-frontend JS-specific injection / regex / unsafe-DOM patterns.
npm audit sast-frontend Known CVEs in the frontend lockfile.
gitleaks secrets-scan Secrets committed to git.
hadolint iac-scan Dockerfile lints.
Helm lint + kubesec iac-scan Chart + rendered-manifest posture.
Trivy container-scan OS + language-layer CVEs.
Grype container-scan Second-opinion CVE scan.
Dockle container-scan CIS Docker Benchmark.
Syft container-scan SBOM (SPDX + CycloneDX).
ZAP baseline dast-api Passive HTTP scan against a live service container.
helm-unittest policy-test Chart posture assertions.
OpenSSF Scorecard scorecard Repo hygiene score.

Supply chain

  • Images — signed with SLSA build provenance via actions/attest-build-provenance@v1. Verify:

    gh attestation verify \
      oci://ghcr.io/vavallee/bindery@sha256:<digest> \
      --repo vavallee/bindery
  • Release archives — Syft SPDX SBOMs attached alongside each artifact. Verify with syft validate or feed into grype sbom:....

  • Base images — digest-pinned in Dockerfile. Dependabot PRs surface upstream digest updates weekly.


Known limitations

  • No per-endpoint rate limit beyond login. Broader abuse protection lives at the reverse proxy — run Bindery behind Traefik / Caddy / nginx with rate-limit middleware in production.
  • No DNS-rebinding Host-header check in the app. Rely on the reverse proxy to enforce a canonical Host. The SSRF validator catches the reverse case (outbound rebinding), not the inbound one.
  • No in-cluster CVE admission gate. The CI pipeline catches this before the image ships, but we don't re-scan at deploy time inside the cluster.
  • ZAP baseline is passive. Active DAST (full ZAP scan, Burp spider) is too noisy to run on every PR; we rerun the passive scan weekly.

Reporting a vulnerability

Use the GitHub Security Advisory flow: https://github.com/vavallee/bindery/security/advisories/new.

Targets:

  • Acknowledgement within 7 days.
  • Assessment within 14 days.
  • Fix within 90 days (shorter if exploited in the wild).

Reporters are credited in the release notes unless they ask to remain anonymous. There is no bug bounty — only credit and a timely fix.

Clone this wiki locally