This repository is public, so it documents behavior without publishing the live deployment map. Credentials, account names, addresses, public hostnames, ports, interface names, and hardware identifiers are deployment data rather than reusable documentation.
Tracked configuration uses variables or documentation-only examples. Copy
ansible/inventory/group_vars/homelab/site.yml.example to the ignored
site.yml and fill it from the private operational record before running
Ansible. The deployment renders site-specific monitoring configuration under
the ignored .generated/ directory.
Real .env files also remain ignored and are supplied manually. Ansible checks
their presence and permissions but never reads or creates their values. Exact
ingress routes and endpoint inventories belong in the private operational
record, not in public documentation.
Direct Compose commands require the non-secret interpolation keys shown in the
tracked .env.example files. Routine deployment uses
ansible/playbooks/deploy.yml, which injects the private site values and
renders the monitoring override before Compose validation.
Encrypted secrets are not committed to this public repository. SOPS with age recipients may be reconsidered for a separate private operations repository if manual restoration becomes burdensome. That design would need multiple recovery recipients and a tested offline restore before replacing the current manual secret restoration process.
Central identity is provided by a self-hosted authentik stack (see
docs/identity.md). authentik is the directory (the single user directory)
and the OpenID Connect provider, offering authenticator-app MFA and
passwordless passkey login (WebAuthn, user verification required), for
bookorbit, Actual Budget, Grafana, Tandoor
Recipes, and SparkyFitness.
Actual and Tandoor Recipes keep their local login as a fallback (Tandoor's is
hidden by default but reachable at ?form=1), so an outage of the portal
never locks those apps. bookorbit was not switched to OIDC-only the way
Kavita and Audiobookshelf were (BO-7 only turned off auto-linking an OIDC
login to an existing local account, a different setting); whether its own
local password login stays open to members has not been separately verified.
Grafana and SparkyFitness are
SSO-only with no browser-reachable fallback: Grafana's is a break-glass local
admin an operator re-enables in the database, and SparkyFitness's is an
accepted risk (recovery needs SSH access to flip its login env vars back).
Plex keeps Plex authentication; authentik also accepts a Plex login as a
source, so a member can sign into authentik itself with the Plex account they
already have, scoped away from the operator (see docs/identity.md).
Request apps keep Plex authentication plus local recovery. Infrastructure
retains local console and administrator recovery.
Only authentik is exposed to the internet (through the Cloudflare tunnel);
authentik's database and its own LDAP outpost are internal-only. Every secret is read from files on the server and
none are committed. The stack's data lives under ${APPDATA} and is covered by
restic. Backup, session revocation, and local recovery accounts follow the same
file-based, restic-protected model as the rest of the fleet.
A second factor is enrollable by every account, required of the operator
everywhere, and optional for members. Grafana demands it of everyone who can
reach it; every other app and forward-auth gate demands it of the
grafana-admins group and accepts a password or a passkey from members. Under
the member policy authentik does not challenge an enrolled authenticator, so for
members the practical second factor is a passkey, which satisfies the login in
one step by itself. This is an
accepted risk taken in August 2026 after mandatory MFA proved to be the step
where new members gave up on activating their accounts; the reasoning, and what
it costs, are written out in docs/identity.md. Group membership is still
enforced independently of it, so a password alone never grants more than that
account's own access.
WireGuard on the router is the preferred path for remote access to private operator services. Public ingress remains limited to services with an active remote-user requirement, with one deliberate exception: the member status page (Gatus) is served without any login, because its whole purpose is to answer "is it down?" while the identity stack itself may be the thing that is down. It is read-only, exposes only friendly service names with up/down history for hostnames that are already public, names no internal address or operator tool, and runs hardened (distroless image, non-root, read-only filesystem, no mutating API configured). Residual risk accepted: the page enumerates member-facing hostnames an attacker could also collect from certificate transparency logs.
The three gated hostnames record every request the gate did not let through: the proxy writes an access log and only the non-successful entries are kept, in the private log store on the same host. Before this a member turned away and an operator refused at the admin path left no trace anywhere, so a gate that had broken looked the same as an evening with no visitors. An entry holds the address the request came from, the hostname and path, and, once the portal has answered, the account name and groups it returned; session cookies and authorization headers are redacted by the proxy before anything is written. Successful requests are discarded on the way in, so ordinary member browsing is not retained. Residual risk accepted: this stores member addresses and account names that were previously not recorded at all, which is the cost of being able to tell a broken gate from an empty one.
One reverse-proxy listener stays directly internet-reachable: Caddy terminates
TLS for three media hostnames and proxies to Plex, Jellyfin, and Navidrome,
keeping sustained media playback off the Cloudflare tunnel (Plex's terms of
service preclude it; Jellyfin and Navidrome follow the same reasoning). Plex
enforces its own authentication. Jellyfin also owns its login, backed by
authentik's own LDAP outpost through its LDAP plugin (see docs/identity.md),
with a local admin as break-glass; its /metrics path is refused at the
proxy. That login now depends on authentik being reachable: it used to bind
lldap directly and survive an authentik outage on its own, and that
independent path is gone since lldap's retirement. Navidrome's
web UI sits behind the forward-auth gate (the
proxy passes the authenticated username to Navidrome from a fixed trusted
address, and strips any client-supplied copy of that header); its Subsonic
API and public-share paths bypass the gate and authenticate against
Navidrome's own per-user credentials, which Subsonic clients require. An
unknown hostname fails the TLS handshake because the proxy holds no other
certificate.
The plain-HTTP listener is deliberately not reachable from the internet (firewall change verified from an external vantage, 2026-07-28). It previously served only a redirect to HTTPS, but it also made the tunnel-only sites reachable at the origin with a crafted Host header, bypassing the Cloudflare edge. Closing it means those sites are reachable only through the tunnel, and certificate renewal relies on the TLS-ALPN challenge over the HTTPS listener.
Residual risk, accepted: the remaining direct surface is the reverse proxy and the authenticated APIs of Plex and Jellyfin, all kept current through automated dependency updates. An origin-side rate limit or log-driven ban list was evaluated and rejected: authentication failures terminate at the media servers rather than the proxy log, and volumetric attacks cannot be absorbed at the origin anyway, so either control would add complexity without measurable protection.
- Keep operator services LAN- or VPN-only and retain their built-in login.
- Use unique application credentials and integration keys, and rotate them independently after compromise.
- Do not add proxy authentication while applications remain directly reachable on the LAN; it would be bypassable and complicate API clients.
- Review public tunnel routes periodically and remove unused routes.
- Preserve local recovery for the router, hosts, SSH, and other infrastructure.