automation-bot e74e04955a docs: fix iac-pipeline.md drift (provisioning/compliance role counts, templates)
Partial fix - diagram update still pending (Nextcloud unavailable).

Live-verified against roles_metadata.yml: provisioning stage now has
2 roles (clamav, netbird_agent - added for NetBird mesh enrollment),
compliance stage now has 16 (added ssh_ca_client). Technitium's shown
tag-file example was stale (21->22 tags, missing netbird_agent).
containers.tf gained an 18-line header comment that pushed its actual
resource block past line 20, so the doc's 'head -20' example no longer
reproduced the shown output - fixed to a targeted sed range. Also
found and documented a genuine live inconsistency: host_tags.yml.tpl
gained an inventory_group field with the 2026-08 LXC rollout, but no
existing VM host has been re-applied since, so none of their rendered
files have it yet (only new LXCs do) - called out explicitly rather
than papered over.
2026-09-19 09:35:10 +03:00
2026-08-24 21:27:11 +03:00
2026-06-09 10:20:07 +03:00

About Me

👋 Hi there — I'm @thesabear

LinkedIn Website

🖥️
12
fleet hosts
🐳
44
compose stacks
📚
16
documented systems
🧱
4
trust boundaries
🔁
13
automated maintenance flows

I'm a curious, self-taught technologist who runs a real, multi-host homelab as a production-grade playground. 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, run at homelab scale, for real, 24/7, by one person.

My long-term goal is to build a career in IT — specifically in environments where learning never stops and innovation is part of the culture. What started as a hobby project has grown into a full-on journey into DevOps, automation, infrastructure, and security;

  • Proxmox — a hypervisor cluster running a fleet of hardened VMs/LXCs provisioned via Terraform
  • Terraform + Packer — for bootstrapping new hosts for the fleet, from hardened, minimal images and continuously re-hardened by Ansible
  • HashiCorp Vault — issuing PKI certs and managing every application secret
  • Caddy + Authentik — a two-zone reverse-proxy design fronted by SSO, behind pfSense and firewalld for real network segmentation
  • Kestra — fleet orchestration with scheduled flows backed by Ansible roles and playbooks
  • NetBird — a self-hosted zero-trust WireGuard mesh, every host and device with its own identity, deny-by-default policy instead of trusting LAN position
  • Gitea — a self-hosted registry building and shipping container images for every stack that's opted in
  • Wazuh + Dashy — a self-built SOC watching all of it

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.

Why Four Trust Boundaries, Not One

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.

Trust Boundaries and Blast Radius

The three routing authorities are also split across two separate physical nodes, so a hardware failure is contained the same way a routing failure is — never all three at once.

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

Zero Trust, Actually Implemented

Zero trust is where enterprise security has been heading for years now — NIST formalized it as a reference architecture, and "trusted because it's on the LAN" has been steadily losing ground everywhere as the default posture. The core idea is simple to state and hard to actually build: nothing gets access because of where it's connecting from, only because of what it's proven itself to be. Network position stops being a substitute for identity, and every single request gets checked against explicit policy instead of inheriting trust from whatever segment it happened to arrive on.

This fleet doesn't just talk about that model — the four trust boundaries above are one expression of it, and remote/roaming access is a second, separate one, since "how do you reach the fleet from outside it" is a genuinely different problem from "how do services inside it talk to each other." That second problem is solved with a self-hosted zero-trust mesh (NetBird, WireGuard-based): every host and every device gets its own cryptographic identity, access is granted per-identity through explicit policy, and the mesh's own built-in allow-everything default is disabled — nothing crosses it unless a rule says so.

Perimeter model versus zero-trust model: on the left, anything inside one shared boundary can reach anything else inside it; on the right, every device has its own identity and only an explicit policy grants access, everything else blocked regardless of network position

Full breakdown — the mesh-vs-VPN reasoning, the complete policy model (groups, scoped keys, peer approval, posture checks), the real bring-up story, and live evidence straight from the running system: docs/netbird-zero-trust.md.

Architecture At a Glance

Architecture Overview Solid arrows are hard dependencies — the target has to be reachable for the source to function. Dashed arrows are secondary relationships (request routing like "proxies"/"auth", or async telemetry reporting) that don't block the source if the target's unavailable. 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.

Systems

⏱️ Cron Scheduler

Every scheduled job across the fleet — orchestration flows, system timers, app-internal schedules — lives in one inventory, visualized as a load heatmap weighted by actual resource impact — a disk-heavy backup job counts for more than a quick health check, not just raw job count — so a new job's time slot can be picked without colliding with existing load. → docs/cron-scheduler.md

🖥️ Dashy SOC Board

One custom-built dashboard aggregates fourteen different data sources — security alerts, logs, metrics, CI health, secrets state, DNS, backups, firewall stats — into a single glanceable board, replacing what used to be fourteen separate tabs to check. Acknowledgement state lives entirely in the browser, no backend to host or secure. Every widget's visual shape matches the shape of its own data — a gauge for a proportion, a nested tree for parent/child hardware, a poster grid for media — instead of one generic card template stretched to fit everything. → docs/dashy-soc-board.md

🧯 Disaster Recovery & Upgrades

Three independent recovery layers cover three different kinds of loss — a lost host rebuilds from code, VM-level backups cover full-host snapshots, and a separate volume-level backup covers application data specifically. Failure-domain mapping goes down to the disk: one specific volume, if lost, would take out the boot images for two hosts at once — identified and factored into backup priority rather than discovered the hard way. OS-level and application-level upgrades run on entirely separate, deliberately decoupled schedules and safety checks. → docs/disaster-recovery.md

🌐 DNS

Two internal DNS zones split browser-facing traffic from service-to-service traffic — which zone a hostname resolves in is what decides which trust boundary a request enters, not just where it's found. Technitium runs both zones, self-hosted, with direct DNS bypass blocked at the firewall so the split is actually enforced, not just a naming convention. Not every app gets a record in both zones, either: something that's purely a backend service other apps talk to only needs the app-to-app zone, and something purely browser-facing only needs the user-facing one — each app gets exactly the DNS records its real traffic pattern uses. → docs/dns-naming.md

🧱 Firewall & Network Segmentation

No blanket internal trust anywhere on the network — every real service-to-service flow gets its own explicit rule, and everything not explicitly allowed is logged and dropped by default. Enforced at two layers: a firewall at the network edge, and firewalld locally on every host, each opening only what that specific host actually needs. A few rules go narrower still — inbound access to the secrets platform's own admin API, for instance, is gated to only the two or three services that ever legitimately need it, not the whole fleet. → docs/firewall-and-network.md

📇 Homelab Inventory

Every physical host, VM/LXC, and running service in the fleet, re-derived from the machines themselves once a week rather than hand-maintained — so the reference is structurally unable to drift out of sync with reality. A three-tier Ansible collection (hardware off the hypervisors, guests from the Proxmox APIs, listening sockets off every host) writes three linked tables, surfaced on one dashboard page. The one hand-held detail — a NAS that reports a placeholder string for its own model — is protected by a merge-only write that structurally can't overwrite it. → docs/homelab-inventory.md

🏗️ Infrastructure as Code

Packer builds a hardened base image, Terraform provisions each VM from it, and Ansible configures and continuously re-hardens every host after that — three tools, three separate responsibilities, connected by one tag that threads through all three stages instead of one tool trying to do every job. Terraform's part in that link ends at creation — it writes the tags file once, and that file then drives Ansible independently for the rest of that host's life. → docs/iac-pipeline.md

🔔 Notifications

Every alert in the fleet — from apps, from hosts, from security events — funnels through one Node-RED instance and one shared formatter before reaching Slack, so a notification reads the same regardless of which of a dozen-plus sources triggered it. A dedicated role puts every host onto the same pipeline for OS-level health, independent of any single application. One shared endpoint handles every app's webhook regardless of shape — a source field where an app provides one, structural fingerprinting of the payload where it doesn't — instead of standing up a dedicated integration per app. → docs/notifications.md

📈 Observability

Every fleet host runs a native metrics-and-logs agent that pushes outward to a central Loki/Prometheus stack, rather than waiting to be scraped — chosen specifically so monitoring never needs inbound access opened through the firewall model to every host it watches. Logs, container output, and DNS query history all land in one searchable place, and a handful of apps go further still, exposing their own internal metrics directly into the same pipeline instead of relying on host-level visibility alone. → docs/observability.md

🔁 Orchestration

Kestra is the orchestration engine that runs the fleet's scheduled maintenance — OS patching, container updates, app updates, disk cleanup, and internal KB upkeep. Most of it runs fully unattended, but updates to the secrets platform, DNS, and monitoring agent — the systems everything else depends on — wait for an explicit human approval before applying. → docs/orchestration.md

🔑 Secret Distribution Model

Three separate delivery paths move material out of Vault, purpose-built for what's actually being delivered — a short-lived certificate, an application secret, or a full config file — each with its own contract for what the rest of the fleet does if that specific path fails. Admin and tooling credentials deliberately live in a fourth, completely separate system instead — encrypted directly in git — since they're used by playbooks and operators, not consumed by a running service. → docs/secret-distribution.md

🔐 Secrets & PKI

A self-hosted Vault cluster is both the certificate authority and the secret store for every host in the fleet — every TLS certificate, every database password, every API key traces back to this one system. Nothing reaches Vault directly; a local agent on each host authenticates itself and renders what that host needs to disk. Replaced an earlier setup where secrets sat in plaintext inside git. A DNS resolver's cert once renewed out of sync and ran expired for 19 days before anyone noticed — the fix wasn't tighter monitoring, it was making that renewal step structurally unable to skip itself again. → docs/secrets-and-pki.md

🛡️ Security Monitoring

A Wazuh agent runs on every host in the fleet, continuously watching file integrity, rootkit behavior, and compliance drift against a CIS-based benchmark. A curated runbook root-causes routine alerts once so they're never re-triaged from scratch, and hardening controls apply fleet-wide with specific, reasoned exceptions rather than uniform enforcement regardless of fit — audit logging, for one, is skipped on every LXC container in the fleet because the kernel capability it needs simply isn't available inside a container, not because it was overlooked. → docs/security-monitoring.md

📦 Self-Hosted Registry & Build Pipeline

Docker images get built and hosted entirely in-fleet on the Git server's own built-in registry — no runtime dependency on Docker Hub or ghcr for the majority of stacks, which have opted in. A weekly scheduled pass checks upstream image digests before committing to a full rebuild, so nothing gets rebuilt unless something upstream actually changed. → docs/registry-and-build.md

🪪 SSO & Identity

Authentik, self-hosted, fronts single sign-on fleet-wide — native OIDC where an app supports it, proxy-based forward-auth covering everything else, so apps that were never built with SSO in mind still get it. Every OIDC login crosses reverse-proxy zones mid-handshake, and Git automation runs on a scoped bot account rather than an admin identity. → docs/sso-and-identity.md

🕸️ Zero-Trust Mesh Overlay

A self-hosted WireGuard mesh gives every fleet host and personal device its own stable identity, independent of where it physically sits — access is granted per-identity through mesh policy, deny-by-default, not inferred from LAN position. Rejected a classic VPN concentrator on purpose: the mesh's control plane only ever coordinates, real traffic flows peer-to-peer and never routes through it. → docs/netbird-zero-trust.md

What's Running

44 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.


Scripts & Tooling

A few standalone scripts live in projects/ alongside the Docker stacks — real tooling, not demo code:

  • Ansible run-menu — an interactive playbook runner, the same pattern used identically across both the fleet-config and infra-provisioning repos, so running anything doesn't mean remembering exact flags.
  • cuesplit — a small FLAC-splitting utility, frame-accurate against a .cue sheet.

👀 What I Have Explored

I use my homelab as a real-world playground to deepen my understanding of core IT concepts. Some of the areas I've learned and put into practice are:

  • Infrastructure-as-Code: infrastructure that's provisioned, hardened, and reproducible from code — not clicked together by hand
  • Security: defense-in-depth in practice — network segmentation, secrets management, and identity, layered so no single control is ever the only thing standing between an attacker and everything else
  • Automation: fleet-wide operations running end-to-end on its own, with a human kept in the loop exactly where the consequences of getting it wrong are highest
  • Monitoring: observability and security detection working as one system, not two separate concerns bolted together

Each project started as something I didn't know how to do. Now it's just part of how this fleet runs — and pushes me further toward being a well-rounded IT/DevOps engineer.


🌱 What I'm Focused On Right Now

  • Refining the Ansible roles and playbooks that keep the fleet patched and consistent
  • Looking for genuine opportunities to put my Gitea CI/CD to work beyond hosting the registry, wherever it'd actually help
  • Sharpening my scripting — Bash and Python, aimed at real-world IT skills I can use beyond this homelab

⚡ Fun Fact

I'm entirely self-taught. Every tool, trick, concept, and workflow in my repo's is something I learned by doing — from reading docs to breaking my lab at 2 a.m. and fixing it again before anyone noticed anything was ever wrong. There's no ticket queue here and no one to escalate to — when something breaks at midnight, I'm the on-call engineer, the incident commander, and the entire post-mortem team arguing with myself about whose fault it was (mine, always mine). I enjoy understanding not just how something works, but how to automate it, secure it, and integrate it cleanly into a larger system — "just add one more service" remains a permanent, unsolved architectural risk.

S
Description
No description provided
Readme MIT
16 MiB
0 Stars 1 Watchers 0 Forks
Languages
Shell 56.8%
Python 37.1%
Dockerfile 6.1%