Harden public showcase privacy controls
Some checks failed
public-safety / privacy-and-secret-scan (push) Has been cancelled

This commit is contained in:
fizzlepoof
2026-09-08 22:04:38 +00:00
parent cc35523888
commit 3e0975af21
9 changed files with 97 additions and 159 deletions

View File

@@ -11,14 +11,17 @@ jobs:
privacy-and-secret-scan: privacy-and-secret-scan:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Check public-safe content - name: Check public-safe content
run: python3 scripts/check-public-safety.py 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 - name: Scan Git history for secrets
uses: gitleaks/gitleaks-action@v2 uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -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 | | **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 | | **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 ## Design highlights
@@ -45,7 +45,7 @@ docs/
networking.md Segmentation and access-control model networking.md Segmentation and access-control model
operations.md Deployment, verification, backup, and rollback patterns operations.md Deployment, verification, backup, and rollback patterns
security.md Public/private boundary and secret-handling rules 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/ examples/
compose/ Sanitized Compose and environment examples compose/ Sanitized Compose and environment examples
scripts/ scripts/
@@ -62,7 +62,7 @@ scripts/
## Public-safety policy ## Public-safety policy
Every commit is checked for: Every commit is checked with a repository-specific privacy scanner for:
- private or management addresses - private or management addresses
- MAC addresses and device identifiers - MAC addresses and device identifiers
@@ -70,7 +70,7 @@ Every commit is checked for:
- private-key material, tokens, passwords, and authorization headers - private-key material, tokens, passwords, and authorization headers
- real `.env` files and key-bearing file types - 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 ## Scope

View File

@@ -9,7 +9,7 @@ This repository contains public-safe architecture documentation and sanitized ex
If you find sensitive information in this repository: If you find sensitive information in this repository:
1. Do not quote it in a public issue. 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. 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. 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.

View File

@@ -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 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 ### Internal application request

View File

@@ -31,9 +31,9 @@ Removing a value from the latest commit does not remove it from Git history.
## Repository controls ## 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. - Gitleaks scans complete Git history in CI.
The scanner is deliberately conservative. Placeholder examples should use unmistakable values such as: The scanner is deliberately conservative. Placeholder examples should use unmistakable values such as:
@@ -64,4 +64,4 @@ https://service.example.net
## Reporting ## 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.

View File

@@ -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 ## Primary application host
### Core platform The primary host concentrates the shared application plane:
- TrueNAS SCALE - storage-aware container runtime
- Docker application runtime - internal reverse proxy and authenticated tunnel connector
- shared PostgreSQL, MariaDB, and Redis - identity and SSO
- Traefik internal reverse proxy - shared PostgreSQL, MariaDB, and Redis data services
- Newt tunnel connector - source hosting and container administration
- Tailscale private-overlay connector - household productivity, document, photo, and media front ends
- Authentik identity and SSO - monitoring, metrics, logs, dashboards, uptime checks, and notifications
- Authelia retained for limited legacy migration paths - workflow automation
- Headscale and Headplane pilot control plane - always-on local model serving, model routing, vector search, and metasearch
- Gitea source hosting
- Dockhand and Homepage administration interfaces
### AI and search Representative technologies include TrueNAS SCALE, Docker, Traefik, Authentik, PostgreSQL, Redis, Prometheus, Grafana, Ollama, and LiteLLM.
- Ollama always-on model serving Large datasets are mounted from the storage host rather than duplicated into container-local volumes.
- 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
## Storage and ingestion host ## Storage and ingestion host
### Data plane The storage host keeps capacity-heavy and data-local workloads together:
- ZFS-backed bulk storage - ZFS-backed bulk datasets
- application-data and media datasets - snapshots and backup targets
- snapshot and backup targets - media acquisition, organization, and post-processing
- synchronized school/document storage - file synchronization
- private remote-support rendezvous and relay
- a secondary DNS resolver
- selected CPU-only inference utilities
- host and container monitoring
### Media ingestion Representative technologies include Unraid, ZFS, the common media-automation ecosystem, Syncthing, RustDesk, Technitium DNS, and Netdata.
- 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
## Offline and resilience host ## 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 - offline reference and educational libraries
- Kiwix - local notes and browser utilities
- two Kolibri generations for retained content compatibility - local model and vector services
- CyberChef - household helper and agent services
- Flatnotes - CPU speech-to-text
- MeshCore Web - Home Assistant in a dedicated virtual machine
- local Ollama and Qdrant - 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 Managed appliance workloads on this host are excluded from broad cleanup automation and maintained through their own control plane.
- 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
## GPU operator workstation ## GPU operator workstation
- CachyOS desktop The workstation provides optional accelerated capacity and daily operator tooling:
- high-performance NVIDIA GPU compute
- optional heavy Ollama and speech workloads - high-performance local model inference
- Docker runtime - accelerated speech-to-text and other GPU jobs
- printing service - container runtime
- printing
- private-overlay access - private-overlay access
- file-synchronization client - file synchronization
- 3D-printing and slicing applications - 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 ## Low-power edge node
A small independent Linux node handles hardware-adjacent, always-on functions:
- weather collection and dispatch - weather collection and dispatch
- mesh-radio/repeater support - mesh-radio and repeater support
- wired server-lane placement - wired placement on a server-oriented network segment
- low-power always-on operation - upstream reporting without joining the primary application failure domain
## Placement rules ## Placement rules
1. Shared applications and databases prefer the primary application host. 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. 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. 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. 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.

View File

@@ -1,11 +1,11 @@
services: services:
database: database:
image: postgres:17-alpine image: postgres:17.11-alpine3.24@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_DB: ${DATABASE_NAME} POSTGRES_DB: ${DATABASE_NAME:?set DATABASE_NAME}
POSTGRES_USER: ${DATABASE_USER} POSTGRES_USER: ${DATABASE_USER:?set DATABASE_USER}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD} POSTGRES_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD}
volumes: volumes:
- database-data:/var/lib/postgresql/data - database-data:/var/lib/postgresql/data
networks: networks:
@@ -17,13 +17,9 @@ services:
retries: 5 retries: 5
application: application:
image: traefik/whoami:v1.11 image: traefik/whoami:v1.11.0@sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab
restart: unless-stopped restart: unless-stopped
depends_on:
database:
condition: service_healthy
networks: networks:
- data
- ingress - ingress
expose: expose:
- "80" - "80"
@@ -34,7 +30,7 @@ networks:
data: data:
internal: true internal: true
ingress: ingress:
name: ${INGRESS_NETWORK} name: ${INGRESS_NETWORK:?set INGRESS_NETWORK}
external: true external: true
volumes: volumes:

View File

@@ -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_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) 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") 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( SECRET_ASSIGNMENT = re.compile(
r"(?i)\b(?:api[_-]?(?:key|token)|access[_-]?token|auth[_-]?token|token|password|passwd|" r"(?i)\b(?:api[_-]?(?:key|token)|access[_-]?token|auth[_-]?token|token|password|passwd|"
r"client[_-]?secret|cookie|private[_-]?key)\b\s*[:=]\s*[\"']?([^\s\"',}]+)" 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") OPAQUE = re.compile(r"\b[A-Za-z0-9_+/=-]{32,}\b")
RISKY_SUFFIXES = { RISKY_SUFFIXES = {
".pem", ".key", ".p12", ".pfx", ".kdbx", ".ovpn", ".mobileconfig" ".pem", ".key", ".p12", ".pfx", ".kdbx", ".ovpn", ".mobileconfig",
".db", ".sqlite", ".sqlite3", ".dump", ".pcap", ".pcapng",
} }
RISKY_NAMES = { RISKY_NAMES = {
".env", "id_rsa", "id_ed25519", "id_ecdsa", "id_dsa", "wg0.conf" ".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]: def tracked_files() -> list[Path]:
@@ -88,6 +92,8 @@ def main() -> int:
findings.add(f"{relative}: prohibited key/config suffix") findings.add(f"{relative}: prohibited key/config suffix")
if re.fullmatch(r"wg\d+\.conf", name): if re.fullmatch(r"wg\d+\.conf", name):
findings.add(f"{relative}: prohibited VPN configuration filename") 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: try:
data = path.read_bytes() data = path.read_bytes()
@@ -95,6 +101,7 @@ def main() -> int:
findings.add(f"{relative}: unreadable: {exc}") findings.add(f"{relative}: unreadable: {exc}")
continue continue
if b"\0" in data[:4096]: if b"\0" in data[:4096]:
findings.add(f"{relative}: binary tracked file requires manual review")
continue continue
for line_number, line in enumerate(data.decode("utf-8", errors="ignore").splitlines(), 1): 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")): if not domain.endswith(("example.com", "example.net", "example.org")):
findings.add(f"{prefix}: non-example email address") 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): for match in SECRET_ASSIGNMENT.finditer(line):
value = match.group(1) value = match.group(1)
if not is_placeholder(value): if not is_placeholder(value):
@@ -134,12 +148,13 @@ def main() -> int:
if not is_placeholder(value): if not is_placeholder(value):
findings.add(f"{prefix}: authorization material") 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): if is_placeholder(candidate):
continue continue
# Commit hashes and content digests are still identifiers; require a label. # Commit hashes and content digests are still identifiers; require a label.
labelled_hash = re.search( 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, line,
) )
if labelled_hash and re.fullmatch(r"[0-9a-fA-F]{32,128}", candidate): if labelled_hash and re.fullmatch(r"[0-9a-fA-F]{32,128}", candidate):
@@ -154,7 +169,11 @@ def main() -> int:
print(f"- {finding}") print(f"- {finding}")
return 1 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 return 0