Security
Status: Accepted
The threat model, and how each threat is mitigated. A single-operator, LAN-only, self-hosted tool — but “small” is not “no threats”, and one of them (VPN failure) has consequences outside the machine.
Satisfies: C2, C4, C6, A7, G8.
flowchart LR subgraph in["In scope"] s1[Operator secrets] s2[VPN isolation] s3[Web UI boundary] s4[Supply chain] s5[Untrusted stack forks] s6[LAN trust boundary] end subgraph out["Out of scope — stated, not ignored"] o1[Nation-state adversary] o2[Physical host access] o3[Malicious operator] o4[Compromised host OS] endThe out-of-scope items are named deliberately. A household media tool that claimed to defend against a nation-state or a compromised host OS would be lying, and a threat model’s honesty is most of its value. If the operator’s machine is already owned, lemonfiber’s guarantees are void — and it says so rather than implying otherwise.
Assets
Section titled “Assets”| Asset | Sensitivity |
|---|---|
| VPN private key | High — enables tunnel impersonation |
| Usenet/indexer credentials | High — paid accounts, some tied to identity |
| Service API keys | Medium — full control of each service |
| Web UI session | Medium — controls the whole stack |
| The operator’s real IP | High — the VPN exists to protect it |
| Media library | Low confidentiality, high replaceability cost |
| Household watch history | Low, but private within the home |
STRIDE by component
Section titled “STRIDE by component”lemonfiber (the control surface)
Section titled “lemonfiber (the control surface)”| Threat | Vector | Mitigation |
|---|---|---|
| Spoofing | Something impersonates the web UI on the LAN | Loopback by default; LAN binding refused without auth (C6-R4); plain HTTP stated honestly (C6-R6) |
| Tampering | Config or manifest altered out of band | Drift detection surfaces it (C9); manifest validation refuses malformed forks (F1-R9) |
| Repudiation | “I didn’t change that” | Every change journaled with actor and prior value (E4-R1) |
| Info disclosure | Secret in a log / bundle / error | Allow-list redaction, fails closed (C4-R5); secrets never in output (A7-R3); enforced by test (Q-R26) |
| Denial of service | Web UI flooded | Rate-limited auth (C6-R11); local-only surface limits exposure |
| Elevation | Household member reaches admin | Admin surfaces unreachable from LAN by binding, not obscurity (C6-R1) |
The VPN path — the consequential one
Section titled “The VPN path — the consequential one”| Threat | Vector | Mitigation |
|---|---|---|
| Info disclosure | VPN fails open, real IP leaks to peers | Fail-closed killswitch; egress verified empirically by comparing IPs inside both containers (C2-R1); continuous while torrents run (C2-R9) |
| Tampering | qBittorrent escapes the namespace | network_mode: service:gluetun; a client not sharing the namespace reports leaking (C2-R12) |
| Repudiation | “Was I ever leaking?” | Leak detection notifies at critical severity and is recorded (C2-R10) |
This is the row that matters most. Every other threat here costs money or
privacy within the home; a VPN leak exposes the operator’s identity to
strangers. It is the one place the spec insists on empirical proof over
configuration (P3).
The credential store
Section titled “The credential store”| Threat | Vector | Mitigation |
|---|---|---|
| Info disclosure | Other local user reads secrets | Owner-only file permissions (A7-R8); the guarantee is stated honestly — protects against other users, not against malware running as the operator (A7-R9) |
| Tampering | Backup carries secrets to insecure storage | Backups labelled sensitive at creation (A7-R12, E3-R4) |
| Elevation | Service API key reused as a foothold | Keys are per-service and rotatable (A7-R4) |
Supply chain
Section titled “Supply chain”| Threat | Vector | Mitigation |
|---|---|---|
| Tampering | Malicious dependency | cargo-deny advisories + source allow-list (Q-R32); forbid(unsafe_code) shrinks the blast radius |
| Tampering | Dependency adds telemetry | Banned and checked (G8-R11, Q-R32) |
| Tampering | Compromised container image | Pinned tags (E1-R1); images from known publishers; digest pinning available |
| Info disclosure | Build leaks secrets | No secrets in the build; release artifacts signed with checksums (Q-R34) |
| Tampering | A published binary is swapped or rebuilt maliciously | SLSA build provenance per release, which a consumer can verify; the SBOM OPS-R20 also asks for is not produced yet |
The stack fork boundary
Section titled “The stack fork boundary”| Threat | Vector | Mitigation |
|---|---|---|
| Tampering | A --stack-dir fork does something hostile |
It runs with the operator’s own privileges by their own choice — same trust as any software they run. Manifest validation still applies; capabilities beyond an allow-list are refused (stack.toml contract) |
| Elevation | Fork requests NET_ADMIN for a non-VPN service |
Capability allow-list refuses it at validation |
Secure-by-default posture
Section titled “Secure-by-default posture”The defaults are the security model, because defaults are what people run (P5):
| Default | Not the default |
|---|---|
| Admin surfaces on loopback | Exposed to LAN |
| Web UI off until asked | Always-on daemon |
Only Gluetun gets NET_ADMIN |
Broad capabilities |
| Pinned image tags | Floating |
| No telemetry | Opt-out analytics |
| HTTP stated honestly | Self-signed TLS teaching click-through |
Privacy as a security property
Section titled “Privacy as a security property”G8: no telemetry, no installation
identifier, every outbound request enumerable and disableable — enforced by a
test (G8-R9), not by intent. For a tool people run precisely to avoid being
watched, phoning home would be a security failure, not merely a faux pas.
Supply-chain posture and its ceilings
Section titled “Supply-chain posture and its ceilings”SonarCloud runs on every repo. OpenSSF Scorecard runs on each repo that
publishes an artefact or the specification; wiring it to the newest four is
outstanding (Q-R59). The checks we can enforce, we enforce — and stay green:
| Enforced | How |
|---|---|
| Pinned-Dependencies | Every uses: is SHA-pinned (ADR-0009); Dependabot advances them |
| Token-Permissions | Minimal, job-scoped permissions: on every workflow |
| SAST | SonarCloud on every PR; CodeQL where a compiled or workflow-heavy repo makes it meaningful |
| Vulnerabilities | OSV-Scanner; zero open SonarCloud vulnerabilities |
| Dependency-Update-Tool | Dependabot on every repo (ADR-0016) |
| Dangerous-Workflow | No untrusted input in run:; fork PRs never see secrets |
| Branch-Protection | PR-required, signed commits, strict status checks, linear history, conversation-resolution |
| Fuzzing | cargo-fuzz targets over the manifest parser and its validation; a smoke run on every PR that touches them, a long run weekly, corpus carried between runs |
| Security-Policy / License / Maintained / CI-Tests | Present and green |
Some checks are structurally capped for a solo, pre-release, ethical-source project. These are documented, not gamed — a low sub-score with a stated reason is honest; a padded one is not:
| Capped check | Why | Rises when |
|---|---|---|
| Code-Review, Contributors | One maintainer, who cannot review their own PRs | A second maintainer joins and PRs are reviewed before merge |
| Signed-Releases | The artifacts are attested — actions/attest runs on each build and gh attestation verify succeeds against every published one — but Scorecard reads the release’s assets, and an attestation is not one; it lives in GitHub’s attestation store |
Provenance appears beside the artifacts, or the check learns to ask the store. L1-R2 obliges a verifiable signature at 1.0.0, with L1-R4 and L1-R5 obliging an installer to check it (Q-R44, OPS-R20) |
| Packaging | Pre-release, and the shell installer is the only one published | The tap publish returns at 1.0.0 (L1-R3), which waits on a tap token, alongside the PowerShell installer and a Windows target |
| CII-Best-Practices | The badge is a manual registration | Registered at public launch |
| License | Hippocratic 3.0 is deliberately not OSI-approved, so Scorecard may not recognise it | Not a defect — an accepted trade-off (licence rationale) |
enforce_admins is on for every repository that carries code or specification,
so the maintainer meets the same gates as anybody else and main cannot be
written past them. It is off only on .github, which holds community health
files and no gated content. Where a rule is ever set aside, the override is
recorded (overrides).
Disclosure
Section titled “Disclosure”A private path in each repo’s SECURITY.md (GOV-R24). Security fixes are the
primary legitimate override use — a spec PR
describing the vulnerability must not precede the patch.
Requirements
Section titled “Requirements”| ID | Requirement |
|---|---|
| Q-R38 | The threat model MUST state its out-of-scope threats explicitly, not omit them. |
| Q-R39 | VPN egress isolation MUST be verified empirically, continuously while torrents run. |
| Q-R40 | Secrets MUST NOT appear in any output, enforced by test; redaction MUST fail closed. |
| Q-R41 | Credential-store guarantees MUST be stated accurately, including what they do not protect against. |
| Q-R42 | cargo-deny MUST enforce advisories, licence allow-list, and a telemetry/banned-crate list. |
| Q-R43 | Container capabilities beyond an allow-list MUST be refused at manifest validation. |
| Q-R44 | Release artifacts MUST be checksummed and signed. |
| Q-R45 | Each repo MUST publish a private security disclosure path. |
| Q-R46 | The no-telemetry property MUST be enforced by an automated test. |
| Q-R63 | The enforceable OpenSSF Scorecard / SonarCloud checks MUST be kept green; a structurally-capped check MUST be documented with its reason and lift condition, never gamed. |
Related
Section titled “Related”This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page