From 3e0975af21e9b89893198e75ebda7f934e839594 Mon Sep 17 00:00:00 2001 From: fizzlepoof <251490314+fizzlepoof@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:04:38 +0000 Subject: [PATCH] Harden public showcase privacy controls --- .github/workflows/public-safety.yml | 7 +- LICENSE | 2 +- README.md | 8 +- SECURITY.md | 2 +- docs/architecture.md | 2 +- docs/security.md | 6 +- docs/services.md | 186 ++++++++-------------------- examples/compose/docker-compose.yml | 16 +-- scripts/check-public-safety.py | 27 +++- 9 files changed, 97 insertions(+), 159 deletions(-) diff --git a/.github/workflows/public-safety.yml b/.github/workflows/public-safety.yml index 4ce167b..b3b6fd7 100644 --- a/.github/workflows/public-safety.yml +++ b/.github/workflows/public-safety.yml @@ -11,14 +11,17 @@ jobs: privacy-and-secret-scan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 - name: Check public-safe content run: python3 scripts/check-public-safety.py + - name: Compile detector source + run: python3 -m py_compile scripts/check-public-safety.py + - name: Scan Git history for secrets - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/LICENSE b/LICENSE index 1b76822..efcd22d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 fizzlepoof +Copyright (c) 2026 Homelab Showcase contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 44a1c64..d164153 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A public-safe overview of a real multi-host homelab built around segmented netwo | **GPU workstation** | CachyOS workstation | Operator workstation and opportunistic high-performance GPU compute | | **Edge node** | Low-power Linux node | Weather dispatch and mesh-radio edge services | -See [Architecture](docs/architecture.md) for the data flow and [Services](docs/services.md) for the current service placement. +See [Architecture](docs/architecture.md) for the data flow and [Services](docs/services.md) for the representative service design by role. ## Design highlights @@ -45,7 +45,7 @@ docs/ networking.md Segmentation and access-control model operations.md Deployment, verification, backup, and rollback patterns security.md Public/private boundary and secret-handling rules - services.md Current service placement by host + services.md Representative service design by host role examples/ compose/ Sanitized Compose and environment examples scripts/ @@ -62,7 +62,7 @@ scripts/ ## Public-safety policy -Every commit is checked for: +Every commit is checked with a repository-specific privacy scanner for: - private or management addresses - MAC addresses and device identifiers @@ -70,7 +70,7 @@ Every commit is checked for: - private-key material, tokens, passwords, and authorization headers - real `.env` files and key-bearing file types -Gitleaks also scans the complete Git history in CI. See [Security](docs/security.md). +The detector source is executed and syntax-checked rather than matched against its own regex literals. Binary files are rejected for manual review. Gitleaks separately scans the complete Git history in CI. See [Security](docs/security.md). ## Scope diff --git a/SECURITY.md b/SECURITY.md index 9f8bae6..6b63971 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,7 +9,7 @@ This repository contains public-safe architecture documentation and sanitized ex If you find sensitive information in this repository: 1. Do not quote it in a public issue. -2. Contact the repository owner through an already established private channel. +2. Use [GitHub private vulnerability reporting](https://github.com/fizzlepoof/homelab-showcase/security/advisories/new), or contact the repository owner through an already established private channel. 3. Include only the affected path and the type of exposure until the value has been revoked. A confirmed credential exposure is handled by rotating the credential first, removing it from current files, rewriting affected Git history, and verifying a fresh clone. diff --git a/docs/architecture.md b/docs/architecture.md index ca98566..87f3691 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -110,7 +110,7 @@ The edge node handles always-on weather and mesh-radio functions close to the at client → tunnel gateway → internal reverse proxy → SSO policy → application ``` -The tunnel endpoint is the only public routing layer. Application containers do not require direct inbound router exposure. +The tunnel endpoint is the primary public routing layer for HTTP applications. Application containers do not require direct inbound router exposure; narrowly scoped non-HTTP exceptions are documented and isolated separately. ### Internal application request diff --git a/docs/security.md b/docs/security.md index 7ab4c7b..585e3b2 100644 --- a/docs/security.md +++ b/docs/security.md @@ -31,9 +31,9 @@ Removing a value from the latest commit does not remove it from Git history. ## Repository controls -The public repository uses two checks: +The public repository uses layered checks: -- `scripts/check-public-safety.py` rejects infrastructure identifiers, private paths, key-like files, and common secret patterns. +- `scripts/check-public-safety.py` rejects infrastructure identifiers, private paths, key-like files, binary files, raw-export filenames, non-approved URL hosts, and common secret patterns. Its source is executed and syntax-checked rather than matched against its own detector literals. - Gitleaks scans complete Git history in CI. The scanner is deliberately conservative. Placeholder examples should use unmistakable values such as: @@ -64,4 +64,4 @@ https://service.example.net ## Reporting -Do not open a public issue containing a suspected secret. Revoke it first and use a private contact channel for disclosure. +Do not open a public issue containing a suspected secret. Revoke it first, then use [GitHub private vulnerability reporting](https://github.com/fizzlepoof/homelab-showcase/security/advisories/new). The repository owner may also be contacted through an already established private channel. diff --git a/docs/services.md b/docs/services.md index 64fac3e..7381dbb 100644 --- a/docs/services.md +++ b/docs/services.md @@ -1,168 +1,88 @@ -# Current Service Placement +# Representative Service Architecture -This is a public-safe service inventory verified against the live hosts during the September 2026 refresh. Versions, ports, addresses, domains, device IDs, and internal paths are intentionally omitted. +This page is a deliberately generalized snapshot of the lab's service design, informed by read-only host, container, storage, and service inspection on 2026-09-08. It is not a complete production inventory. Exact products are included only when they illustrate a reusable pattern; precise placement, versions, counts, ports, addresses, domains, and runtime identifiers remain private. ## Primary application host -### Core platform +The primary host concentrates the shared application plane: -- TrueNAS SCALE -- Docker application runtime -- shared PostgreSQL, MariaDB, and Redis -- Traefik internal reverse proxy -- Newt tunnel connector -- Tailscale private-overlay connector -- Authentik identity and SSO -- Authelia retained for limited legacy migration paths -- Headscale and Headplane pilot control plane -- Gitea source hosting -- Dockhand and Homepage administration interfaces +- storage-aware container runtime +- internal reverse proxy and authenticated tunnel connector +- identity and SSO +- shared PostgreSQL, MariaDB, and Redis data services +- source hosting and container administration +- household productivity, document, photo, and media front ends +- monitoring, metrics, logs, dashboards, uptime checks, and notifications +- workflow automation +- always-on local model serving, model routing, vector search, and metasearch -### AI and search +Representative technologies include TrueNAS SCALE, Docker, Traefik, Authentik, PostgreSQL, Redis, Prometheus, Grafana, Ollama, and LiteLLM. -- Ollama always-on model serving -- LiteLLM model gateway -- Open WebUI -- Qdrant vector database -- SearXNG metasearch -- Firecrawl API, browser worker, queue, and supporting data services -- a VPN-isolated search egress path - -### Media and libraries - -- Plex -- Tautulli -- Audiobookshelf -- Calibre Web Automated -- Seerr -- Immich application and machine-learning services -- RomM -- GameVault -- Dispatcharr - -Large libraries remain on the storage host and are mounted by the application host. - -### Productivity and household applications - -- Paperless-ngx with document conversion and extraction sidecars -- Karakeep with browser and search sidecars -- n8n -- KitchenOwl -- Donetick and a dashboard bridge -- Shlink and its web client -- Qui -- Scholarsome -- Doris Barbell -- Kima Hub - -### Monitoring and notification - -- Grafana -- Prometheus -- Loki -- Promtail -- cAdvisor -- node exporter -- Netdata -- Uptime Kuma -- Gotify - -### Radio visibility - -- MeshMonitor -- local map-tile service +Large datasets are mounted from the storage host rather than duplicated into container-local volumes. ## Storage and ingestion host -### Data plane +The storage host keeps capacity-heavy and data-local workloads together: -- ZFS-backed bulk storage -- application-data and media datasets -- snapshot and backup targets -- synchronized school/document storage +- ZFS-backed bulk datasets +- snapshots and backup targets +- media acquisition, organization, and post-processing +- file synchronization +- private remote-support rendezvous and relay +- a secondary DNS resolver +- selected CPU-only inference utilities +- host and container monitoring -### Media ingestion - -- Sonarr, including a separate anime workflow -- Radarr -- Lidarr -- Readarr variants -- Prowlarr -- Bazarr -- qBittorrent through a VPN gateway -- qbit_manage -- Unpackerr -- Autobrr -- Notifiarr -- Shelfmark - -### Supporting services - -- Syncthing hub -- RustDesk rendezvous and relay -- CPU text reranker -- secondary Technitium DNS replica -- Netdata -- Hawser -- Newt connector +Representative technologies include Unraid, ZFS, the common media-automation ecosystem, Syncthing, RustDesk, Technitium DNS, and Netdata. ## Offline and resilience host -### Offline knowledge platform +The resilience host stays useful when the primary application host or WAN is unavailable: -- Project N.O.M.A.D. administration layer -- Kiwix -- two Kolibri generations for retained content compatibility -- CyberChef -- Flatnotes -- MeshCore Web -- local Ollama and Qdrant +- offline reference and educational libraries +- local notes and browser utilities +- local model and vector services +- household helper and agent services +- CPU speech-to-text +- Home Assistant in a dedicated virtual machine +- game-server management +- mesh-radio observation +- a secondary DNS resolver +- trusted local file intake -Project N.O.M.A.D.-managed containers are treated as an appliance layer and are not manually rebuilt by general maintenance automation. +Representative technologies include Ubuntu Server, KVM, Home Assistant OS, Kiwix, Kolibri, Ollama, Qdrant, and Docker. -### Local agents and household tools - -- Honcho API, deriver, PostgreSQL/Vector database, and Redis -- Doris Schoolhouse -- Doris Kitchen -- CPU Whisper service -- LocalSend trusted intake -- Hawser -- node exporter - -### Resilience and hardware-adjacent services - -- Home Assistant OS in a KVM virtual machine with a dedicated Zigbee radio -- secondary Technitium DNS replica -- MeshCore companion observer as a system service -- Pelican/Wings game-server management -- Newt tunnel connector +Managed appliance workloads on this host are excluded from broad cleanup automation and maintained through their own control plane. ## GPU operator workstation -- CachyOS desktop -- high-performance NVIDIA GPU compute -- optional heavy Ollama and speech workloads -- Docker runtime -- printing service +The workstation provides optional accelerated capacity and daily operator tooling: + +- high-performance local model inference +- accelerated speech-to-text and other GPU jobs +- container runtime +- printing - private-overlay access -- file-synchronization client +- file synchronization - 3D-printing and slicing applications -The workstation is intentionally opportunistic capacity. Background services must continue when it is asleep or offline. +The workstation is opportunistic capacity. Routine background services must continue when it sleeps or is offline. ## Low-power edge node +A small independent Linux node handles hardware-adjacent, always-on functions: + - weather collection and dispatch -- mesh-radio/repeater support -- wired server-lane placement -- low-power always-on operation +- mesh-radio and repeater support +- wired placement on a server-oriented network segment +- upstream reporting without joining the primary application failure domain ## Placement rules 1. Shared applications and databases prefer the primary application host. -2. Large data and media ingestion stay close to bulk storage. +2. Large data and ingestion stay close to bulk storage. 3. Offline knowledge and resilience services remain independent of the primary host. 4. Heavy GPU work may use the workstation, but routine automation cannot require it. 5. Hardware-adjacent radio and weather services stay on low-power edge nodes. -6. DNS, file synchronization, and remote support span hosts to avoid a single failure domain. +6. DNS, file synchronization, and remote support span hosts to reduce single points of failure. +7. The complete service inventory, operational exceptions, and incident state remain in the private source-of-truth repository. diff --git a/examples/compose/docker-compose.yml b/examples/compose/docker-compose.yml index 4dbe0c3..e1e9401 100644 --- a/examples/compose/docker-compose.yml +++ b/examples/compose/docker-compose.yml @@ -1,11 +1,11 @@ services: database: - image: postgres:17-alpine + image: postgres:17.11-alpine3.24@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73 restart: unless-stopped environment: - POSTGRES_DB: ${DATABASE_NAME} - POSTGRES_USER: ${DATABASE_USER} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME:?set DATABASE_NAME} + POSTGRES_USER: ${DATABASE_USER:?set DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD} volumes: - database-data:/var/lib/postgresql/data networks: @@ -17,13 +17,9 @@ services: retries: 5 application: - image: traefik/whoami:v1.11 + image: traefik/whoami:v1.11.0@sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab restart: unless-stopped - depends_on: - database: - condition: service_healthy networks: - - data - ingress expose: - "80" @@ -34,7 +30,7 @@ networks: data: internal: true ingress: - name: ${INGRESS_NETWORK} + name: ${INGRESS_NETWORK:?set INGRESS_NETWORK} external: true volumes: diff --git a/scripts/check-public-safety.py b/scripts/check-public-safety.py index 2e63fcf..ef10c5d 100755 --- a/scripts/check-public-safety.py +++ b/scripts/check-public-safety.py @@ -33,6 +33,7 @@ PRIVATE_MOUNT = re.compile(r"(?i)/mnt/(?!storage\b|media\b|backups\b|example\b)[ PRIVATE_OPT = re.compile(r"(?i)/opt/(?!example\b|application\b)[a-z0-9._-]+\b") PRIVATE_KEY = re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----", re.I) EMAIL = re.compile(r"(?i)\b[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z]{2,})\b") +URL_HOST = re.compile(r"(?i)https?://([a-z0-9.-]+)") SECRET_ASSIGNMENT = re.compile( r"(?i)\b(?:api[_-]?(?:key|token)|access[_-]?token|auth[_-]?token|token|password|passwd|" r"client[_-]?secret|cookie|private[_-]?key)\b\s*[:=]\s*[\"']?([^\s\"',}]+)" @@ -43,11 +44,14 @@ AUTHORIZATION = re.compile( OPAQUE = re.compile(r"\b[A-Za-z0-9_+/=-]{32,}\b") RISKY_SUFFIXES = { - ".pem", ".key", ".p12", ".pfx", ".kdbx", ".ovpn", ".mobileconfig" + ".pem", ".key", ".p12", ".pfx", ".kdbx", ".ovpn", ".mobileconfig", + ".db", ".sqlite", ".sqlite3", ".dump", ".pcap", ".pcapng", } RISKY_NAMES = { ".env", "id_rsa", "id_ed25519", "id_ecdsa", "id_dsa", "wg0.conf" } +RAW_EXPORT_NAME = re.compile(r"(?i)(?:^|[-_.])(?:backup|dump|export|baseline|snapshot)(?:[-_.]|$)") +ALLOWED_URL_HOSTS = {"github.com", "service.example.net"} def tracked_files() -> list[Path]: @@ -88,6 +92,8 @@ def main() -> int: findings.add(f"{relative}: prohibited key/config suffix") if re.fullmatch(r"wg\d+\.conf", name): findings.add(f"{relative}: prohibited VPN configuration filename") + if RAW_EXPORT_NAME.search(name): + findings.add(f"{relative}: raw export/backup-style filename requires review") try: data = path.read_bytes() @@ -95,6 +101,7 @@ def main() -> int: findings.add(f"{relative}: unreadable: {exc}") continue if b"\0" in data[:4096]: + findings.add(f"{relative}: binary tracked file requires manual review") continue for line_number, line in enumerate(data.decode("utf-8", errors="ignore").splitlines(), 1): @@ -124,6 +131,13 @@ def main() -> int: if not domain.endswith(("example.com", "example.net", "example.org")): findings.add(f"{prefix}: non-example email address") + for match in URL_HOST.finditer(line): + host = match.group(1).lower() + if host not in ALLOWED_URL_HOSTS and not host.endswith( + (".example.com", ".example.net", ".example.org") + ): + findings.add(f"{prefix}: non-approved URL host") + for match in SECRET_ASSIGNMENT.finditer(line): value = match.group(1) if not is_placeholder(value): @@ -134,12 +148,13 @@ def main() -> int: if not is_placeholder(value): findings.add(f"{prefix}: authorization material") - for candidate in OPAQUE.findall(line): + line_without_urls = re.sub(r"https?://\S+", "", line) + for candidate in OPAQUE.findall(line_without_urls): if is_placeholder(candidate): continue # Commit hashes and content digests are still identifiers; require a label. labelled_hash = re.search( - r"(?i)\b(?:sha(?:1|256|512)|digest|commit|checksum|example[_-]?hash)\b", + r"(?i)(?:\b(?:sha(?:1|256|512)|digest|commit|checksum|example[_-]?hash)\b|\buses\s*:)", line, ) if labelled_hash and re.fullmatch(r"[0-9a-fA-F]{32,128}", candidate): @@ -154,7 +169,11 @@ def main() -> int: print(f"- {finding}") return 1 - print(f"Public-safety scan passed: {len(files)} tracked files checked") + content_count = len(files) - int(any(path.relative_to(ROOT).as_posix() == SELF for path in files)) + print( + "Public-safety scan passed: " + f"{content_count} tracked files content-scanned; detector source executed separately" + ) return 0