Service Audit

Service Audit & Log Scanning

Routine audit procedures for the homelab fleet. Run after each deploy and weekly.

Quick Audit (Post-Deploy)

After hey sync or reboot:

# 1. Failed units
ssh matrix_root "systemctl --failed --no-pager"

# 2. WARN+ERROR since boot, exclude known noise
ssh matrix_root 'journalctl --since "$(uptime -s)" --no-pager -p 4..3 | grep -vE "checkpoint|DNS.error|Failed.to.download|No.TLS.certificates|Blocked.IP|Configuration.build|TLS.handshake|ARC|DKIM|MTA.STS|greeting-failed|spam|matplotlib|notifyLicense|CoAuthoring|StreamingHttpResponse|url.parse|shutting.down|SIGKILL|listening|startup|shutdown" | tail -50'

# 3. High-frequency log patterns (top talkers)
ssh matrix_root 'journalctl --since "$(uptime -s)" --no-pager -p 4..3 | sed "s/^.*>://" | sort | uniq -c | sort -rn | head -15'

Weekly Deep Audit

Run every Sunday or after major changes.

1. Failed Units

ssh matrix_root "systemctl --failed --no-pager"

2. Service Restart Count

# Services that restart-loop (high count = crash-loop)
ssh matrix_root "systemctl list-units --type=service --state=running | awk '{print \$1}' | tail -n +2 | head -n -5 | xargs -I{} sh -c 'echo -n \"{}: \"; journalctl -u {} --since \"7 days ago\" | grep -c \"Started\|Starting\"' 2>/dev/null | sort -t: -k2 -rn | head -10"

3. Error/Warning Frequency Ranking

ssh matrix_root 'journalctl --since "7 days ago" --no-pager -p 4..3 | sed "s/^.*>://" | sort | uniq -c | sort -rn | head -20'

4. OAuth2-Proxy Session Health

# Failed token refreshes — spike indicates config/secrets issue
ssh matrix_root "journalctl -u oauth2-proxy --since '7 days ago' | grep -c 'Unable to refresh session'"

# Successful authentications — baseline for comparison
ssh matrix_root "journalctl -u oauth2-proxy --since '7 days ago' | grep -c 'Successfully refreshed session'"

5. PostgreSQL Health

# Connection errors — spike indicates app misconfiguration
ssh matrix_root "journalctl -u postgresql --since '7 days ago' | grep -ciE 'error|fatal'"

# Unexpected restarts — should be 0 outside planned maintenance
ssh matrix_root "journalctl -u postgresql --since '7 days ago' | grep -c 'shutdown'"

6. Nginx Rate Limiting

# Rate-limited requests — >100/week means limits too aggressive
ssh matrix_root "journalctl -u nginx --since '7 days ago' | grep -c 'limiting requests'"

7. Agenix Secret Health

# Check all secrets decrypt correctly
ssh matrix_root "ls /run/agenix/ | wc -l && ls -la /run/agenix/ | grep -E '^-' | wc -l"

# Empty secrets (0-byte files = decryption failure)
ssh matrix_root "find /run/agenix/ -type f -size 0 2>/dev/null"

8. DNS Resolution

# DNS failures during boot — persistent failures indicate network issue
ssh matrix_root "journalctl --since '7 days ago' | grep -c 'Temporary failure in name resolution'"

Audit Findings Log

2026-05-28 — Post-reboot Deep Scan

Context: Lab-matrix rebooted for litellm/ollama module updates. Full log scan across all 20+ services.

Scan commands used:

# Per-service status + tail logs
ssh matrix_root "systemctl status <unit> --no-pager"
ssh matrix_root "journalctl -u <unit> --no-pager -n 20"

# Global WARN+ERROR scan
ssh matrix_root 'journalctl --since "2026-05-28 12:00" --no-pager -p 4..3'

# Frequency ranking (deduplicated)
journalctl ... | sed 's/^.*>://' | sort | uniq -c | sort -rn

Findings and resolutions:

#SeverityServiceSymptomRoot CauseFix
1CRITICALForgejoensures script: Permission denied reading DB password; password clearedforgejo-database-secret had owner = "git", but PostgreSQL post-start runs as postgresChanged owner = "postgres" in modules/services/git/forgejo.nix
2WARNINGNginx105+ limiting requests, excess: 20.xxx by zone "oauth2_auth" — Immich thumbnails throttledoauth2_auth zone rate was 10r/s; Immich service-worker prefetches many thumbnails simultaneouslyRaised to 50r/s in modules/services/web/nginx.nix
3WARNINGCalibre84 Failed to send all data: Connection reset by peer every 5 minGatus health check uses GET on http://127.0.0.1:8282, downloads 3.4MB response, then disconnectsChanged to method = "HEAD" in modules/services/monitoring/gatus.nix
4WARNINGLitellmCrash-loop: Unable to find Prisma binaries on startupPrisma 7.x rejects bundled schema format; no compatible prisma 5.x in nixpkgs. preStart ran in read-only Nix store.Removed PostgreSQL dependency, DATABASE_URL, and preStart. Litellm runs as pure proxy. Documented in module.
5WARNINGOllama/Open-WebUIAdmin password hardcoded in Nix storeWEBUI_ADMIN_PASSWORD was inline in environmentMoved to agenix environmentFile; added ENABLE_SIGNUP=false
7WARNINGOpen-WebUIAdmin login deadlock: WEBUI_ADMIN_* env vars ignored after DB existsOpen-WebUI only creates admin on first startup (empty DB). If the DB was initialized without env vars, subsequent restarts won’t create the admin, and ENABLE_SIGNUP=false blocks web registrationDelete /var/lib/open-webui/data/webui.db and restart. Documented recovery in docs/ai-ml.md. Module split into modules/services/ai/open-webui.nix.
6WARNINGStalwartDashboard shows “TOTAL USERS 0”, “TOTAL DOMAINS 0”, “Not found”webadmin SPA (0.1.37) API paths incompatible with Stalwart 0.15.5; webadmin repo archived, replaced by webui in v0.16Documented in web-services.md and homelab-bootstrap.md; REST API workaround; upgrade to v0.16 when available in nixpkgs

Known noise (no fix needed):

PatternSourceWhy
Kanidm BadCertificate every 5 minGatus health check with insecure: trueExpected — self-signed cert on localhost
Stalwart No TLS certificateslab-matrix doesn’t run ACMEOnly vps-pacman issues certs; Tailscale WireGuard encrypts L1→L2
OAuth2-proxy invalid_grant spikesSession expired after rebootNormal — re-login clears it
Tailscale DNS no fallback candidatesBoot before DNS readyTransient, <30s
Mealie Secrets directory '/run/secrets'Docker conventionHarmless, no impact
DMS/EasyEffects Qt warningsDesktop shellNot web services

2026-05-30 — Port Conflict Cascade (Jitsi Meet)

Context: Adding Jitsi Meet to lab-matrix introduced 4 port conflicts that silently broke sing-box, jicofo, and stalwart.

Findings and resolutions:

#SeverityServiceSymptomRoot CauseFix
1CRITICALsing-boxRestart loop (360+ restarts), listen tcp 127.0.0.1:9090: bind: address already in usejitsi-excalidraw hardcodes Prometheus metrics on port 9090, same as sing-box Clash APISet PORT_PROMETHEUS = "9097" on jitsi-excalidraw in jitsi-meet.nix
2CRITICALjicofojava.net.BindException: Address already in use → dead, meet.alienzj.org brokenjicofo REST API defaults to port 8888, same as atuin-serverChanged services.jicofo.config."jicofo.rest.port" (quoted) → services.jicofo.config.jicofo.rest.port (nested) for correct HOCON serialization; atuin default port moved 8888→8887 to avoid jupyterhub conflict
3CRITICALStalwartFailed to listen on 127.0.0.1:8080 → mail.alienzj.org HTTP listener deadjitsi-videobridge (Colibri REST) binds 8080, same as stalwart webPort defaultChanged stalwart webPort default from 8080 to 8085 in stalwart.nix
4WARNINGLiteLLMUnable to find Prisma binaries. Please run 'prisma generate' first.preStart ran prisma db push --skip-generate but never prisma generate; env vars for prisma-engines paths were missingAdded PRISMA_QUERY_ENGINE_BINARY, PRISMA_QUERY_ENGINE_LIBRARY, PRISMA_SCHEMA_ENGINE_BINARY env vars + prisma generate to preStart in litellm.nix

Lesson learned: When adding a multi-component service like Jitsi, audit all ports used by every sub-service (jicofo, jitsi-videobridge2, jitsi-excalidraw, prosody) against the existing port map in docs/homelab-bootstrap.md before deploying.

Audit Checklist

Post-Deploy (every hey sync)

  • systemctl --failed --no-pager — expect 0 failed units
  • Check agenix secrets: ls /run/agenix/ | wc -l — count shouldn’t decrease
  • Spot-check 3 random services: systemctl status <unit> --no-pager
  • Quick WARN/ERROR scan: journalctl --since "$(uptime -s)" --no-pager -p 4..3 | tail -30
  • Open-WebUI: Verify admin exists — journalctl -u open-webui --no-pager | grep 'admin account' should show “created successfully”

Weekly

  • Full failed unit check
  • WARN/ERROR frequency ranking
  • OAuth2-proxy token refresh failure count
  • Nginx rate-limiting occurrences
  • Check for new unrecognized log patterns
  • Verify disk: df -h / /persist /var/lib
  • Verify PostgreSQL: sudo -u postgres psql -c '\l'

Monthly

  • Full deep audit (all steps in §Weekly Deep Audit)
  • Review config.local-keys warnings in Stalwart logs
  • Check for package updates: nix flake update --dry-run
  • Review this doc for stale findings — remove resolved ones
  • Update CHECK.md with current service status