Harden public showcase privacy controls
Some checks failed
public-safety / privacy-and-secret-scan (push) Has been cancelled
Some checks failed
public-safety / privacy-and-secret-scan (push) Has been cancelled
This commit is contained in:
7
.github/workflows/public-safety.yml
vendored
7
.github/workflows/public-safety.yml
vendored
@@ -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 }}
|
||||
|
||||
2
LICENSE
2
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
186
docs/services.md
186
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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user