Files
public/README.md
T
thesabear e2765ba749 Remove unverified LDAP workaround claim from SSO docs
The source material says header-based forward-auth is 'the viable
path instead, if SSO is wanted for an app like this' - conditional
contingency guidance for a scenario that may not have actually come
up, not a description of something built and running. Presenting it
as live, deployed behavior in both docs/sso-and-identity.md and the
README was a real accuracy error. Removed from both; the doc still
covers what's actually confirmed (Authentik, dual-zone registration,
native OIDC vs. proxy forward-auth split, least-privilege Git access).
2026-08-22 19:12:59 +03:00

14 KiB

thesabear's Homelab

Most homelabs are a media server with some extra apps bolted on. This one is a platform: the same categories of systems a company's platform/security team owns — secrets management, zero-trust network segmentation, self-hosted CI/CD, human-gated fleet orchestration, a self-built SOC — run at homelab scale, for real, 24/7, by one person.

11
fleet hosts
55
compose stacks
16
documented systems
4
trust boundaries
13
automated maintenance flows

The decision that shows the most

Most self-hosted setups have one trust boundary: "you're on the LAN, you're trusted." That's the easy answer, and it's wrong — a single compromised container can reach everything else with no further resistance. The other easy answer, mutual TLS on every single connection with no exceptions, is also wrong: real overhead, real certificate-lifecycle burden, on traffic that doesn't need it — exactly the kind of complexity that gets quietly disabled under pressure, which is worse than never building it.

This fleet runs four trust boundaries, each calibrated to who's actually on the other end — public internet traffic, human browsers on personal devices, machine-provisioned containers, and non-HTTP infra traffic each get a different control, matched to their real risk, not a blanket policy applied everywhere or nowhere. Enforced by three independent routing authorities instead of one central router, specifically so a failure or misconfiguration in any single one only ever takes out its own third of the traffic — never all of it at once. It didn't start this way, either: the public proxy used to be its own VM, and the internal service-to-service model used to be a flat, unauthenticated mesh with no boundary at all. Both got replaced deliberately, trading operational simplicity for a smaller blast radius.

Full breakdown — the boundary-by-boundary reasoning, the routing-authority failure table, all 8 traffic laws: docs/architecture-overview.md.

This repo is a sanitized showcase of the systems behind that platform — the actual engineering decisions and tradeoffs, not just a list of installed apps. Every real IP, hostname, and internal domain has been stripped or genericized throughout.

Architecture at a glance

flowchart LR
    Internet(["Internet"]) --> Edge["Edge Firewall"]
    Edge --> LAN["LAN"]

    LAN --> DNS["Internal DNS<br/>two zones"]
    LAN --> Vault["Vault<br/>PKI + secrets"]
    DNS --> Vault

    Vault --> Apps["App Host<br/>41 stacks"]
    Vault --> Automation["Automation VM<br/>Git + orchestration"]
    LAN --> FileServer["File Server"]
    DNS --> FileServer
    LAN --> Automation

    Apps --> InternalProxy["Internal Proxy Zone"]
    Apps --> SSO["SSO"]

    Vault --> EdgeProxy["User-Facing Proxy<br/>mTLS"]
    DNS --> EdgeProxy
    Apps -.proxied.-> EdgeProxy
    SSO -.auth.-> EdgeProxy

    Apps -.-> Monitoring["Monitoring"]
    Automation -.-> Monitoring
    FileServer -.-> Monitoring
    EdgeProxy -.-> Monitoring

    Apps -.-> SecMon["Security Monitoring"]
    Automation -.-> SecMon
    FileServer -.-> SecMon
    EdgeProxy -.-> SecMon

    classDef edge fill:#f8cecc,stroke:#b85450,color:#333
    classDef core fill:#dae8fc,stroke:#6c8ebf,color:#333
    classDef obs fill:#fff2cc,stroke:#d6b656,color:#333
    class Internet,Edge edge
    class LAN,DNS,Vault,Apps,Automation,FileServer,InternalProxy,SSO,EdgeProxy core
    class Monitoring,SecMon obs

Solid arrows are real dependencies; dotted arrows are asynchronous telemetry reporting, not a blocking dependency. Full breakdown of all 8 traffic laws (ingress, east-west, secrets, DNS, observability, notifications, CI/CD, management) with the actual trust-boundary reasoning: docs/architecture-overview.md.

Every system, at a glance

System The core decision Doc
🔐 Secrets & PKI One Vault cluster as CA + secret store, zero plaintext secrets in git →
🔑 Secret Distribution Four distinct flows, each with its own failure contract →
🪪 SSO & Identity Dual-zone SSO registration, header-based fallback for a real LDAP limitation →
🧱 Firewall & Network Zero blanket-allow rules — every flow explicit, everything else dropped →
🌐 DNS Two zones decide which trust boundary a request enters →
📈 Observability Push-based metrics, chosen specifically to fit the firewall model →
🛡️ Security Monitoring Curated alert runbook + justified compliance exceptions →
🔁 Orchestration 13 flows, check → pause → human approval → apply →
🔔 Notifications Two fan-in branches, one shared formatter, zero runtime coupling →
📦 Registry & Build Self-hosted registry, digest-checked before any rebuild →
🖥️ Dashy SOC Board 14 data sources aggregated into one glanceable board →
⏱️ Cron Scheduler Fleet-wide job inventory, weighted load heatmap →
📝 Content Pipeline Content tracker → automation → blog; social fan-out still incomplete →
🏗️ Infrastructure as Code Packer → Terraform → Ansible, one tag threading all three →
🧯 Disaster Recovery 3 independent recovery layers, each covering a different kind of loss →
📐 Architecture Overview 4 trust boundaries, 3 routing authorities, calibrated not maxed →

Systems

🔐 Secrets & PKI

Every secret in this fleet used to sit in plaintext inside git-tracked .env files — readable by anyone with repo access, permanent in git history even after being "fixed." Today the real value of any secret lives in exactly one place, rendered locally to disk by an agent that authenticates itself, and nothing else ever calls the vault directly. Even the platform's own biggest risk — a bootstrap token that never expires — is ranked #1 of 3 known gaps, not left unranked. → docs/secrets-and-pki.md

🔑 Secret Distribution Model

A certificate expiring is a slow problem measured in days. A missing application secret is often an outage measured in seconds. Treating those two the same was the mistake avoided here: four separate delivery contracts instead of one universal pipeline, each built around how badly its own specific failure actually hurts. → docs/secret-distribution.md

🪪 SSO & Identity

Authentik, self-hosted, fronts single sign-on for every app in the fleet that supports it — a dozen-plus apps on native OIDC where the upstream software allows it, everything else behind proxy-based forward-auth so apps that were never built with SSO in mind still get it. Every app registers twice, once per reverse-proxy zone, because authentication always happens on the internal zone even when the app itself lives on the browser-facing one. Git automation follows the same least-privilege instinct: a scoped bot account with write access replaces routing CI through an admin identity, so a leaked automation credential is never an admin credential. → docs/sso-and-identity.md

🧱 Firewall & Network Segmentation

Zero blanket internal trust — every real service-to-service flow earns its own explicit rule, everything else gets logged and dropped. Even that model has a named limit worth knowing: container traffic slips past the host firewall entirely, so the real protection on any given host is narrower than the ruleset alone would suggest. → docs/firewall-and-network.md

🌐 DNS

Two internal zones exist for one reason: which zone a hostname resolves in decides whether a request gets treated as a human on a browser or a machine calling another machine. Get that resolution wrong and the wrong trust boundary applies to that request — not a naming detail, the actual security-relevant part of the whole model. → docs/dns-naming.md

📈 Observability

Metrics get pushed outward, never pulled — a call made specifically so the monitoring stack never needs inbound access punched through the firewall model above to every single host it watches. The collection agent runs natively on every host, not in a container, installed by a role that keeps re-checking itself rather than trusting it stayed correct after day one. → docs/observability.md

🛡️ Security Monitoring

Routine alerts get root-caused exactly once and never re-investigated from scratch again — including one case where the alert itself turned out to be the bug, not the system it was checking. Compliance hardening applies fleet-wide with named, reasoned exceptions instead of blanket enforcement, and the benchmarking tool runs as a native binary after its own official container image hit a real compatibility wall. → docs/security-monitoring.md

🔁 Orchestration

Nothing changes on this fleet automatically. Every update starts as a dry run, reports exactly what it would do, and then sits paused — sometimes for days — until a human actually approves it. Thirteen flows run this way, and one genuinely hard edge case (the automation engine updating the very node it runs on) gets handled with a fire-and-forget path built specifically for that one scenario. → docs/orchestration.md

🔔 Notifications

Every alert from every source — apps, hosts, security events — comes out formatted identically, because all of it funnels through one shared step before reaching Slack. That includes the debt: one decommissioned app's routing code is still sitting in the flow unswept, and a credential that should live in exactly one place is actually kept in sync by hand across three. → docs/notifications.md

📦 Self-Hosted Registry & Build Pipeline

Runs on the Git server's own built-in registry — no separate service to operate, no runtime dependency on Docker Hub. A cheap digest check runs before every possible rebuild, so a week where nothing upstream actually changed costs almost nothing to verify. → docs/registry-and-build.md

🖥️ Dashy SOC Board

Fourteen different systems — alerts, logs, metrics, CI health, secrets state, DNS, backups — used to mean fourteen different tabs to check. One custom-built dashboard joins all of it into a single board, with acknowledgement state that lives entirely in the browser: nothing to host, nothing to secure, nothing to back up. → docs/dashy-soc-board.md

⏱️ Cron Scheduler

Every scheduled job across the fleet lives in one inventory, rendered as a load heatmap weighted by actual resource impact — a backup job counts for more than a lightweight health check, because an overlapping backup is a worse collision than two quiet jobs sharing an hour. → docs/cron-scheduler.md

📝 Content Pipeline

The one system in this repo that isn't finished — included anyway, because a portfolio of only-finished systems doesn't show how anything actually gets built. A content tracker, an automation glue layer, and a blog already work end to end; the social-publishing leg is still being wired up. → docs/content-pipeline.md

🏗️ Infrastructure as Code

Image builds, provisioning, and configuration stay as three separate tools with three separate rhythms — an image gets rebuilt rarely, a VM gets provisioned once, config gets re-applied continuously — connected by one deliberate handoff instead of one tool trying to do all three jobs at once. → docs/iac-pipeline.md

🧯 Disaster Recovery & Upgrades

"A lost host" and "a database's lost data" are different kinds of loss needing different answers, so three independent recovery layers each cover one — with one real gap between them: a storage pool covered by neither. OS and application upgrades stay deliberately decoupled too: batching them together would let one host's expected no-op silently abort every other host's run. → docs/disaster-recovery.md

What's running

~55 Docker Compose stacks, grouped by purpose rather than by which host happens to run them — see stacks/: identity & security, platform/infra, media, productivity, and dev tools.


About me

Self-taught, building toward a career in IT/DevOps — everything in this repo is something I learned by doing, from reading docs to breaking my own lab and fixing it again.

LinkedIn

Website