Sync docs, audit artifacts, project notes, and VerdictTank proposal docs

- audit/phase-one + phase-two: security audit briefs, findings, credential-rotation plan, Docker-USER hardening scripts, rollback refs
- disaster-recovery/restore-test-log.md + backup-dr-audit-2026-08-10.md
- clients/ (modelortho SEO audit, ai-biz-dev competitive landscape), notes/ (tiktok strategy)
- projects/: front-desk-voice-agent, seo-visibility-checker product plan, hotnow-savannah HTML, resend-transactional-email, backup-dashboard-enhancements, code-review-graph, seo-ci-architecture
- proposals/verdicttank/: architecture v4.0, methodology, judge-pool review, consolidation reasoning, cross-check review
- docs/super-search/firecrawl-provider-strategy.md
- updates: CHANGELOG, model-chain, projects-master-readme, intelsight.io
- .gitignore: exclude nested standalone repos (seo-tool, venturebuilt)
This commit is contained in:
root
2026-08-26 02:27:28 -04:00
parent 23e9751d38
commit f5175f1ce0
55 changed files with 14669 additions and 3 deletions
@@ -0,0 +1,127 @@
# C4 Credential Rotation Plan (Phase Two, WS1)
Status: APPROVED (D1). Phase 0 complete, Phase 1 in progress.
Date: 2026-08-13
Owner: Sho'Nuff
## 0. What this plan covers (and what it does not)
- The C4 **permission subset** is DONE. Logged as `P2-C4-001` (app1-bu chmod 600 on 11 secret files + umask 077 root-cause fix). This plan does not re-open it.
- This plan covers the **second half of C4**: inventory every plaintext credential, rotate them, and move them to Vaultwarden.
- Source-hygiene follow-ups are included as Phase 6 (they are rotation-adjacent, not permission work).
## 1. The two decisions this plan needs from you (up front)
| # | Decision | What it unlocks |
|---|---|---|
| D1 | **APPROVED 2026-08-13** ("A - Rotate, B - Rotate, C - Revoke"): rotate live AI keys (A+B), revoke dead (C) | Phase 2 (in progress) |
| D2 | **Go/no-go: rotate the MSP-backdoor shared credential, plus approve the per-portal order** | Phase 3 |
Everything else in this plan runs under those two gates. Phases 4-6 are lower-risk hygiene and I will flag them individually before executing.
## 2. Evidence base (phase-one report, section 3.1 C4)
1. Two unsanitized copies of `key-inventory.md` on Core (one inside Hermes's reference dir).
2. Private `scripts` repo hardcodes the MSP-backdoor admin password reused across client onboardings (Git-A Finding 1). Single most consequential live credential.
3. app1-bu `/root/.hermes/.env` (mode 644) holds ~20 plaintext secrets: root passwords for app1/app2/app3, Telegram bot token, Cloudflare/Netcup/SyncroMSP tokens, and eight AI-provider keys.
4. `rally.service`, `seemytrip.service`, `giftaroast.service` hardcode `JWT_SECRET`, `DEEPSEEK_API_KEY`, `ADMIN_AI_KEY`, and Twilio credentials in world-readable unit files.
5. app3 MySQL root password in `/opt/backup-restore/snapshot.sh` (mode 775) and `/root/backup.sh`.
6. Public repo `itpp-infrastructure` re-leaks live admin passwords in a prior audit report (section 7, deferred per Germaine).
Recommended order from the report: app1-bu .env first, then key-inventory.md, then systemd units, then app3 snapshot.sh.
## 3. Safety rules (non-negotiable, apply to every phase)
1. **Fallback-first.** Before any LiteLLM-adjacent or credential-affecting change, Hermes stays pinned to direct DeepSeek. This is already in force since C1 (`P2-C1-001`); re-confirm at start of each phase.
2. **Vaultwarden-before-changes.** Retrieve a credential from Vaultwarden before touching the live system, never from a plaintext file, shell history, or a prior report.
3. **No autonomous key rotation.** Every rotation happens only after the go/no-go above, in the order listed.
4. **Verify before done.** Every rotation is followed by a live check (API call, login, health endpoint). No "should work" statements.
5. **Log + rollback.** Each change is written to `change-log.md` at the moment it is made, with the rollback path stated before the change.
6. **Zero artifact style.** No em dashes or en dashes in any plan or change-log entry. All credential values appear as `[REDACTED]`.
## 4. Phase 0: Inventory (read-only, no changes)
Goal: a single authoritative list of every live plaintext credential, cross-referenced against Vaultwarden.
Steps:
1. Re-run the secret-sprawl scan on the three hosts with the raw hit counts (6,296 Core / 5,504 app1-bu / 1,275 app2) and triage down to live, consequential secrets only.
2. Categorize every secret:
- (A) AI provider keys
- (B) MSP-backdoor shared credential
- (C) server root passwords (app1/app2/app3)
- (D) service tokens (Telegram, Cloudflare, Netcup, SyncroMSP, Twilio)
- (E) JWT secrets and DB passwords (systemd units, app3 snapshot/backup scripts)
- (F) source-repo leaks (key-inventory.md copies, scripts repo, public repo section 7)
3. Cross-reference each item against Vaultwarden: mark `already vaulted`, `missing`, or `stale`.
4. Deliverable: an inventory table with a rotation priority, a current location, and a target Vaultwarden item for each credential.
Effort note: this is where the L-XL sizing sits. The raw scan counts are noise; the consequential list is far smaller.
## 4a. Phase 0 result (2026-08-13)
- Inventory complete. The report's "eight keys" was an undercount: the live AI surface is ~19 upstream keys across three layers (config.yaml literals, .env plaintext, LiteLLM encrypted credentials), many duplicated.
- GEMINI_API_KEY and GOOGLE_AI_STUDIO_KEY hold the same value under two names (one rotation, two lines to update).
- Verdict: A (fallback chain, 7) = Rotate. B (operational LLM, 7) = Rotate. C (dead, 4) = Revoke.
- Correction: PARALLEL_API_KEY is not dead. It is a live Super Search provider (Parallel.ai Search, fallback #11 in server.py), reclassified as a Phase 3 service token (Keep).
- Vault gaps: DeepSeek, OpenAI, Google/Gemini, Groq, MiniMax have no Vaultwarden item; Cohere/Fireworks/Perplexity/Mistral have portal logins only.
## 5. Phase 1: AI provider keys (needs D1)
Sequence (fallback-first, rotate non-active providers before the active one):
1. Confirm Hermes is pinned to direct DeepSeek and the direct endpoint is healthy (`/v1/models` + a test completion).
2. From Phase 0 inventory, confirm the exact list of eight AI providers and which LiteLLM / env locations hold each key.
3. Rotate the keys for all providers EXCEPT the one Hermes is actively pinned to (DeepSeek), one at a time:
- Generate a new key at the provider portal.
- Store the new key in Vaultwarden (target item from Phase 0).
- Update LiteLLM config and/or env to reference the vault-backed value.
- Verify the provider still resolves through the fallback chain (test call).
- Write the change to `change-log.md` with rollback.
4. Rotate the pinned provider (DeepSeek) LAST, immediately updating Core `.env` and LiteLLM before any subsequent API call can fail.
5. Re-verify the full fallback chain end to end and switch Hermes back to its normal provider only after all eight keys are confirmed live.
Rollback: each step keeps the prior key in the change-log rollback note until the next step verifies green.
## 6. Phase 2: MSP-backdoor shared credential (needs D2)
Sequence (this is the highest-consequence single credential):
1. Identify the exact shared admin credential and every client system / vendor portal currently using it (inventory from `scripts` repo + onboarding records).
2. Produce a per-portal rotation order, ordered by blast radius (most-impacted or most-exposed first), and get your sign-off on the order.
3. Vaultwarden-before-changes: confirm the replacement credential is generated and vaulted before touching any live system.
4. Rotate one portal at a time: update the portal, verify login with the new credential, update any dependent automation/env, then move to the next.
5. Remove the hardcoded password from the private `scripts` repo and replace it with a Vaultwarden lookup or a `[REDACTED]` placeholder.
6. Log every portal change in `change-log.md` with rollback.
## 7. Phase 3: server root passwords and service tokens (lower risk)
- Rotate app1/app2/app3 root passwords; store in Vaultwarden; update the one place that currently references them in plaintext (app1-bu `.env`).
- Rotate service tokens: Telegram bot token, Cloudflare, Netcup, SyncroMSP, Twilio. Each token rotation re-issues at the vendor console, then updates the consuming service and any env/unit file.
- Verify each service still functions after its token rotates (send a test, poll an endpoint, etc.).
## 8. Phase 4: JWT secrets and DB passwords
- Move `JWT_SECRET`, `DEEPSEEK_API_KEY`, `ADMIN_AI_KEY`, and Twilio creds out of `rally.service`, `seemytrip.service`, `giftaroast.service` into vault-backed env or a `chmod 600` env file, then reload the units.
- Rotate the app3 MySQL root password in `/opt/backup-restore/snapshot.sh` and `/root/backup.sh`; store in Vaultwarden; update the scripts to source it from a 600-mode file.
- Verify each service/backup still runs after the change.
## 9. Phase 5: source hygiene (flagged separately)
- Redact or delete the two `key-inventory.md` copies on Core.
- Confirm the `scripts` repo MSP password removal from Phase 2 is committed and pushed.
- Public repo `itpp-infrastructure` re-leak (report section 7): deferred per your earlier instruction; restating it here so it is not silently dropped. Confirm whether to keep it deferred or fold it into this plan.
## 10. Verification gates (plan-wide)
- [ ] Phase 0 inventory complete and cross-referenced against Vaultwarden
- [ ] Fallback chain verified end to end after all AI key rotations
- [ ] Every portal login verified after MSP-backdoor rotation
- [ ] Every service/unit verified functional after its token/JWT/DB rotation
- [ ] `change-log.md` has a dated entry with rollback for every single change
- [ ] No plaintext credential remains in any world-readable location (re-run the scan)
## 11. Approval needed
- D1: AI provider key rotation go/no-go (unlocks Phase 1)
- D2: MSP-backdoor rotation go/no-go + per-portal order (unlocks Phase 2)
- Phases 3-5: flag individually before execution (no blanket approval implied)
+45
View File
@@ -0,0 +1,45 @@
# ITPP Phase Two Change Log
Engagement: ITPP Phase Two (remediation). Supersedes the placeholder framework.
Started: 2026-08-13
Conductor: Sho'Nuff
Authority: Germaine Brown (sole authorizer per P&P section 2 and section 8)
This log records every live change made during Phase Two, per Policy and Procedure section 2:
(a) what changed, (b) who authorized it, (c) when, and (d) how to roll back.
Every entry is written at the moment the change is made, not discovered later.
Passwords and secrets are never recorded in plaintext here. The entry points to the
Vaultwarden item that holds the new value.
---
## Schema
| Field | Meaning |
|---|---|
| ID | Sequential entry number (P2-###) |
| Date | When the change was applied (ET) |
| Change | What changed (old -> new where applicable) |
| Authorized by | Germaine, or a referenced approval (e.g. "Phase Two fast-track C7") |
| Rollback | How to undo it |
| Status | done / in-progress / rolled-back |
| Notes | Dependencies verified, LiteLLM-adjacent flag, exemption reference if any |
---
## Entries
| ID | Date | Change | Authorized by | Rollback | Status | Notes |
|---|---|---|---|---|---|---|
| P2-C3-001 | 2026-08-13 | Enrolled Wazuh agents 4.9.2 on 5 hosts: core, app2, app3, app1-bu, wphost02 (manager app1 152.53.36.131; agent IDs 001-005, all Active) | Phase Two fast-track C3 (Germaine) | Disable/remove agent service on each host, then remove agent record from manager via agent_control -r `<id>` | done | LiteLLM-adjacent=no. Verified via agent_control -l on manager: all 5 new agents + agent 000 (manager) show Active. No password/secret recorded (authd enrollment used use_password=no, no credential involved). |
| P2-C7-001 | 2026-08-13 | Rotated Grafana admin password (default 'admin' removed; 24-char random value stored in Vaultwarden item 'Grafana - Core Admin'); restricted port 3002 from Anywhere to Tailscale 100.64.0.0/10 | Phase Two fast-track C7 (Germaine) | Recreate container with GF_SECURITY_ADMIN_PASSWORD=admin (see rollback/grafana-inspect.json); ufw allow 3002/tcp to restore Anywhere | done | LiteLLM-adjacent=no. Verified: admin/admin login -> 401, new password -> 200, UFW shows only 100.64.0.0/10 rule. Password persisted in Vaultwarden item d3785ae9-085d-4e40-ba1b-69b66fef9e34. |
| P2-C8-001 | 2026-08-13 | wphost02 full DB backup coverage; closed the "6 of 8 unprotected" gap. RECON: found the offsite S3 script (`/root/backup.sh` on wphost02, using `mysqldump --all-databases`) was already being triggered daily via a Core-side crontab entry (`0 5 * * * ssh ... root@5.161.62.38 '/root/backup.sh'`, running since ≥2026-07-19); this was NOT visible from wphost02's own crontab, which is why the earlier finding only checked local `db-dump.sh` (2 of 8 DBs, 7-day local retention) and missed it. Verified via journalctl (`wphost02-backup` tag on Core) and a fresh manual run of backup.sh: all 8 WordPress DBs (apextrackexperience_1781549652, boxpilotlogistics_1770339547, debtrecoveryexperts_1778934554, iAmGMB_1764020288, katiewattsdesign_1735425014, mainWP_1717713767, vigilanttac_1728911691, voipsimplicity_1732250845) are present in the all-databases.sql dump and in per-site tarballs, landing daily in s3://hermes-vps-backups/wphost02-backup/<date>/ (Wasabi). Also extended wphost02's local `/root/db-dump.sh` (still only covered 2/8 DBs on local disk, 7-day retention) to dump all 8 DBs individually for local defense-in-depth; ran it successfully (22 files, all 8 today's dumps present). No new crontab entry was needed; the offsite S3 job was already wired in on Core and already covers all 8 DBs; only the local-disk script was extended. | Phase Two Workstream 1 C8 (Germaine) | restore db-dump.sh from /root/db-dump.sh.bak-20260813 on wphost02 (scp back or `cp db-dump.sh.bak-20260813 db-dump.sh`); no crontab line was added so there is nothing to remove there; the pre-existing Core crontab line `0 5 * * * ssh -i /root/.ssh/itpp-infra ... root@5.161.62.38 '/root/backup.sh'` is unchanged from before this task. | done | LiteLLM-adjacent=no. No decommission action taken; no live WordPress site or config modified; no new S3 credentials created (existing Wasabi creds in wphost02:/root/.aws/credentials, already valid, reused as-is). Verified via S3 object listing (10 objects, fresh timestamp) and journalctl, not by trusting script exit code alone. |
| P2-C1-001 | 2026-08-13 | LiteLLM fallback switch (pre-C1 safety gate): Hermes primary model provider admin-ai (LiteLLM on app1) -> deepseek (direct to api.deepseek.com). Also switched delegation.provider and delegation.model to direct deepseek, and delegation.fallback provider admin-ai -> deepseek. Model stays deepseek-v4-pro (no downgrade; direct endpoint serves both deepseek-v4-pro and deepseek-v4-flash). | Phase Two safety rule (fallback switch required before any LiteLLM-adjacent change); C1 app1 portion is LiteLLM-adjacent | Restore config from /root/.hermes/config.yaml.bak-20260813-C1, or revert via: hermes config set model.provider admin-ai; hermes config set delegation.provider admin-ai; hermes config set delegation.model claude-sonnet-5; and set delegation.fallback provider back to admin-ai. | done | LiteLLM-adjacent=yes (this IS the fallback switch). Verified direct deepseek /v1/models returns both deepseek-v4-pro and deepseek-v4-flash, and /v1/chat/completions returns a valid choices array with no error. DEEPSEEK_API_KEY present in .env and valid. Note: the running gateway session continues on admin-ai until next restart but is protected by the already-active direct-first fallback chain (deepseek -> google -> xai -> anthropic -> openai). Residual admin-ai references remain only in vision paths (auxiliary.vision and top-level vision), non-blocking for C1 terminal/SSH work. |
| P2-C9-001 | 2026-08-13 | Warm-standby DB snapshot consistency: hermes-live-sync.sh now exports state.db and memory_store.db via SQLite online backup (.backup) into a consistent snapshot uploaded as the authoritative S3 object, and excludes the raw live WAL/SHM files from the sync. Deleted obsolete transient state.db-wal/shm and memory_store.db-wal/shm objects from s3://hermes-vps-backups/live/. | Phase Two Workstream 1 C9 (Germaine) | cp ~/.hermes/scripts/hermes-live-sync.sh.bak-20260813-C9 ~/.hermes/scripts/hermes-live-sync.sh (raw file copy behavior restored) | done | LiteLLM-adjacent=no. Verified: .backup of the 2.98 GB live state.db completed in 9s and passed PRAGMA integrity_check (ok); fixed script ran exit 0 and uploaded the 2980638720-byte state.db and 1871872-byte memory_store.db snapshots at 15:30 ET. Root cause: aws s3 sync of a live WAL-mode SQLite DB can capture a torn checkpoint (evidence: state.db.corrupted plus 3 malformed-backup-* objects in S3 dated 2026-07-09). Residual follow-up: profiles/anita/state.db* still synced raw because top-level excludes do not match profile paths; and ~7.5 GB of stale corrupted/malformed S3 objects left in place pending a cleanup decision. |
| P2-C4-001 | 2026-08-13 | Restored least-privilege file permissions on app1-bu standby: chmod 600 on 11 world-readable secret-bearing files (.hermes/.env, .hermes/config.yaml, profiles/anita/.env, profiles/anita/google_client_secret.json, docker/vaultwarden/.env, docker/twenty/.env, references/glc-credentials.md, references/liberty-credentials.md, .aws/credentials.bak, and two state-snapshot .env files). Fixed root cause: added umask 077 to hermes-standby-sync.sh, hermes-standby-watchdog.sh, and hermes-standby-restore.sh (standby) plus hermes-live-sync.sh (Core) so aws s3 sync no longer re-creates synced files as 644 and the Core .snapshots output is no longer transiently world-readable. | Phase Two Workstream 1 C4 (Germaine) | Restore the three .bak-20260813-C4 script backups (cp each .bak-20260813-C4 back over its script); re-chmod 644 the files (not recommended) | done | LiteLLM-adjacent=no. Root cause: aws s3 sync does not preserve POSIX permissions (S3 objects carry no mode bits), so the 10-min standby sync re-created Core 600 files as 644 under the default umask 022. Verified: functional test of aws s3 sync under umask 077 lands a downloaded object as 600; bash -n passes on all three edited scripts. Core already holds the same files at 600, so no Core change was needed. |
| P2-C1-002 | 2026-08-13 | DOCKER-USER firewall hardening made persistent on Core, app1, app2, app3. Live IPv4 rules (applied earlier 2026-08-13) now survive reboot and docker restart via /usr/local/sbin/itpp-docker-user.sh (idempotent flush+rebuild of DOCKER-USER, IPv4 + IPv6 where present) wired as an ExecStartPost drop-in at /etc/systemd/system/docker.service.d/itpp-docker-user.conf on each host. Closed to Tailscale-only (100.64.0.0/10 + ESTABLISHED,RELATED only): Core browserless 3000, camofox 9377, uptime-kuma 3001; app1 komodo 9120, twenty 3003, wazuh dashboard 5601, wazuh indexer 9200; app2 bookstack 6875 (direct), minio 9001, gitea ssh 3022, ragflow 9380-9384/9392/9393, infinity 23817/23820, unifi admin 8443/8843/8880, unms 81/8444; app3 buzz relay 3000. Preserved public: app1 wazuh manager 1514/1515/55000 tcp + 514 udp; app2 dns 53 tcp+udp, support-api 6880, unifi inform 8080, traccar 8082 + 5000-5150 tcp+udp, unms-nginx 8089, bookstack 6875 via ctorigdstport proxy path, unms-netflow 2055 udp, unifi 3478/10001 udp. | Phase Two Workstream 1 C1 (Germaine) | On each host: rm /etc/systemd/system/docker.service.d/itpp-docker-user.conf && systemctl daemon-reload; then iptables -F DOCKER-USER (and ip6tables -F DOCKER-USER where present); rm /usr/local/sbin/itpp-docker-user.sh | done | LiteLLM-adjacent=no. Rules scoped -i eth0 so docker-bridge traffic is untouched. Verified live: iptables -L DOCKER-USER -n -v on all 4 hosts shows correct chains with -i eth0 and active packet counters. Persistence proven end-to-end: ufw reload on Core preserved FORWARD integration and DOCKER-USER rules; systemctl restart docker on app3 flushed DOCKER-USER (counters reset) then ExecStartPost re-applied rules immediately, all 8 app3 containers returned healthy (hexclave-server health:starting then healthy). Pre-existing issues noted, not C1: crm.debtrecoveryexperts.com and crm.intelsight.io 502 because Core Caddy proxies to Core-local localhost:3003 where nothing listens (Twenty runs app1:3003); admin-ai /v1/models 401 despite /health 200 (auth, not firewall); infinity 23817 returns 000 from minio while 23820 returns 404 (port semantics). |
| P2-C4-002 | 2026-08-13 | C4 credential rotation: D1 approved (rotate A and B, revoke C). Phase 0 inventory complete. Rollback snapshots taken: /root/.hermes/config.yaml.bak-20260813-C4-pre and /root/.hermes/.env.bak-20260813-C4-pre, both chmod 600. | Germaine (A rotate, B rotate, C revoke, 2026-08-13) | Restore both C4-pre backups | done | LiteLLM-adjacent=yes (this entry is the go/no-go gate for LiteLLM key changes). No keys changed yet; decision record only. Phase 0 verdict: A=7 rotate, B=7 rotate, C=4 revoke. |
| P2-C4-003 | 2026-08-13 | Revoked 4 dead AI keys from plaintext: removed AI21_API_KEY, ALIBABA_API_KEY (duplicate, x2), ZAI_API_KEY, NVIDIA_API_KEY from Core /root/.hermes/.env and app1-bu /root/.hermes/.env (5 lines to 0), plus the stale NVIDIA NIM comment line. PARALLEL_API_KEY reclassified: it is a live Super Search provider (Parallel.ai Search, fallback #11 in server.py), not an orphan, moved to Phase 3 service tokens (Keep). | Germaine (C revoke) | Restore /root/.hermes/.env.bak-20260813-C4-pre on Core and app1-bu .env from its prior state | done | LiteLLM-adjacent=no (plaintext .env removal only; no LiteLLM credential touched). Verified before/after line counts and mode 600 on both hosts. Residual: NVIDIA LiteLLM credential (0 working models) still in app1 Postgres, deletion deferred to the LiteLLM step. |
| P2-C4-004 | 2026-08-14 | Rotated 5 assistant-owned portal API keys (b1 set): Cohere, Fireworks, Mistral, Perplexity, FAL. Applied across three layers: (1) LiteLLM credentials PATCHed via /credentials/{name} on app1 (Cohere, Fireworks, Mistral, Perplexity, Fal) — 5/5 HTTP 200; (2) Core /root/.hermes/.env swapped (COHERE_API_KEY, FIREWORKS_API_KEY, MISTRAL_API_KEY, PERPLEXITY_API_KEY, FAL_KEY) with backup /root/.hermes/.env.bak-20260814-b1; (3) Vaultwarden — created 3 new 'X - API Key' items (Cohere, Fireworks, Mistral; username info@itpropartner.com) and updated Perplexity ('console.perplexity.ai') and FAL ('fal.ai') item passwords in place. Mistral+Cohere keys generated in-console as 'LiteLLM-20260813'; Fireworks/FAL/Perplexity keys supplied by Germaine. | Germaine (C4 D1 GO + b1 direction 2026-08-14) | Core .env: cp /root/.hermes/.env.bak-20260814-b1 /root/.hermes/.env. LiteLLM: re-PATCH the 5 credentials with pre-rotation values (old keys remain active provider-side; recoverable from .env.bak-20260813-C4-pre and Vaultwarden password history). Vaultwarden: restore Perplexity/FAL item passwords via bw item password history; delete the 3 new 'X - API Key' items. | done | LiteLLM-adjacent=yes. Verified: all 5 keys live against providers (Mistral/Cohere/Fireworks/Perplexity HTTP 200; FAL past auth gateway); LiteLLM PATCH 5/5 HTTP 200; .env values correct with pre-existing dual NETCUP_API_KEY quirk preserved; Vaultwarden re-read 5/5 PASS. Active consumers: Perplexity -> Super Search (super-search.service restarted; health_check reports perplexity:ok, server v2.4.0); FAL -> image_generate (picks up new key on next Hermes restart; running process holds prior key in os.environ). Cohere/Fireworks/Mistral have no active Core consumer (LiteLLM 'use if set' only). Old keys NOT revoked provider-side. |
| P2-C4-005 | 2026-08-14 | Rotated the admin-ai LiteLLM master key: old (len 66, sk-litel...) -> new (len 67, sk-x2top..., sk- + 64 mixed-case alphanumeric, no special chars). Applied to: (1) app1 /root/docker/litellm/.env LITELLM_MASTER_KEY and UI_PASSWORD (backup .env.bak-20260814-adminai); (2) Core /root/.hermes/scripts/cost-alert-watchdog.py MASTER_KEY (backup .bak-20260814-adminai). Recreated litellm container via `docker compose up -d --force-recreate litellm` (a bare `docker restart` does NOT re-read env vars — caught during verification). Vaultwarden consolidated: new key in item 'LiteLLM (admin-ai) Master Key' 0772c50c; updated 'admin-ai.itpropartner.com' bb01ef49 to the new key; deleted stale 'LiteLLM Master Key' 446da6ca and duplicate 'admin-ai.itpropartner.com' 8c8a5eb1. | Germaine (added new key to Vaultwarden as 'LiteLLM (admin-ai) Master Key', 2026-08-14) | Restore app1 .env from .env.bak-20260814-adminai and watchdog from .bak-20260814-adminai, then `docker compose up -d --force-recreate litellm` (old key is now revoked 401, so rollback requires restoring the old value to .env first); restore deleted Vaultwarden items from trash. | done | LiteLLM-adjacent=yes. Verified on app1 localhost:4000 — new key HTTP 200 on /spend/keys and /global/spend, old key HTTP 401 (revoked); cost-alert-watchdog ran clean against the new key (surfaced real alert: hermes-agent-v5 at 88% budget); Core 25-char virtual key 'Hermes (Sho'Nuff) LiteLLM Virtual API Key' (0aed1ca4) unaffected, /v1/chat/completions HTTP 200; LITELLM_SALT_KEY unchanged. Core admin-ai provider uses the 25-char virtual key (not the master key), so no Core config.yaml change was required. |
+83
View File
@@ -0,0 +1,83 @@
# ITPP Phase Two - Remediation Cost Estimate
**Date:** 2026-08-13
**Status:** AWAITING GERMAINE APPROVAL (C7 + C3 pre-approved fast-track, already executing)
**Conductor:** Sho'Nuff
---
## Anchor (Phase One actuals, for calibration)
Phase One was a read-only audit of 6 hosts plus the repo estate. From `brief.md` and the final report:
- Approved estimate: subtotal ~$5.40
- Realistic range: $8-10
- Ceiling: ~$13
- **Actual: exceeded the ceiling.** Driven by claude-sonnet-5 subagent usage and re-runs from 429 rate-limit failures.
- 7-day estate-wide DeepSeek total: $61.81 (all usage, not audit-only).
Phase Two is **live-change remediation** (investigate, change, verify, rollback-test, log), roughly 2-4x the per-unit cost of a read-only finding. The estimate below carries margin because Phase One actuals exceeded estimate. Under the $20/day cap, this spreads across ~8-12 execution days.
Effort labels are taken verbatim from the Phase One report recommendations table (S = under 1h, M = half day, L = 1-2 days, XL = multi-day).
---
## Workstream 1 - Critical findings (itemized, per requirement)
| Item | Finding (report ref) | Effort | Estimate |
|---|---|---|---|
| C1 | Docker published-port UFW bypass, ~20 public consoles | M-L | $5-14 |
| C4 | Plaintext credentials, inventory + rotate + vault | L-XL | $12-28 |
| C5 | LiteLLM Postgres backup gap (pg_dump wrong DB name) | S-M | $2-5 |
| C6 | app3 shared MySQL, no tenant boundary (~24 sites) | L-XL | $12-28 |
| C8 | wphost02 backup gap + decommission (split-brain) | M | $3-8 |
| C9 | Warm standby state-DB sync (RPO ~28d stale) | M | $3-8 |
| C10 | Single shared SSH key, passwordless root (Indep D4) | M | $3-8 |
| D3 | Public repo re-leaks admin creds (EX-001, deferred) | M | $3-8 |
| C3 | Wazuh agents on 5 hosts (FAST-TRACK, in progress) | M | $3-8 |
| C7 | Grafana rotate + Tailscale (FAST-TRACK, in progress) | S | $1-3 |
**WS1 subtotal: ~$47-118**
## Workstreams 2-6 (workstream-level)
Scope drawn from Phase One report sections noted.
| Workstream | Scope (report ref) | Estimate |
|---|---|---|
| WS2 - High findings | MFA enforcement (Gitea/CloudPanel/Vaultwarden/Grafana, report #11), fail2ban + unattended-upgrades + auditd (#12), backup gaps + restore-tests (#13), UNMS to UISP + image pinning (#14), port 8200 + runaway processes + gateway systemd (#15), retire NOPASSWD:ALL for named sudo (#10) | $20-50 |
| WS3 - Segmentation | C2 Tailscale ACL tags + three-tier model (internal/client/product) from report section 6 | $12-28 |
| WS4 - Git reorg | Repo estate cleanup (report 2.3) + private-repo credential exposures (Git-A) | $5-14 |
| WS5 - Documentation | Say-do gap fixes across 12 doc sections (report section 5) | $3-10 |
| WS6 - Operationalize P&P | Stand up Running Exemptions doc + change-management cadence (bootstrapped) | $3-10 |
**WS2-6 subtotal: ~$43-112**
## Independence / verification buffer
Conductor independence checks (Sonnet 5) on consequential live-change "is this safe to do live" calls, per the Phase One mitigation pattern: $6-12
---
## Totals
| Line | Range |
|---|---|
| WS1 (Critical) | $47-118 |
| WS2-6 | $43-112 |
| Independence buffer | $6-12 |
| **TOTAL** | **~$96-242 (midpoint ~$170)** |
Pacing: ~8-12 days at the $20/day cap. No opus-tier models; primary DeepSeek V4 Pro, Sonnet 5 only for independence checks.
## Scope-variance flags (flag before absorbing)
- **C4** inventory may be larger than the report captured (6,296 Core / 5,504 app1-bu / 1,275 app2 secret-sprawl hits need per-file triage).
- **C6** app3 DB segmentation is structural and may surface new per-tenant requirements mid-flight.
- **D3** depends on Germaine's repo decision (currently deferred as EX-001).
- **C1** on app1 is LiteLLM-adjacent (Hermes's own inference path); fallback chain will be used and logged.
## Pre-flight confirmation
- Fallback chain verified intact: primary = admin-ai (LiteLLM on app1); fallbacks = deepseek (direct api.deepseek.com), google, xai, anthropic, openai, all configured direct.
- C7 (Core) and C3 (agent enrollment, manager ports already published) are NOT LiteLLM-adjacent; no fallback switch required.
@@ -0,0 +1,223 @@
[
{
"Id": "66114deb715b3c90199bff9be1b4bbce3522e3152d6b4c0ff15256cbe2dfccf7",
"Created": "2026-07-16T06:40:35.087889714Z",
"Path": "/run.sh",
"Args": [],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 670022,
"ExitCode": 0,
"Error": "",
"StartedAt": "2026-07-16T06:40:35.232657942Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
"Image": "sha256:c0b69935a2469f0add84fa71a1632a2594c78aeeb016ba9f5d8728a4b4c1b976",
"ResolvConfPath": "/var/lib/docker/containers/66114deb715b3c90199bff9be1b4bbce3522e3152d6b4c0ff15256cbe2dfccf7/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/66114deb715b3c90199bff9be1b4bbce3522e3152d6b4c0ff15256cbe2dfccf7/hostname",
"HostsPath": "/var/lib/docker/containers/66114deb715b3c90199bff9be1b4bbce3522e3152d6b4c0ff15256cbe2dfccf7/hosts",
"LogPath": "/var/lib/docker/containers/66114deb715b3c90199bff9be1b4bbce3522e3152d6b4c0ff15256cbe2dfccf7/66114deb715b3c90199bff9be1b4bbce3522e3152d6b4c0ff15256cbe2dfccf7-json.log",
"Name": "/grafana",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "docker-default",
"ExecIDs": null,
"HostConfig": {
"Binds": [
"grafana_data_final:/var/lib/grafana"
],
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "host",
"PortBindings": {},
"RestartPolicy": {
"Name": "unless-stopped",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"ConsoleSize": [
0,
0
],
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": [],
"BlkioDeviceReadBps": [],
"BlkioDeviceWriteBps": [],
"BlkioDeviceReadIOps": [],
"BlkioDeviceWriteIOps": [],
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DeviceRequests": null,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": [],
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/3ab0a39f1573bc37ee3fc9203d8f876bb9ed9df325b39ca776e3ef86da2bc9ff-init/diff:/var/lib/docker/overlay2/96d3092d12c0e0b0c1bd01a5818f003bb7fab32246a893224296a67799841669/diff:/var/lib/docker/overlay2/4da7e8838b31487c835f93853d4a81e5cd06434fa34d04fdbbf2141cb95abc13/diff:/var/lib/docker/overlay2/8e1e7eaf9bac203b5e4a54e1c80c317f6483d40610b1776e9f6911aa18c41a72/diff:/var/lib/docker/overlay2/02a0a88da9ab58ca40a03ab418e1248317e5917fa318654d4cf60bf394b21b07/diff:/var/lib/docker/overlay2/9cc7ea00867e008c469d86f0bf2244d3e60737a665648609a303c93c8e3805cf/diff:/var/lib/docker/overlay2/8b1f24758f00124ab15b7b5b9957594184426ad6aa1911d76c5add384d1613ed/diff:/var/lib/docker/overlay2/bd4250cfbd3624493c084c81954c59a9744c13a863da7cd3ef348f0accf19719/diff:/var/lib/docker/overlay2/041749d1c02cf48d8a3257d604c5a7cfae504fc36f492854a4e056c0562cf599/diff:/var/lib/docker/overlay2/21d92f72aced24d754ce4a6daf302e9dd122273b48fdd107e8ede5c1018d4a4d/diff:/var/lib/docker/overlay2/2311d9ffcf0c02b1565678784a4f5bb120660ae0c021094f152cbd9747a5b916/diff",
"MergedDir": "/var/lib/docker/overlay2/3ab0a39f1573bc37ee3fc9203d8f876bb9ed9df325b39ca776e3ef86da2bc9ff/merged",
"UpperDir": "/var/lib/docker/overlay2/3ab0a39f1573bc37ee3fc9203d8f876bb9ed9df325b39ca776e3ef86da2bc9ff/diff",
"WorkDir": "/var/lib/docker/overlay2/3ab0a39f1573bc37ee3fc9203d8f876bb9ed9df325b39ca776e3ef86da2bc9ff/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "volume",
"Name": "grafana_data_final",
"Source": "/var/lib/docker/volumes/grafana_data_final/_data",
"Destination": "/var/lib/grafana",
"Driver": "local",
"Mode": "z",
"RW": true,
"Propagation": ""
}
],
"Config": {
"Hostname": "core",
"Domainname": "",
"User": "472",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"3000/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"GF_SECURITY_ADMIN_USER=admin",
"GF_SECURITY_ADMIN_PASSWORD=admin",
"GF_SERVER_HTTP_PORT=3002",
"PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"GF_PATHS_CONFIG=/etc/grafana/grafana.ini",
"GF_PATHS_DATA=/var/lib/grafana",
"GF_PATHS_HOME=/usr/share/grafana",
"GF_PATHS_LOGS=/var/log/grafana",
"GF_PATHS_PLUGINS=/var/lib/grafana/plugins",
"GF_PATHS_PROVISIONING=/etc/grafana/provisioning"
],
"Cmd": null,
"Image": "grafana/grafana:11.4.0",
"Volumes": null,
"WorkingDir": "/usr/share/grafana",
"Entrypoint": [
"/run.sh"
],
"OnBuild": null,
"Labels": {
"maintainer": "Grafana Labs <hello@grafana.com>"
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "fb05509631e71c8372e97ea4f43331331476ce53ecef4e52494b20e96c9debc0",
"SandboxKey": "/var/run/docker/netns/default",
"Ports": {},
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "",
"Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "",
"IPPrefixLen": 0,
"IPv6Gateway": "",
"MacAddress": "",
"Networks": {
"host": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "",
"NetworkID": "f7ebc0a30ba605067a1c4f5b22c519f3c3cb4f46e8a4b4359787cf0fe8d09a37",
"EndpointID": "458bbfe47f84e5aab9820274ff509a215b8b24bc99e8cbb388081a0d75f028b0",
"Gateway": "",
"IPAddress": "",
"IPPrefixLen": 0,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DriverOpts": null,
"DNSNames": null
}
}
}
}
]
+37
View File
@@ -0,0 +1,37 @@
# Running Exemptions Document
Authority: Germaine Brown is the sole authorizer of any departure from the
Policy & Procedure document (P&P section 8). No exemption is assumed; it is requested,
justified, approved, and recorded here.
## Schema
| Field | Meaning |
|---|---|
| ID | EX-### |
| Date | When requested/approved (ET) |
| Requester | Who raised it |
| Provision departed from | Which P&P section/policy |
| Request + justification | The business reason |
| Authorization | Germaine's decision |
| One-time / ongoing | Duration |
| Follow-up date | For ongoing exemptions |
| Status | open / closed |
---
## Entries
### EX-001: Public-repo credential exposure (D3) deferred
| Field | Value |
|---|---|
| ID | EX-001 |
| Date | 2026-08-13 (raised in Phase One, re-surfaced at Phase Two start) |
| Requester | Germaine Brown |
| Provision departed from | P&P section 1 (no live credentials in any repo): the public repo `itpp-infrastructure` re-leaks live admin credentials |
| Request + justification | Defer the repo lockdown ("leave the repo alone for now") while Phase One was read-only |
| Authorization | Deferred by Germaine in Phase One |
| One-time / ongoing | One-time, with a Phase Two follow-up |
| Follow-up date | Phase Two start (now): this is Workstream 1 item 10 (D3/EX-001) |
| Status | OPEN: pending Germaine's updated decision now that Phase Two is starting |
@@ -0,0 +1,30 @@
#!/bin/bash
# ITPP Phase Two C1 - DOCKER-USER hardening (management consoles Tailscale-only)
# Host: app1 (152.53.36.131) | Applied 2026-08-13 | Authorized: Phase Two Workstream 1 C1 (Germaine)
# Idempotent: flushes and rebuilds DOCKER-USER (IPv4 + IPv6). Safe to re-run manually.
# Closes (Tailscale-only): komodo 9120, twenty-server 3003, wazuh dashboard 5601, wazuh indexer 9200.
# Preserves (public): wazuh manager 1514/1515/55000 tcp + 514 udp (5 active agents).
# NOTE: litellm is 127.0.0.1:4000 behind Caddy (admin-ai.itpropartner.com) - untouched here.
set -eu
if ! iptables -L DOCKER-USER -n >/dev/null 2>&1; then
iptables -N DOCKER-USER
fi
iptables -F DOCKER-USER
iptables -A DOCKER-USER -s 100.64.0.0/10 -j RETURN
iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
iptables -A DOCKER-USER -i eth0 -p tcp -m multiport --dports 1514,1515,55000 -j RETURN
iptables -A DOCKER-USER -i eth0 -p udp -m multiport --dports 514 -j RETURN
iptables -A DOCKER-USER -i eth0 -j DROP
iptables -A DOCKER-USER -j RETURN
if ip6tables -L DOCKER-USER -n >/dev/null 2>&1; then
ip6tables -F DOCKER-USER
ip6tables -A DOCKER-USER -s fd7a:115c:a1e0::/48 -j RETURN
ip6tables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
ip6tables -A DOCKER-USER -i eth0 -p tcp -m multiport --dports 1514,1515,55000 -j RETURN
ip6tables -A DOCKER-USER -i eth0 -p udp -m multiport --dports 514 -j RETURN
ip6tables -A DOCKER-USER -i eth0 -j DROP
ip6tables -A DOCKER-USER -j RETURN
fi
exit 0
@@ -0,0 +1,42 @@
#!/bin/bash
# ITPP Phase Two C1 - DOCKER-USER hardening (management consoles Tailscale-only)
# Host: app2 (152.53.39.202) | Applied 2026-08-13 | Authorized: Phase Two Workstream 1 C1 (Germaine)
# Idempotent: flushes and rebuilds DOCKER-USER (IPv4 + IPv6). Safe to re-run manually.
#
# Closes (Tailscale-only, dropped by the final eth0 DROP; admin UIs still reachable via Caddy :443 -> localhost):
# unifi-controller admin UI 8443 + guest portal 8843/8880 (unifi.itpropartner.com -> Caddy -> localhost:8443);
# unms-nginx admin UI 8444 + http 81 (unms.forefrontwireless.com -> Caddy -> localhost:8444);
# minio console 9001, infinity 23817+23820, ragflow 9380-9384+9392+9393 (ragflow.itpropartner.com -> Caddy -> localhost:9392);
# gitea ssh 3022 (web is 127.0.0.1:3001 -> Caddy git.itpropartner.com).
# Preserves (public):
# technitium DNS 53 tcp+udp;
# bookstack 6875 (published 6875 -> container 80, matched via ctorigdstport; app3 support site proxies here);
# support-api 6880 (app3 support.itpropartner.com -> 152.53.39.202:6880);
# traccar 8082 + 5000-5150 tcp+udp (GPS devices + Core Caddy cross-proxy);
# unifi-controller 8080 tcp (23 remote devices inform here) + 3478/10001 udp (STUN/discovery);
# unms-nginx 8089 tcp + unms-netflow 2055 udp (UISP device-facing; conservative - see change log notes).
set -eu
if ! iptables -L DOCKER-USER -n >/dev/null 2>&1; then
iptables -N DOCKER-USER
fi
iptables -F DOCKER-USER
iptables -A DOCKER-USER -s 100.64.0.0/10 -j RETURN
iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
iptables -A DOCKER-USER -i eth0 -p tcp -m multiport --dports 53,6880,8080,8082,8089,5000:5150 -j RETURN
iptables -A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 6875 -j RETURN
iptables -A DOCKER-USER -i eth0 -p udp -m multiport --dports 53,2055,3478,10001,5000:5150 -j RETURN
iptables -A DOCKER-USER -i eth0 -j DROP
iptables -A DOCKER-USER -j RETURN
if ip6tables -L DOCKER-USER -n >/dev/null 2>&1; then
ip6tables -F DOCKER-USER
ip6tables -A DOCKER-USER -s fd7a:115c:a1e0::/48 -j RETURN
ip6tables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
ip6tables -A DOCKER-USER -i eth0 -p tcp -m multiport --dports 53,6880,8080,8082,8089,5000:5150 -j RETURN
ip6tables -A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 6875 -j RETURN
ip6tables -A DOCKER-USER -i eth0 -p udp -m multiport --dports 53,2055,3478,10001,5000:5150 -j RETURN
ip6tables -A DOCKER-USER -i eth0 -j DROP
ip6tables -A DOCKER-USER -j RETURN
fi
exit 0
@@ -0,0 +1,25 @@
#!/bin/bash
# ITPP Phase Two C1 - DOCKER-USER hardening (management consoles Tailscale-only)
# Host: app3 (152.53.241.111) | Applied 2026-08-13 | Authorized: Phase Two Workstream 1 C1 (Germaine)
# Idempotent: flushes and rebuilds DOCKER-USER (IPv4 + IPv6). Safe to re-run manually.
# Closes (Tailscale-only): buzz-prod-relay 3000/tcp (publicly served via nginx loopback buzz.iamgmb.com).
# Preserves: none (hexclave 8101-8102 is loopback; nginx 80/443 is host INPUT chain).
set -eu
if ! iptables -L DOCKER-USER -n >/dev/null 2>&1; then
iptables -N DOCKER-USER
fi
iptables -F DOCKER-USER
iptables -A DOCKER-USER -s 100.64.0.0/10 -j RETURN
iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
iptables -A DOCKER-USER -i eth0 -j DROP
iptables -A DOCKER-USER -j RETURN
if ip6tables -L DOCKER-USER -n >/dev/null 2>&1; then
ip6tables -F DOCKER-USER
ip6tables -A DOCKER-USER -s fd7a:115c:a1e0::/48 -j RETURN
ip6tables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
ip6tables -A DOCKER-USER -i eth0 -j DROP
ip6tables -A DOCKER-USER -j RETURN
fi
exit 0
@@ -0,0 +1,21 @@
#!/bin/bash
# ITPP Phase Two C1 - DOCKER-USER hardening (management consoles Tailscale-only)
# Host: core (152.53.192.33) | Applied 2026-08-13 | Authorized: Phase Two Workstream 1 C1 (Germaine)
# Idempotent: flushes and rebuilds the DOCKER-USER chain. Safe to re-run manually.
# Closes (Tailscale-only): browserless 3000/tcp, camofox-browser 9377/tcp, uptime-kuma 3001/tcp.
# Preserves: none (no public Docker services on core; Caddy 80/443 is host INPUT chain).
set -eu
if ! iptables -L DOCKER-USER -n >/dev/null 2>&1; then
iptables -N DOCKER-USER
fi
iptables -F DOCKER-USER
iptables -A DOCKER-USER -s 100.64.0.0/10 -j RETURN
iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
iptables -A DOCKER-USER -i eth0 -j DROP
iptables -A DOCKER-USER -j RETURN
# IPv6: Docker ip6tables integration is off on core (no ip6tables DOCKER-USER chain,
# no IPv6 DNAT). [::] publishes are userland docker-proxy (INPUT path) and are already
# blocked by UFW IPv6 default-deny (only 22/80/443/8080-tailscale/51821/8890 allowed).
exit 0
@@ -0,0 +1,12 @@
[Unit]
Description=ITPP P2-C1 DOCKER-USER firewall hardening (Tailscale-only management consoles)
After=docker.service
Wants=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/itpp-docker-user.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target