Repository navigation
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.
- 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.
- 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.
| 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. |
-
Session cookie — HMAC-signed with a per-install secret (32 random bytes stored in the
settingstable 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.
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 allinternal/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=1This flips the notification validator from Strict to LAN policy. Loopback and cloud-metadata endpoints remain blocked.
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. |
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-srcdoes not allowunsafe-inlineorunsafe-eval. -
img-src https:admits book covers from any HTTPS origin — the metadata providers can return covers from arbitrary CDN hosts.
-
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;
/tmpis anemptyDir. - Capabilities — all Linux capabilities dropped.
-
Seccomp —
RuntimeDefaultprofile. - 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.
| 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.
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. |
-
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 validateor feed intogrype sbom:.... -
Base images — digest-pinned in
Dockerfile. Dependabot PRs surface upstream digest updates weekly.
- 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.
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.
Getting started
Setup guides
How-to guides — proxy auth (v1.0)
How-to guides — OIDC (v1.0)
- Google Sign-In
- GitHub OAuth via Dex
- Authelia as OIDC provider
- Authentik
- Keycloak
- Rotate OIDC client secrets
- Recover from broken OIDC
How-to guides — multi-user (v1.0)
Reference
Contributing