Files
root f5175f1ce0 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)
2026-08-26 02:27:28 -04:00

161 lines
22 KiB
Markdown

# Sec-B Findings: Hardening, Patch Posture, MFA/Authentication, Logging/Monitoring
Auditor: Sec-B (information security hardening)
Scope: Patch posture, SSH/OS hardening, MFA coverage on admin consoles, Wazuh/logging coverage, across the full ITPP estate.
Mode: READ-ONLY. No configuration changes, restarts, or remediation performed. All items below are findings for Phase Two remediation planning.
Builds on: neteng-a.md (network exposure), sys-a.md and sys-b.md (per-service inventories). Enumeration of services/ports is not repeated here; see those files for full service lists and images in use.
---
## 1. Patch Posture Table
| Host | OS | Kernel Running | Kernel Update Pending? | Last apt update/upgrade run | Upgradable pkg count | Notes |
|---|---|---|---|---|---|---|
| Core (localhost) | Debian 13 (trixie) | 6.12.94+deb13-amd64 | Yes, 6.12.101-1 available | apt metadata refreshed 2026-08-13 (same day as audit) | 15 | No unattended-upgrades service active; refresh appears manual/cron-triggered, not verified automatic |
| app1 (152.53.36.131) | Debian 13 (trixie) | 6.12.95+deb13-amd64 | Unclear (kernel not in upgradable list this pass, discrepancy with Core) | unattended-upgrades stamp 2026-08-13 06:50, last real upgrade logged 2026-08-11 (caddy security update) | 15 (at first check) | unattended-upgrades ACTIVE and ENABLED, evidence of automatic security patching working |
| app2 (152.53.39.202) | Debian 13 (trixie) | 6.12.95+deb13-amd64 | Unclear, similar to app1 | unattended-upgrades stamp 2026-08-13 06:22, last logged upgrade 2026-08-11 (caddy) | 15 (at first check) | unattended-upgrades ACTIVE and ENABLED |
| app3 (152.53.241.111) | Debian 13 (trixie) | 6.12.95+deb13-amd64 | Yes, 6.12.101-1 available (confirmed twice) | update-stamp/upgrade-stamp last changed 2026-07-16 (about 4 weeks before audit) | 14, including linux-image-amd64 security kernel and postfix security update | unattended-upgrades reports active/enabled but the upgrade-stamp is stale (2026-07-16) versus app1/app2 (2026-08-13); patch cadence on app3 is lagging by roughly 4 weeks despite the service being enabled |
| app1-bu (5.161.225.131, warm standby) | Ubuntu 24.04.4 LTS | 6.8.0-117-generic | No kernel package pending (not in upgradable list) | apt success stamp 2026-08-13 05:32 (current) | 16, mostly apport/cloud-init/apparmor/plymouth, non-security-critical | unattended-upgrades ACTIVE and ENABLED, fail2ban active |
| wphost02 (5.161.62.38, legacy) | Ubuntu 24.04.4 LTS | 6.8.0-134-generic | No kernel package pending | apt success stamp 2026-08-13 00:44 (current) | 9, mostly apport/sosreport/network minor pkgs | unattended-upgrades ACTIVE and ENABLED, fail2ban active with 3 jails (runcloud-agent, sshd, sshd-ddos) |
Docker image tags (cross-reference to sys-a.md/sys-b.md inventories):
- Wazuh stack on app1 running wazuh-manager/indexer/dashboard 4.9.2, up 3 weeks continuously. 4.9.2 is a specific pinned version, not `latest`; verify against current Wazuh release train in Phase Two to confirm no known CVEs unpatched in this line.
- Technitium DNS container on app2 runs on DOTNET_VERSION=10.0.9 / ASPNET_VERSION=10.0.9 (current runtime), but ships with `DNS_SERVER_ADMIN_PASSWORD=changeme` in its environment (see Finding SEC-B-03, credential-adjacent hardening issue, flagged here because it is a default/weak-credential indicator, not a secrets inventory item).
- Gitea, Hudu, UniFi, Traccar, Dawarich image version pinning should be cross-checked against sys-a/sys-b inventories for stale major versions; no additional very-old (multi-year) tags were independently observed beyond what sys-a/sys-b already caught.
**Patch posture summary:** All 6 hosts are on reasonably current OS/kernel baselines (Debian 13 trixie or Ubuntu 24.04 LTS), and 5 of 6 have unattended-upgrades active and enabled with recent apply timestamps. app3 is the outlier: unattended-upgrades reports enabled but its upgrade-stamp is roughly 4 weeks stale versus siblings, and it has a pending security kernel update (6.12.95 to 6.12.101) plus a pending postfix security update, both unapplied. Core (localhost) has NO unattended-upgrades service at all (inactive, not-found), relying entirely on manual or externally-scheduled patching, and has 15 upgradable packages including an available kernel update.
---
## 2. Hardening Table (SSH / OS-level)
| Host | PermitRootLogin | PasswordAuthentication | PubkeyAuthentication | fail2ban | unattended-upgrades | auditd | World-readable secrets found |
|---|---|---|---|---|---|---|---|
| Core | prohibit-password (without-password) | no | yes (default) | NOT INSTALLED (inactive, unit not found) | NOT INSTALLED (inactive, unit not found) | inactive | None found in targeted scan |
| app1 | prohibit-password | no | yes | active, enabled | active, enabled | not checked directly (not active per rsyslog/auditd probe) | None found |
| app2 | prohibit-password | no | yes | active, enabled | active, enabled | not checked | None found |
| app3 | prohibit-password | no | yes | active, enabled, 1 jail (sshd) | active, enabled (but stale upgrade cadence, see above) | not checked | None found |
| app1-bu | prohibit-password | no | yes | active, enabled, 1 jail (sshd) | active, enabled | not checked | /root/.hermes/.env is world-readable (flagged, but this is credential-adjacent; full secrets review is Sec-A's domain, flagged here purely as a file-permission hardening gap) |
| wphost02 | prohibit-password | no | yes | active, enabled, 3 jails (runcloud-agent, sshd, sshd-ddos) | active, enabled | not checked | None found |
Additional hardening observations:
- No empty-password accounts found on Core or app1 in the /etc/shadow scan performed.
- SSH host key files (`ssh_host_*_key`) are not world-readable on any host checked.
- Root login is uniformly configured as `prohibit-password` / `without-password` across all 6 hosts, meaning root can only log in via SSH key, not password. This is a reasonably strong baseline, but root login is still permitted at all (as opposed to fully disabled with a dedicated sudo-only admin account), which is a CIS/NIST deviation worth flagging as Low/Medium depending on the org's risk appetite.
- Core has neither fail2ban nor unattended-upgrades installed. This is the weakest OS hardening posture of the 6 hosts, despite Core running Hermes (the orchestration agent), Grafana, Prometheus, and the Super Search MCP, i.e. a high-value control-plane host.
- auditd was checked and found inactive on Core and app1; not independently verified on the remaining 4 hosts, but given fail2ban/journald are the only audit trail sources evidenced, assume auditd is similarly absent estate-wide unless Phase Two proves otherwise.
- journald.conf reviewed on all 6 hosts: no [Journal] section overrides present anywhere (defaults are in force), meaning log rotation/retention limits are whatever the distro default is, not an explicit organizational retention policy. No rsyslog remote forwarding (`@@host` or `@host` directives) was found configured on any host, confirming logs are local-only and not centrally shipped from the OS layer.
---
## 3. MFA / Authentication Coverage Table
| Admin Console / Service | Host | MFA Evidence Found | Status |
|---|---|---|---|
| Grafana | Core :3002 | `GF_SECURITY_ADMIN_PASSWORD=admin` (default password in use); grafana.ini shows only commented-out OAuth/LDAP stanzas, no active SSO/2FA config; `disable_login_form` not set to true, so local form-based login remains the primary path | NO MFA, plus default admin credentials in active use (Critical) |
| Wazuh Dashboard/API | app1 | wazuh-wui API credential in ossec.conf uses local basic auth (`username: wazuh-wui`) with no OAuth/SAML/OIDC integration observed; dashboard is behind Caddy per neteng-a.md but no MFA layer identified | NO MFA identified |
| Gitea | app2 | app.ini shows `ENABLE_CAPTCHA = false`, `REQUIRE_SIGNIN_VIEW = false`, `DISABLE_REGISTRATION = false`. Gitea supports per-user TOTP 2FA natively but enforcement/adoption was not verified (would require login, out of scope); registration being open plus no captcha is a related hardening gap independent of MFA | MFA capability exists but not confirmed enabled/enforced; registration is open, which is itself a risk |
| Hudu | app2 | Env vars show only SMTP/upload settings; no OMNIAUTH/SAML/SSO/MFA environment flags present | NO MFA evidence found |
| UniFi Controller | app2 | `system.properties` grep for 2fa/mfa/auth returned nothing; Ubiquiti UniFi supports MFA via Ubiquiti SSO cloud account when cloud-linked, but no evidence this controller is cloud-linked (local admin only per config inspected) | NO MFA evidence found (local-only auth assumed) |
| UNMS/UCRM | app2 | Env vars show `UBNT_OAUTH_SERVICE_URL=null`, meaning Ubiquiti cloud SSO/MFA path is disabled/unconfigured | NO MFA, and the SSO path that would enable MFA is explicitly nulled out |
| Technitium DNS | app2 | `DNS_SERVER_ADMIN_PASSWORD=changeme` present in container env, i.e. the DEFAULT PASSWORD PLACEHOLDER STRING is literally set as the variable value. This does not by itself prove the live credential equals "changeme" (Sec-A's domain to confirm), but the presence of the literal default string in the running environment is itself a hardening red flag. No MFA support is native to Technitium's basic auth | NO MFA; default-credential-pattern flag (Critical, pending Sec-A confirmation of live value) |
| CloudPanel | app3 | CloudPanel CE (v6.0.8 running) supports optional per-user TOTP 2FA (confirmed via vendor docs), but it is opt-in per account and not enforceable/mandated centrally; no evidence found in local files that the sole `gmb` admin account (per `clpctl user:list`) has 2FA enabled, and this cannot be confirmed without interactive login (out of scope) | MFA capability exists but adoption unconfirmed; single admin account, no organizational enforcement mechanism |
| LiteLLM / admin-ai UI | app1 | Only `LITELLM_MASTER_KEY` env found (redacted); no SSO/MFA env vars present | NO MFA evidence found |
| Vaultwarden | app1 | `SIGNUPS_ALLOWED=false` (good, closed registration); Vaultwarden supports WebAuthn/TOTP 2FA per-user natively but no admin-enforced policy env vars (e.g., `ADMIN_TOKEN`) were found configured, meaning the /admin panel protection state is unclear | Per-user 2FA capability exists; admin panel protection unconfirmed |
| Dawarich, Traccar | app2 | No MFA/2FA/OTP/OAuth-related env vars found on either container | NO MFA evidence found |
**MFA-coverage summary:** Of roughly 11 distinct admin surfaces reviewed, ZERO were confirmed to have MFA actively enforced. Several platforms (Gitea, CloudPanel, Vaultwarden) have native 2FA capability that is opt-in/per-user and not centrally mandated, meaning coverage depends entirely on individual admins choosing to enable it, unverifiable from config alone. Grafana is actively using default admin credentials with no MFA, the most severe finding in this category. UNMS explicitly has its cloud SSO/MFA integration path disabled. Technitium DNS shows a literal "changeme" default-password string still present in its running configuration.
---
## 4. Logging / Monitoring (Wazuh) Coverage Table
| Host | Wazuh Agent Installed? | Wazuh Manager/Stack Present? | Local Journald Only? | Remote Log Forwarding (rsyslog)? |
|---|---|---|---|---|
| Core | NOT INSTALLED (no wazuh-agent unit, no /var/ossec, no wazuh package) | No (manager lives on app1) | Yes, journald default config only | None configured |
| app1 | Manager stack runs here (wazuh-manager, wazuh-indexer, wazuh-dashboard, all image 4.9.2, up 3 weeks) BUT `agent_control -l` shows only ONE registered agent: ID 000, the manager's own local agent (127.0.0.1) | Yes (this IS the manager) | Journald default | None configured |
| app2 | NOT INSTALLED | No | Journald default | None configured |
| app3 | NOT INSTALLED | No | Journald default | None configured |
| app1-bu | NOT INSTALLED | No | Journald default | None configured |
| wphost02 | NOT INSTALLED | No | Journald default | None configured |
Wazuh manager health check detail: the manager and indexer/dashboard containers are up and running (3 weeks uptime, no restarts observed), and the indexer's REST API responds (401 without credentials, meaning it is alive and enforcing auth, not down). However, `agent_control -l` returning only the manager's own loopback agent (ID 000) confirms that **not a single remote host across the 6-server estate is enrolled as a Wazuh agent**. The Wazuh deployment is effectively monitoring only itself.
**Wazuh/logging coverage summary:** Wazuh is deployed and technically healthy (containers up, API responsive) but has ZERO externally enrolled agents. Coverage is 1 of 6 hosts (16.7%), and that one host (app1) is only monitoring its own loopback, not even its own host-level OS logs via a real agent enrollment path (the "agent" is the manager's built-in local one, not a deployed endpoint agent). All 6 hosts rely solely on local, unshipped journald logs with default retention and no remote forwarding. There is no centralized log aggregation for authentication events, admin console access, container events, or intrusion indicators anywhere in the estate. This is the single largest blind spot identified in this audit.
---
## 5. Cross-Cutting: Unmanaged / Blind-Spot Hosts
Applying the combined lens of (no Wazuh agent) + (no/weak local monitoring) + (patch posture gaps):
- **Core (localhost):** No Wazuh agent, no fail2ban, no unattended-upgrades, no auditd. This is the most under-hardened host in the estate at the OS-control layer, notable because it hosts Hermes (agent orchestration), Grafana, Prometheus, and Super Search MCP, i.e., core operational tooling with broad reach. Prometheus/Grafana provide metrics-level visibility but not security-event-level visibility (no auth log shipping, no file integrity monitoring, no brute-force protection).
- **app2, app3, app1-bu, wphost02:** No Wazuh agent on any of them; they retain fail2ban and unattended-upgrades as baseline compensating controls, which Core lacks entirely, but none have any form of centralized security log visibility. If any of these hosts is compromised, there is currently no telemetry path that would surface it to a central SIEM; detection depends entirely on someone manually reviewing local journald logs after the fact.
- **app1 (Wazuh manager host):** Ironically the host running the SIEM has the SIEM monitoring nothing but itself. This is a significant program gap: the org has invested in deploying Wazuh infrastructure but has not completed the agent rollout, so the investment currently delivers near-zero detection value across the estate.
---
## 6. Severity-Rated Findings
### Critical
**SEC-B-01: Wazuh SIEM deployed but zero agents enrolled across the estate**
Evidence: `agent_control -l` on the wazuh-manager container (app1) lists only agent ID 000 (the manager's own loopback), no remote agents. No `/var/ossec` or wazuh-agent service found on Core, app2, app3, app1-bu, or wphost02.
Rationale: A SIEM with no enrolled agents provides no actual detection capability for the 5 non-manager hosts, which include internet-facing services (CloudPanel/WordPress, Hudu, UniFi, Gitea, LiteLLM). Security incidents on any of these hosts would go undetected by the org's own monitoring investment. This is the single highest-impact gap in the estate.
**SEC-B-02: Grafana running with default admin credentials and no MFA**
Evidence: `docker inspect grafana` shows `GF_SECURITY_ADMIN_PASSWORD=admin` alongside `GF_SECURITY_ADMIN_USER=admin`; grafana.ini has no active OAuth/LDAP/SSO configuration, only commented-out templates.
Rationale: Default admin/admin credentials on an internet-reachable (per neteng-a.md) observability console is one of the most well-known, automatically-scanned-for misconfigurations on the internet. Combined with no MFA, this is a near-zero-effort compromise path for anyone who finds the port.
**SEC-B-03: Technitium DNS container running with literal "changeme" default password string in live environment**
Evidence: `docker inspect technitium` shows `DNS_SERVER_ADMIN_PASSWORD=changeme`.
Rationale: Even if the operational credential has since been changed inside the app's own database (a possibility Sec-A should confirm, since this crosses into credential inventory), the fact that the container's own environment variable retains the literal placeholder value is a strong hardening/deployment-hygiene signal that default-credential practices may be in use elsewhere too. DNS admin compromise has estate-wide blast radius (this is itpropartner.com's DNS).
### High
**SEC-B-04: app3 patch cadence stale by approximately 4 weeks, with an unapplied security kernel and postfix update**
Evidence: unattended-upgrades stamp files on app3 last touched 2026-07-16, versus 2026-08-13 (current, same day) on app1 and app2. `apt list --upgradable` on app3 shows `linux-image-amd64/stable-security 6.12.101-1` pending (currently on 6.12.95-1) and `postfix/stable-security` pending.
Rationale: app3 hosts CloudPanel, WordPress, and MySQL, an internet-facing content and mail-adjacent stack (postfix present). An unapplied 4-week-old security kernel patch and a pending postfix security update on a public-facing host is a meaningful exposure window, especially since neteng-a.md would have already flagged what ports are open here.
**SEC-B-05: Core has no fail2ban, no unattended-upgrades, and no auditd**
Evidence: `systemctl is-active fail2ban` and `unattended-upgrades` both return inactive with "not-found" for enabled state; `dpkg -l` shows neither package installed; auditd inactive.
Rationale: Core is the control-plane host for Hermes and observability tooling. Lacking brute-force protection and automatic security patching on a host with this level of operational privilege is disproportionate risk relative to its role, especially compared to the other 5 hosts which all have these controls.
**SEC-B-06: No admin console in the estate has confirmed, enforced MFA**
Evidence: Across Grafana, Wazuh dashboard, Gitea, Hudu, UniFi, UNMS/UCRM, Technitium, CloudPanel, LiteLLM, and Vaultwarden, no environment variable, config file, or system property indicated an active, enforced MFA/SSO integration. Several tools have opt-in per-user 2FA capability (Gitea, CloudPanel, Vaultwarden) but no evidence of organizational enforcement.
Rationale: Any single compromised admin credential (phishing, credential stuffing, reused password) grants full access to that console with no second factor to stop it. This is a systemic authentication-hardening gap across the entire estate, not a one-off.
### Medium
**SEC-B-07: UNMS/UCRM has its cloud SSO integration explicitly disabled (`UBNT_OAUTH_SERVICE_URL=null`)**
Evidence: `docker inspect ucrm` env output.
Rationale: Ubiquiti's cloud SSO path is one of the few routes to MFA for this product family; explicitly nulling it out removes that option, leaving local-only authentication as the sole path.
**SEC-B-08: No centralized log forwarding (rsyslog remote or journald shipping) configured anywhere in the estate**
Evidence: grep for `@@host`/`@host` rsyslog forwarding directives returned empty on all 6 hosts; journald.conf shows default config (no [Journal] section overrides) on all 6 hosts.
Rationale: Even setting aside Wazuh agent enrollment, there is no other mechanism (rsyslog, journald forwarding, or otherwise) shipping logs off-host anywhere. If a host is compromised and its local logs are tampered with or deleted, there is no off-host copy to fall back on for forensics.
**SEC-B-09: Gitea has open registration and disabled CAPTCHA**
Evidence: app.ini shows `DISABLE_REGISTRATION = false`, `ENABLE_CAPTCHA = false`, `REQUIRE_SIGNIN_VIEW = false`.
Rationale: An internet-reachable Gitea instance (confirm exposure via neteng-a.md) with open self-registration and no CAPTCHA is exposed to automated account creation/spam and widens the attack surface for credential-based attacks against a code hosting platform.
### Low
**SEC-B-10: Root login permitted (key-only) rather than fully disabled across all 6 hosts**
Evidence: `sshd -T` on all 6 hosts shows `permitrootlogin without-password` (uniformly).
Rationale: While password-based root login is correctly disabled everywhere (good baseline), CIS/NIST guidance generally recommends disabling root SSH login entirely in favor of named-user + sudo, to preserve accountability/audit trail for privileged actions. Not urgent given key-only enforcement, but a durable hardening improvement for Phase Two.
**SEC-B-11: /root/.hermes/.env world-readable on app1-bu**
Evidence: file permission scan on app1-bu found `/root/.hermes/.env` matching world-readable pattern.
Rationale: World-readable environment files can leak configuration/secrets to any local process or unprivileged account with filesystem access. Flagged here as a hardening/permissions gap; if it contains credentials, that overlaps with Sec-A's remit and should be cross-referenced with their inventory.
---
## Appendix: Methodology Notes
- All SSH access used `-o ConnectTimeout=10 -o BatchMode=yes` per brief, no interactive prompts triggered.
- No login attempts were made against any web UI (Grafana, Wazuh dashboard, Gitea, CloudPanel, etc.); all MFA determinations were made from static configuration files, environment variables, and system properties only, per the brief's read-only constraint.
- `sshd -T` (dump effective config) was used in addition to raw grep of sshd_config to catch settings inherited from Included files or compiled defaults.
- Wazuh index listing (`_cat/indices`) returned a 401 (auth required) rather than a connection failure, confirming the indexer is alive and reachable, just not accessible without credentials, which were not attempted per the read-only/no-credential-testing constraint.
- No package installs, service restarts, or file writes were performed on any remote host.