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

38 KiB

Sec-A Findings: IAM, Accounts, Secrets Location, and Least Privilege

Auditor: Sec-A (IAM and secrets inventory) Scope: Account inventory, sudo/root privilege, SSH access and key attribution, MFA coverage on admin surfaces, secrets location (no values captured, only presence/staleness), shared vs personal credential use, least-privilege posture. Mode: READ-ONLY. All evidence below comes from local audit capture files already on disk (/root/audit_core.txt, /root/audit_app1.txt, /root/audit_app2.txt, /root/audit_app3.txt, /root/audit_app1bu.txt, /root/audit_wphost02.txt), collected in a prior session via read-only SSH/curl. No new commands were run against remote hosts to produce this file. All credential values are redacted as [REDACTED]; this document names locations and types only. Overlap note: hardening, patch posture, MFA configuration detail (per-tool), and Wazuh/logging coverage are Sec-B's domain (see sec-b.md). This file focuses on accounts, SSH, secrets location, and least-privilege, and cross-references Sec-B's MFA table rather than repeating it in full.


1. Per-Server Account Inventory

Six hosts: Core (Netcup, this host), app1 (152.53.36.131), app2 (152.53.39.202), app3 (152.53.241.111), app1-bu (Hetzner, warm standby), wphost02 (Hetzner, legacy WordPress/RunCloud).

Host Total /etc/passwd entries Interactive shell accounts (non-system) Notable interactive accounts sudo group members Passwordless sudo (sudoers.d)
Core 34 ippadmin, postgres, scanuser (3) postgres has /bin/bash (service account with a real shell, unusual), scanuser is locked/no-password, purpose not identified in captured data, flag for Phase Two follow-up ippadmin ippadmin ALL=(ALL) NOPASSWD:ALL
app1 24 ippadmin (1) Clean, minimal footprint ippadmin none in sudoers.d beyond README (no explicit NOPASSWD entry for ippadmin found on app1, unlike Core/app2/app3, worth confirming)
app2 25 ippadmin, unms (2) unms service account is in the docker group, meaning it can run containers with root-equivalent power (see Least-Privilege section) ippadmin ippadmin ALL=(ALL) NOPASSWD:ALL
app3 37 ippadmin plus roughly 28 per-client/per-site accounts (gmb, myverdicttank, mainwp, boxpilotlogistics, modelortho, intelsight, mockups, modelorthowww, timapta, hexclave-api, apx, panel, voipsimplicity, transitpin-dash, vigilanttac, proposals, transitpin, docs, buzz, support, myvoip, hexclave-dash, iAmGMB, drecovery, forms, katiewatts, clp, debtrecoveryexperts) This is CloudPanel's per-site account model: one Linux user per hosted site/client. Large blast surface simply by account count, even though most appear unused for SSH (see Section 2) ippadmin ippadmin ALL=(ALL) NOPASSWD:ALL; also clp ALL=(ALL) NOPASSWD:ALL (CloudPanel's own management account) plus a wrapper rule ALL ALL=(ALL) NOPASSWD: /usr/bin/clpctlWrapper letting any account run the CloudPanel control wrapper as root
app1-bu 32 none beyond root Warm standby has essentially no interactive non-root accounts provisioned yet (matches its role as a failover target, not yet fully onboarded) none (sudo group empty) root ALL=(ALL) NOPASSWD:ALL via cloud-init default, standard for that image
wphost02 37 runcloud, ippadmin (2) runcloud is the RunCloud panel's management account ippadmin root ALL=(ALL) NOPASSWD:ALL (cloud-init default) and ippadmin ALL=(ALL) NOPASSWD:ALL

Orphaned/unclear-purpose accounts:

  • Core: scanuser (UID 1001, locked password, /bin/bash shell, home /home/scanuser). No corresponding SSH key, cron job, or docker context found in the captured data tying this account to an active purpose. Flag as a candidate for removal or documentation in Phase Two; do not assume it is safe to delete without confirming with Germaine first (read-only rule).
  • app3: roughly 28 per-client accounts. Most have empty authorized_keys files (see Section 2), meaning they exist as CloudPanel site-owner accounts but show no evidence of direct SSH login capability. This is consistent with CloudPanel's model (site isolation, not direct-login accounts) but should be confirmed against actual client billing/engagement status in Phase Two; several of these usernames (debtrecoveryexperts, boxpilotlogistics, transitpin, hexclave-api) correspond to active named client projects, others (katiewatts, iAmGMB) look personal/less clearly tied to a current engagement and are worth a lifecycle check.
  • wphost02: runcloud and mysql/memcache/beanstalkd service accounts. These are RunCloud-panel defaults consistent with a legacy managed WordPress host; no evidence of misuse, flagged only for completeness since wphost02 is described in the brief as a legacy/sunset host.

Password/shadow posture: root:PASSWORD_HASH_SET on Core, app1, app2, app3, and wphost02 (root has a live password hash and could theoretically log in with a password if PermitRootLogin allowed it; Sec-B confirms PermitRootLogin is key-only across the estate, which mitigates this). On app1-bu, root shows LOCKED_BUT_HASH_PRESENT, a slightly different but comparable state. All other system accounts show LOCKED_NO_PASSWORD, which is expected and healthy for service accounts.


2. SSH Access Inventory

2.1 Key inventory found across authorized_keys files

Key comment/label Fingerprint (truncated) Seen on
itpp-main-server SHA256:oqKRvPA...D7E Core (root and ippadmin)
germaine@itppartner SHA256:dDbLH+b...y3I Core, app1, app2, app3 (root and ippadmin on each)
wisp-backup SHA256:MxQw1oh...DcI Core (root and ippadmin)
itpp-infra SHA256:Jxh0bbT...8uQ Core, app1, app2, app3, app1-bu (root and/or ippadmin on each); also present on wphost02 under a second fingerprint variant, see below
g@germaine@itpropartner.com SHA256:7QuUx/s...8lc Core (root only)
RunCloud_Server_Service SHA256:qMm5i4/...IUo wphost02 (root only, RunCloud-managed)
itpp-infra RUNCLOUD1783623009 / itpp-infra (second fingerprint) SHA256:jnmiJb9...v3U wphost02 (root only)

2.2 Blast radius assessment

Confirmed live (not just documented): the itpp-infra key (fingerprint SHA256:Jxh0bbT9dUV3q1DYYB3hHyhy/1TDj7Q8U4xrVmB38uQ) is present in authorized_keys on Core, app1, app2, app3, and app1-bu, five of the six servers in scope. This confirms the prior session's documentation-based claim with live data. wphost02 has a different key under the same "itpp-infra" label/comment (different fingerprint), so it is not the identical key, but it is functionally the same access pattern using the same naming convention.

Severity implication: compromise of the single private key corresponding to fingerprint SHA256:Jxh0bbT9dUV3q1DYYB3hHyhy/1TDj7Q8U4xrVmB38uQ grants SSH access to 5 of 6 servers in the estate (Core, app1, app2, app3, app1-bu), each as root or root-equivalent (via ippadmin's passwordless sudo). This is the single largest blast-radius item in the IAM domain. There is no key segmentation by host, role, or trust tier; one credential spans the entire estate except the legacy wphost02 host.

germaine@itppartner key is present on Core, app1, app2, and app3 (4 of 6 hosts), a second broad-access personal key, smaller blast radius than itpp-infra but still spanning 4 hosts.

Single-purpose keys (itpp-main-server, wisp-backup, g@germaine@itpropartner.com) are scoped to Core only, a healthier pattern; these do not multiply blast radius.

app3 per-client accounts: of the roughly 28 per-client home directories checked, all sampled authorized_keys files under those accounts were empty (no keys present). This means those accounts cannot currently be reached by direct SSH key login, which is a good containment property for a shared multi-tenant host, assuming CloudPanel manages access to those accounts through its own panel rather than SSH.

app1-bu (warm standby): only the itpp-infra key is authorized, and only for root; there is no germaine@itppartner personal key here. This is a narrower access surface, appropriate for a less frequently accessed failover host, but means the standby depends entirely on one shared key with no personal-key fallback.

wphost02: RunCloud's own service key plus two itpp-infra-labeled keys with different fingerprints are present; no personal (germaine@itppartner) key was found on this host, consistent with it being flagged as legacy/lower-touch in the brief.

2.3 Login activity signal

Last-login data was only meaningfully populated on app1-bu (all accounts "never logged in", consistent with a cold standby that has not yet been used) and wphost02 (root last login 2026-07-21 from 76.195.7.60; ippadmin last login 2025-12-31 from the same IP). Core, app1, app2, and app3 returned no populated last-login rows in the captured data, meaning login history could not be independently verified for those four hosts from this dataset. This is a visibility gap, not a finding of absence.


3. MFA Matrix (Cross-Reference to Sec-B)

Sec-B's findings file (sec-b.md, Section 3) contains the full MFA coverage table across 11 admin surfaces and is the authoritative source for MFA configuration detail. Sec-A's contribution here is the account-and-access angle for each surface: how many people can reach it and via what credential, since that is what MFA (or its absence) is meant to protect.

Admin Surface Host Accounts with access (per this session's data) MFA status (per Sec-B) Sec-A note
Gitea app2 Single admin account (ippadmin, is_admin=true, confirmed live in the prior session via API) Capability exists, not confirmed enforced; open registration One shared admin account for the entire code-hosting platform is itself an IAM concentration risk independent of MFA; see Section 5
Grafana Core Uses admin/[REDACTED default password] per Sec-B No MFA, default credential in use Confirms Sec-B's Critical finding from the accounts angle: this is not even a personal account, it is a shared default account
CloudPanel app3 Single admin account gmb observed via clpctl user:list (per prior session) Capability exists, adoption unconfirmed Single admin account matches the single-admin pattern seen on Gitea; worth asking Germaine whether other staff need scoped CloudPanel accounts rather than sharing gmb
Technitium DNS app2 Environment shows DNS_SERVER_ADMIN_PASSWORD set to the literal default placeholder string No MFA; default-credential pattern Sec-A could not confirm the live in-app credential value without an authenticated read, which the read-only rule prohibits; flagged as an access-verification gap for Phase Two
Wazuh dashboard, Hudu, UniFi, UNMS/UCRM, LiteLLM admin, Vaultwarden, Dawarich, Traccar app1/app2 No per-user account enumeration was possible from the captured local-file data (these are app-level accounts inside each service's own database, not OS accounts) See Sec-B's table This is a genuine visibility gap: Sec-A cannot state how many people have login credentials to these consoles without an authenticated read of each app's user table, which is out of scope for read-only discovery. Flagging per brief rule 4 (flag insufficient access/visibility, do not guess).

MFA gap count (IAM-relevant): 8 of 8 checkable admin surfaces show no confirmed enforced MFA (Gitea, Grafana, CloudPanel, Technitium, Wazuh dashboard, Hudu, UniFi, UNMS/UCRM), consistent with Sec-B's estate-wide finding.


4. Secrets-Location List

No credential values are reproduced below. Entries are location, type, and staleness/rotation signal only.

4.1 Confirmed live secrets locations (.env files, values redacted at capture time)

Host Path Secret types present Last modified Staleness signal
Core /opt/shopping-cart/.env Amazon Associates tag, SMTP credentials 2026-07-27 Recent
Core /opt/mooresunnydaze/.env Admin API key, Stripe secret key, Stripe webhook secret 2026-08-07 Recent
Core /opt/hermes-voice/.env Hermes API key/session key, xAI API key 2026-07-28 Recent
Core /opt/voice-agent/.env Hermes API key/session key 2026-07-26 Recent
app1 /docker/n8n/.env n8n encryption key, Postgres password 2026-07-17 Recent
app1 /root/docker/docuseal/data/docuseal/docuseal.env Database URL, secret key base 2026-07-28 Recent
app1 /root/docker/litellm/.env LiteLLM master key, salt key, Postgres password, UI credentials 2026-07-15 ~1 month old at capture time
app1 /root/docker/litellm/.env.pre-keyfix-20260714-130347 Same secret types as above (pre-rotation snapshot) 2026-07-14 Stale duplicate. This is a leftover pre-rotation backup sitting next to the live config; the filename itself documents a key-fix event on 2026-07-14, meaning this file likely contains a superseded credential set that was never deleted. Medium finding, see Section 7.
app1 /root/docker/super-search/.env Exa, Firecrawl, OpenCorporates API keys 2026-07-15 Recent
app1 /root/docker/twenty/.env Encryption key and app config 2026-07-28 Recent
app1 /root/docker/wazuh/.env Wazuh build/version config (lower sensitivity, mostly version pins) 2026-07-19 Recent
app2 /opt/bookstack/config/www/.env DB, SMTP, S3 credentials, two-factor key present Not independently dated in this pass
app2 /root/docker/dawarich/.env Database password, Postgres password, secret key base 2026-07-22 Recent
app2 /root/docker/hudu/.env Hudu app secrets present Not independently dated in this pass
app2 /root/docker/technitium/docker-compose.yml (inline env, not a .env file) DNS_SERVER_ADMIN_PASSWORD set to the literal default placeholder string in the compose file itself 2026-08-04 Recent file, but the value itself is a stale/never-rotated default, see Section 7
app3 /home/clp/htdocs/app/files/.env Application credentials (CloudPanel-managed app) present Live copy
app3 /home/clp/backups/2026-08-11_04-15-01/app/files/.env, .../2026-08-12_04-15-01/..., .../2026-08-13_04-15-01/... Same credential set as above 2026-08-11, -12, -13 Three consecutive daily backup snapshots each retain a full plaintext copy of the same .env. Every backup rotation multiplies the number of at-rest plaintext credential copies without any additional access control on the backup directory itself. Medium finding, see Section 7.
app3 /opt/buzz/deploy/compose/.env Buzz relay service credentials present Not independently dated in this pass
app3 /opt/docs-auth/docs-auth.env Docs site auth credentials present Not independently dated in this pass
app3 /opt/hexclave/hexclave.env Hexclave app credentials present Not independently dated in this pass
app3 /var/www/msp-forms/.env MSP forms app credentials present Not independently dated in this pass
app1-bu /root/.hermes/.env, /root/.hermes/.env.telegram-backup Hermes core config and Telegram bot token present This is the same file flagged by Sec-B as world-readable on app1-bu; that permissions gap plus this being a live credential file is a compounding issue, cross-referenced in Section 7
app1-bu /root/.hermes/state-snapshots/20260703-011737-pre-update/.env, /root/.hermes/state-snapshots/20260711-150407-pre-update/.env Full Hermes config snapshots from pre-update states 2026-07-03, 2026-07-11 Stale historical copies retained on disk, same document-sprawl pattern as the litellm pre-keyfix file

4.2 Plaintext credential document sprawl (key-inventory.md)

This is the single most significant secrets-location finding of this audit, and it was independently re-confirmed in this session against the raw file inventory rather than only relying on the prior session's account of it.

Ten copies of key-inventory.md exist across the filesystem. File-level comparison (size, modification time, and a redaction-marker check that counts occurrences of [REDACTED]/Vaultwarden/Hudu-style placeholder references without reading or reproducing any actual secret value):

Path Size Modified Redaction-marker count Assessment
/root/projects/itpp-infrastructure/docs/infrastructure/key-inventory.md 12,497 B 2026-08-08 27 Sanitized (canonical). This is the properly redacted version meant for the docs repo.
/root/itpp-docs/docs-source/itpp-infrastructure/key-inventory.md and 3 identical copies under /tmp/itpp-docs-build/, /tmp/audit-export/, /tmp/audit-repos/, /tmp/tmp.YZCSHQoVPf/ 12,503 B each, identical md5 670625a7... 2026-08-08/09 27 each Sanitized, duplicate build/export artifacts of the canonical doc. Document sprawl, not a secrets leak, but five redundant copies of the same file across /tmp build directories is untidy and should be cleaned up as part of normal repo hygiene.
/root/projects/itpp-infrastructure/.backup-before-sanitize-20260723/key-inventory.md 13,136 B 2026-07-23 3 (low redaction-marker count relative to the sanitized versions) CRITICAL: unsanitized, plaintext credential values. File header reads "Generated: 2026-07-23... Contains real credentials". This is a pre-sanitization leftover from a July 23 cleanup pass that documented real root passwords for app1/app2/app3, cloud provider API tokens, S3 access keys, and multiple service tokens, per its own header and per the prior session's confirmed read. It is .gitignore'd (not in git history) but sits unencrypted, root:root, mode 600, on Core's local disk.
/root/.hermes/references/key-inventory.md 13,161 B 2026-08-08 3 (low, matching the unsanitized pattern) CRITICAL: a second, independent unsanitized copy, not identified by name in the prior session's summary. Same header pattern ("Generated: 2026-07-23... CLASSIFIED: Contains real credentials"), root:root, mode 600. This copy lives inside the Hermes agent's own reference-file directory, meaning it is reachable by any skill or process that can read Hermes's reference files, a broader exposure surface than a one-off backup directory.
/root/.hermes/skills/devops/hudu-management/references/key-inventory.md and /root/.hermes/.backups/hermes-backup-2026-07-22/skills/devops/hudu-management/references/key-inventory.md 4,022 B and 3,794 B 2026-07-22 9 each Smaller, older API-key-name-only lists (structure/labels, not full values per the prior session's read), lower risk but still worth folding into the cleanup since they are stale duplicates of a smaller scope.

Net finding: there are two full unsanitized plaintext copies of the complete credential inventory (not one, as the prior session reported), both root:root mode 600 so restricted to local root access only, but both should have been shredded after the sanitized canonical version was created on 2026-08-08. Local root access to Core is not a trivial bar (this is the Hermes host with the broadest operational reach in the estate), so "root-only" is a meaningful but not sufficient mitigation.

4.3 Secrets-sprawl git-grep results (per-repo hit counts, tracked files only)

The following are word-pattern hits (password/secret/token/api_key as text) across git-tracked files in every .git repo found on each host. These are not confirmed leaked credentials; they are candidate locations that need per-file triage before being treated as a real secrets-in-git problem.

Host Repos scanned Total file hits Largest single contributor
Core 68 6,296 /usr/local/lib/hermes-agent (2,704 hits, almost entirely documentation/code referencing the words "secret"/"token" as identifiers, not literal values)
app1 6 27 /root/docker/wazuh (workflow/config files referencing "secret" as a term)
app2 1 1,275 /opt/ragflow (single large open-source repo, README/workflow/test files)
app3 2 719 /opt/buzz (open-source repo, same pattern: workflow files, .env.example, changelogs)
app1-bu 3 5,504 /root/.hermes/docker/twenty (2,741 hits) and /usr/local/lib/hermes-agent (2,761 hits), both open-source vendor codebases mirrored onto the standby host
wphost02 0 0 No .git repos found on this host

Assessment: Medium, not Critical. The overwhelming majority of hits are in vendored open-source code (hermes-agent, twenty, ragflow, buzz, theHarvester, sherlock) where "secret", "token", or "api_key" appear as variable names, documentation words, or CI workflow keys, not as literal credential values. The one path worth a manual look is /root/projects/itpp-infrastructure itself (44 hits on Core, per the prior session's account, and 2 hits on app1-bu limited to README.md and sites/app3.md), since that is the org's own infrastructure documentation repo, not a third-party vendor codebase, and is the most likely place an actual value could have been accidentally committed. This needs a manual per-file read in Phase Two; it was not performed in this session because it would require opening and reading each of the 44 hit locations individually, and the tool budget for this task was reserved for account/SSH/secrets-location work per the task's explicit instructions.

4.4 Configuration files with credential-relevant names (non-.env)

Notable non-.env files that could contain live secrets, filtered from a much larger CONFIG_SECRET_FILES sweep that was mostly PHP extension .ini files (not credential-relevant, omitted from this table):

Host Path Type
Core /root/docker/monitoring/grafana/grafana.ini Grafana config, cross-referenced to Sec-B's default-admin-password finding
app1 /root/docker/litellm/config.yaml LiteLLM routing/model config, may reference upstream API keys by env var name
app2 /opt/gitea/data/gitea/conf/app.ini Gitea app config, contains SECRET_KEY/INTERNAL_TOKEN/JWT_SECRET per the prior session's confirmed (value-redacted) read
app3 /etc/gitea/app.ini A second Gitea config path found on app3; worth confirming in Phase Two whether this is a stale leftover from a prior Gitea install location or an active second instance, since the brief describes Gitea as living on app2
Core /root/.config/goose/secrets.yaml Goose CLI agent secrets file

5. Shared vs Personal Credential Assessment

  • Gitea (app2): one shared admin account (ippadmin), confirmed live via API in the prior session (is_admin=true, last login 2026-08-10). No per-person Gitea accounts exist. Anyone who knows this one credential, or holds a valid API token for it, has full administrative control over every source repository in the org.
  • CloudPanel (app3): one admin account (gmb) per clpctl user:list from the prior session. Same single-shared-account pattern.
  • SSH root/ippadmin access: access to Core, app1, app2, and app3 is governed by the same small set of keys (itpp-infra, germaine@itppartner) shared across hosts rather than per-host or per-person keys. There is no evidence of individual named-user SSH accounts distinct from root and ippadmin; all administrative SSH access funnels through these two shared identities.
  • ippadmin has passwordless full sudo (NOPASSWD:ALL) on Core, app2, app3, and wphost02 (and root has the OS-level password hash set on top of that). This means any of the keys that unlock ippadmin's SSH access are equivalent to unlocking root on that host, with no additional authentication step (no MFA, no password re-prompt) in between.
  • No individual-user accountability mechanism observed. Because SSH access and sudo are both shared/group-level rather than per-person, there is no way, from the data available, to attribute a specific administrative action on any of these hosts to a specific individual. This is a foundational access-control gap that other findings (MFA gaps, unclear last-login history) compound.
  • app3's per-client accounts are the one place where the estate does practice some separation (one Linux account per hosted site), which is appropriate multi-tenant hygiene, but even this layer sits underneath the same shared ippadmin/root access that can reach every one of those accounts.

6. Least-Privilege Findings

  • Almost every Docker container across the estate runs as root inside the container (user=[root(default)]). This was true for the large majority of containers sampled on Core, app1, app2, and app3, including services with no operational need for root (e.g., microbin, searxng, telegraf, mikrotik-exporter, uptime-kuma on Core; komodo-core, docuseal, twenty-db, n8n-postgres, mcp-browser, mcp-email, mcp-git, mcp-filesystem, super-search on app1; bookstack, bookstack-db, docker-ragflow-cpu-1, docker-mysql-1, docker-minio-1, docker-redis-1, technitium, dawarich_sidekiq/app/db/redis, traccar, gitea, unms-nginx, unifi-controller, hudu-app/db/worker/redis, unms-api, ucrm, unms-postgres, unms-siridb, unms-fluentd on app2; hexclave-postgres, hexclave-clickhouse, buzz-prod-postgres/redis/minio on app3). None were flagged as privileged=true (a worse container-escape-relevant setting), which is a positive, but running as the root UID inside the container still means a container-breakout vulnerability in any of these images grants root, not a scoped user, on the host's container runtime layer.
  • Some containers do practice least privilege correctly and are worth naming as the good pattern to extend: browserless (blessuser), grafana (472, Grafana's own non-root UID), prometheus (nobody), openwebui (0:0 is still root, note this is actually still root, listed for completeness), twenty-server/twenty-worker (1000), kokoro-tts (appuser), wazuh-dashboard/wazuh-indexer (their own service users), vaultwarden is root but is a well-maintained image; unms-device-ws-* and unms-rabbitmq/unms-netflow containers (1001), hexclave-server (node), hexclave-cron (curl_user), buzz-prod-relay-1 (buzz:buzz). These show the operators clearly know how to configure non-root containers when the upstream image supports it; the gap is inconsistent application, not lack of capability.
  • app2's unms service account is a member of the docker group. Group membership in docker is functionally equivalent to root on the host, since a member can run any container with arbitrary host bind-mounts. If unms is meant to be a scoped service account for the UNMS/UCRM stack, its docker group membership defeats that scoping and should be reviewed in Phase Two; a properly least-privileged setup would run UNMS's containers under a system-level Docker Compose invocation by ippadmin/root rather than granting the unms account itself docker group membership.
  • ippadmin ALL=(ALL) NOPASSWD:ALL on 4 of 6 hosts (Core, app2, app3, wphost02) is the single most consequential least-privilege gap in the account layer: this is unrestricted, no-password-prompt root escalation for a shared account, with no command allowlisting or logging requirement built into the sudoers entry itself.
  • ALL ALL=(ALL) NOPASSWD: /usr/bin/clpctlWrapper on app3 allows any account, including the low-privilege per-client site accounts, to invoke the CloudPanel control wrapper as root without a password. This needs a Phase Two review of exactly what clpctlWrapper can do; if it exposes any file-write or command-injection surface, every one of the ~28 per-client accounts on app3 effectively has a root-escalation path.
  • Core's postgres account has an interactive /bin/bash shell rather than the more typical /bin/false or /usr/sbin/nologin for a database service account. This is a smaller-scale least-privilege deviation worth tightening if there is no operational reason for interactive postgres logins.

7. Severity-Rated Findings

Critical

SEC-A-01: Two unsanitized plaintext copies of the complete infrastructure credential inventory exist on Core's local disk Evidence: /root/projects/itpp-infrastructure/.backup-before-sanitize-20260723/key-inventory.md (13,136 bytes, root:root, mode 600, modified 2026-07-23) and /root/.hermes/references/key-inventory.md (13,161 bytes, root:root, mode 600, modified 2026-08-08) both carry the header "Contains real credentials, store encrypted, never email plaintext" and show a low redaction-marker count consistent with the prior session's confirmed read that these contain real root passwords, cloud provider API tokens, S3 keys, and service tokens in plaintext. Rationale: A single successful compromise of the Core host at the root level, or any process/skill with read access to Hermes's own reference-file directory, would expose essentially every credential the organization relies on, in one file, in one read. The sanitized canonical version already exists at docs/infrastructure/key-inventory.md; these two copies are leftovers that were never deleted after that sanitization work was done on 2026-08-08, and one of them (the Hermes references copy) is in a location that a wider range of automated processes could plausibly touch than a one-off backup folder. This should be shredded (not just deleted) as a Phase Two remediation item; per the read-only rule, Sec-A did not delete it.

SEC-A-02: A single SSH key grants access to 5 of 6 servers in the estate, each with passwordless root escalation on top Evidence: The itpp-infra key (fingerprint SHA256:Jxh0bbT9dUV3q1DYYB3hHyhy/1TDj7Q8U4xrVmB38uQ) is present in authorized_keys on Core, app1, app2, app3, and app1-bu. On Core, app2, and app3, the account it can reach (ippadmin or root) has NOPASSWD:ALL sudo. Rationale: There is effectively one credential standing between an attacker and full administrative control of 5 of the org's 6 servers. If this key is ever exposed (leaked in a repo, phished, copied to a compromised laptop), the blast radius is close to the entire estate, in a single step, with no second factor to slow it down. This is the highest-leverage single point of failure identified in the IAM domain.

High

SEC-A-03: Gitea and CloudPanel each rely on a single shared admin account with no per-person accountability Evidence: Gitea's GET /api/v1/admin/users (prior session, live read) returned exactly one account, ippadmin, is_admin=true. CloudPanel's clpctl user:list (prior session) showed a single admin account, gmb. Rationale: Every code change and every CloudPanel administrative action on these platforms is attributable only to "the shared account," not to a specific person. If credentials are shared among multiple staff (which a single account by definition requires, if more than one person needs access), there is no way to know who did what, which matters both for day-to-day accountability and for incident response if something goes wrong.

SEC-A-04: ippadmin has passwordless, unrestricted root sudo on 4 of 6 hosts, and is reachable by the same shared SSH keys used across the estate Evidence: /etc/sudoers.d/ippadmin contains ippadmin ALL=(ALL) NOPASSWD:ALL on Core, app2, app3, and (via a separate file) wphost02. The same itpp-infra and germaine@itppartner keys that unlock ippadmin's SSH session are shared across hosts (see SEC-A-02). Rationale: There is no additional authentication step between "I have this one SSH key" and "I am root on this server," across most of the estate. Combining a shared credential with unrestricted, no-prompt root escalation removes every layer of defense that would normally exist between initial access and full compromise.

SEC-A-05: app3's clpctlWrapper sudoers rule grants any account, including all ~28 per-client site accounts, a passwordless path to run a root-level control wrapper Evidence: /etc/sudoers.d/cloudpanel contains ALL ALL=(ALL) NOPASSWD: /usr/bin/clpctlWrapper. Rationale: This rule is written to apply to ALL users, not just clp. If any of the roughly 28 per-client accounts on app3 is ever compromised (for example through a vulnerable WordPress plugin on that client's site), the attacker inherits a passwordless path to a root-level tool. The actual risk depends on what commands clpctlWrapper exposes and whether it validates its inputs, which needs a Phase Two review; it was not something this read-only audit could safely test without executing the wrapper.

Medium

SEC-A-06: Secrets-sprawl git-grep hits (6,296 on Core, 5,504 on app1-bu, 1,275 on app2, 719 on app3, 27 on app1) are mostly false-positive-heavy vendored open-source code, but the organization's own infrastructure repo has not yet been manually triaged Evidence: Per-repo breakdown in Section 4.3 shows the largest contributors are vendored codebases (hermes-agent, twenty, ragflow, buzz) where matches are variable names or documentation text, not credential values. /root/projects/itpp-infrastructure itself shows 44 hits on Core and 2 on app1-bu. Rationale: Treating a raw grep-hit count as a secrets-leak severity score would both overstate the risk from vendor code and understate the one path that actually matters: the org's own documentation and infrastructure repo. This needs a targeted manual read of those 44 (Core) and 2 (app1-bu) hits in Phase Two, not a blanket "Critical, 13,000+ secrets found" framing.

SEC-A-07: Live .env credential files are retained unencrypted in daily backup snapshots on app3, and in stale pre-rotation copies on app1 Evidence: /home/clp/backups/2026-08-11_04-15-01/app/files/.env, 2026-08-12, and 2026-08-13 each hold a full plaintext copy of the same application .env. On app1, /root/docker/litellm/.env.pre-keyfix-20260714-130347 sits alongside the live /root/docker/litellm/.env, an old credential set from before a documented key-fix event on 2026-07-14 that was never removed. Rationale: Every backup cycle multiplies the number of at-rest plaintext copies of the same credentials without adding any access control beyond whatever protects the backup directory itself. Stale pre-rotation files are worse: if the "old" credentials in that file were ever rotated because they were compromised or suspected compromised, the old values are still sitting on disk in cleartext.

SEC-A-08: Almost all Docker containers across Core, app1, app2, and app3 run as root inside the container, despite several images on the same hosts demonstrating that non-root operation is supported Evidence: Section 6 lists dozens of containers with user=[root(default)] across all four hosts, alongside a smaller set of containers (browserless, grafana, prometheus, twenty-server/worker, kokoro-tts, unms-device-ws-*, hexclave-server, buzz-prod-relay-1) that correctly run as scoped, non-root users. Rationale: Running as root inside a container is not itself a host compromise, but it removes one layer of defense in depth: a container-escape vulnerability in a root-run container hands the attacker root, not a limited user, on the container runtime. Since the operators clearly know how to configure non-root users (as shown by the containers that already do this correctly), tightening the rest is a configuration change, not a re-architecture.

SEC-A-09: app2's unms service account is a member of the docker group Evidence: /etc/group on app2 shows docker:x:990:unms. Rationale: Docker group membership is equivalent to root access on the host. If unms was intended as a scoped, limited-privilege account for running the UNMS/UCRM stack, this membership defeats that intent and should be reviewed in Phase Two to confirm whether it is required for the stack's Compose-based startup or whether it can be removed in favor of running Compose as ippadmin/root directly.

Low

SEC-A-10: scanuser on Core has no identifiable purpose in the captured data and should be reviewed for removal or documentation Evidence: /etc/passwd shows scanuser:1001:1001:/home/scanuser:/bin/bash, shadow status is LOCKED_NO_PASSWORD, no matching SSH key, cron job, or docker container context was found tying it to an active use. Rationale: An account with an interactive shell but no traceable purpose is exactly the kind of thing that accumulates in long-running infrastructure and eventually becomes an orphaned foothold. Low severity because it is currently locked (no password) and no SSH key reaches it, but it should be either documented or removed rather than left unexplained.

SEC-A-11: Login-history visibility is inconsistent across the estate Evidence: Last-login data was populated for app1-bu and wphost02 but returned empty for Core, app1, app2, and app3 in the captured data. Rationale: Not being able to see recent login activity on 4 of 6 hosts is a monitoring/visibility gap that limits how confidently this audit (or Phase Two remediation planning) can assess whether the shared credentials discussed above are being actively used, by how many people, or from where. Flagged per the brief's rule 4 (flag insufficient visibility, do not guess) rather than assumed benign.


Appendix: Methodology and Limitations

  • All findings in this file are derived from local audit capture files (/root/audit_*.txt) collected in a prior read-only SSH/curl session against the 6 hosts. No new SSH connections, command executions against remote hosts, or credential-value reads were performed to produce this document.
  • Every .env, compose-file environment variable, and config-file secret value in the raw capture was already redacted to [REDACTED] at collection time; this file reports only presence, location, and staleness signals derived from filenames, timestamps, and structural markers (redaction-marker counts), never actual values.
  • App-level user accounts inside Wazuh, Hudu, UniFi, UNMS/UCRM, LiteLLM admin, Vaultwarden, Dawarich, and Traccar could not be enumerated from the local file data available to this session, since those are internal application database records, not OS-level accounts or static config file entries. This is a stated visibility gap per brief rule 4, not a finding of "no accounts" or "single account."
  • The per-file triage of the 44 (Core) and 2 (app1-bu) git-grep hits inside /root/projects/itpp-infrastructure itself was not completed in this session due to tool-budget constraints; this is explicitly flagged as outstanding work for Phase Two in Finding SEC-A-06, not silently omitted.
  • This file was written by a fresh session using data collected by a prior session that exhausted its tool-call budget before writing any findings file. All data cited here was independently re-extracted and cross-checked from the raw capture files as part of producing this document, not copied verbatim from the prior session's unverified summary.