# Security Audit Report: Data Broker Server **Date:** 2026-01-30 **Server:** `your-server` (YOUR_SERVER_IP), Debian 12 (bookworm), GCP e2-medium **Auditors:** Claude Opus 4.5 (primary) + Perplexity Sonar (validation) + OpenAI Codex (second opinion) **Scope:** Linux server security, user isolation, CI/CD pipeline, notification system, desktop app attack surface **Status:** Read-only audit -- no changes were made to the server --- ## Table of Contents - [Executive Summary](#executive-summary) - [Server Overview](#server-overview) - [Findings Summary](#findings-summary) - [Critical Findings](#critical-findings) - [High Findings](#high-findings) - [Medium Findings](#medium-findings) - [Low Findings](#low-findings) - [Additional Findings from Second Opinion](#additional-findings-from-second-opinion) - [Auditor Consensus Matrix](#auditor-consensus-matrix) - [Prioritized Remediation Plan](#prioritized-remediation-plan) - [Appendix: Methodology](#appendix-methodology) --- ## Executive Summary The server has a solid foundational architecture: SSH key-only authentication, POSIX ACLs for data isolation, per-user home directories with 750 permissions, setgid directories for shared data, and systemd service hardening. However, the audit identified **3 critical, 5 high, and 10 medium/low** findings that undermine these controls. The most severe issues are: 1. **All analysts can read private data** due to an ACL misconfiguration (C2) 2. **Any local user can send fake notifications** to any other user via a world-writable Unix socket (C3) 3. **CI/CD pipeline provides unrestricted root access** through sudoers wildcard rules (C1) These findings were independently validated by Perplexity (CVE references, Unix socket security research) and confirmed by OpenAI Codex second opinion review. --- ## Server Overview ### Infrastructure | Parameter | Value | |-----------|-------| | Hostname | your-server | | GCP Project | your-gcp-project | | Zone | europe-north1-a | | OS | Debian 12 (bookworm) | | External IP | YOUR_SERVER_IP | | Domain | your-instance.example.com | ### Users and Groups | Group | Members | Purpose | |-------|---------|---------| | `dataread` | admin1, admin2, admin3, john, analyst1, jane.smith, bob.jones, alice.wilson, mike.brown, tom.davis | Public data read access | | `data-private` | admin1, admin2, admin3 | Private/sensitive data access | | `data-ops` | deploy, admin1, admin2, admin3, www-data | Application deployment and operations | ### Services | Service | User | Port/Socket | Purpose | |---------|------|-------------|---------| | `webapp` | www-data | gunicorn -> nginx :443 | Self-service portal (Google SSO) | | `notify-bot` | deploy | `/run/notify-bot/bot.sock` | Telegram notification bot | | `ws-gateway` | deploy | 127.0.0.1:8765 + `/run/ws-gateway/ws.sock` | WebSocket gateway for desktop app | | nginx | www-data | :80, :443 | HTTPS reverse proxy | | Postfix | root | :25 | SMTP (outbound mail) | | sshd | root | :22 | SSH access | ### CI/CD Pipeline ``` GitHub repo (main branch) | v (push trigger) GitHub Actions (appleboy/ssh-action@v1.0.3) | v (SSH as deploy user) deploy.sh on server: 1. git fetch + git reset --hard origin/main 2. sudo cp server/bin/* -> /usr/local/bin/ 3. sudo cp server/sudoers-* -> /etc/sudoers.d/ 4. sudo cp *.service -> /etc/systemd/system/ 5. sudo systemctl restart webapp, notify-bot, ws-gateway ``` ### Notification Pipeline ``` User Python scripts (~/user/notifications/*.py) | v (crontab -> notify-runner) bot.sock (/run/notify-bot/bot.sock, mode 0666) | +-> Telegram Bot API (sendMessage, sendPhoto) | +-> ws.sock (/run/ws-gateway/ws.sock, mode 0770) | v (WebSocket over wss://) macOS Desktop App (DataAnalyst.app) ``` --- ## Findings Summary | Severity | Count | IDs | |----------|-------|-----| | **CRITICAL** | 3 | C1, C2, C3 | | **HIGH** | 5 | H1, H2, H3, H4, H5 | | **MEDIUM** | 8 | M1-M8 | | **LOW** | 4 | L1-L4 | --- ## Critical Findings ### C1: CI/CD Pipeline Provides Unrestricted Root Access via Sudoers Wildcards **Severity:** CRITICAL **CVSS estimate:** 9.1 (Network / High impact / No user interaction if GitHub compromised) **Validated by:** Perplexity (CVE-2026-22536 pattern), Codex (confirmed CRITICAL) #### Description The deploy user's sudoers configuration (`server/sudoers-deploy`) uses wildcard glob patterns that allow copying **any file** matching the source pattern into privileged system directories: ``` # /etc/sudoers.d/deploy (lines 8-9, 13) deploy ALL=(ALL) NOPASSWD: /usr/bin/cp /opt/data-analyst/repo/server/bin/* /usr/local/bin/* deploy ALL=(ALL) NOPASSWD: /usr/bin/cp /opt/data-analyst/repo/server/sudoers-* /etc/sudoers.d/* deploy ALL=(ALL) NOPASSWD: /usr/bin/chmod 755 /usr/local/bin/* deploy ALL=(ALL) NOPASSWD: /usr/bin/chmod 440 /etc/sudoers.d/* ``` The deploy user has write access to the source directory (`/opt/data-analyst/repo/`) because `deploy.sh` runs `git reset --hard origin/main`, pulling whatever code exists on the main branch. #### Attack Chain 1. Attacker gains write access to the GitHub repository (compromised personal access token, stolen SSH deploy key, social engineering on a maintainer, or missing branch protection rules) 2. Attacker creates `server/sudoers-backdoor` with content: `ALL ALL=(ALL) NOPASSWD: ALL` 3. Attacker creates `server/bin/backdoor` with a reverse shell or privilege escalation payload 4. Push to `main` triggers GitHub Actions deploy workflow 5. `deploy.sh` executes `git reset --hard origin/main` -- attacker's code is now on disk 6. Script runs `sudo /usr/bin/cp /opt/data-analyst/repo/server/sudoers-backdoor /etc/sudoers.d/backdoor` (matches wildcard) 7. Script runs `sudo /usr/bin/chmod 440 /etc/sudoers.d/backdoor` (valid sudoers file) 8. **Full root access achieved** -- any user on the system can now run any command as root Note: `deploy.sh` does validate sudoers files with `visudo -cf` before copying (line 64), which is a good control. However, the attacker controls the file content, so they can craft a syntactically valid but malicious sudoers file. #### Perplexity Validation Perplexity confirmed this as a known vulnerability pattern, referencing CVE-2026-22536: > _"A common exploitation involves sudoers entries granting a user permission to execute /usr/bin/cp with wildcards, where the attacker controls the source directory. The wildcard expands to copy the malicious file as root."_ Source: SentinelOne vulnerability database, Compass Security research on dangerous sudoers entries. #### Codex Second Opinion > _"git reset --hard origin/main does NOT mitigate a compromised origin. The attacker updates origin/main and you faithfully reset to it and then copy sudoers into /etc/sudoers.d. Critical -- full root compromise via CI/CD."_ #### Current Mitigating Controls - GitHub deploy key is read-only (cannot push, but attacker could use a maintainer's token) - `visudo -cf` validates syntax before copy (prevents broken sudoers, not malicious ones) - Deploy user is not in `sudo` group (can only run explicitly allowed commands) #### Recommendations 1. **Replace wildcards with explicit file paths** in `sudoers-deploy`: ``` # Instead of: deploy ALL=(ALL) NOPASSWD: /usr/bin/cp /opt/data-analyst/repo/server/sudoers-* /etc/sudoers.d/* # Use: deploy ALL=(ALL) NOPASSWD: /usr/bin/cp /opt/data-analyst/repo/server/sudoers-deploy /etc/sudoers.d/deploy deploy ALL=(ALL) NOPASSWD: /usr/bin/cp /opt/data-analyst/repo/server/sudoers-webapp /etc/sudoers.d/webapp ``` Same for `server/bin/*` -- list each script explicitly. 2. **GitHub branch protection** on `main`: require pull request reviews, no force push, require signed commits, restrict who can push. 3. **Pin GitHub Actions** to SHA instead of tag: ```yaml # Instead of: uses: appleboy/ssh-action@v1.0.3 # Use: uses: appleboy/ssh-action@ ``` 4. **Consider**: manage sudoers files outside the git repo (e.g., Ansible vault, manual root-only deployment). --- ### C2: Private Data ACL Grants Read Access to All Analysts **Severity:** CRITICAL **CVSS estimate:** 7.5 (Local / High confidentiality impact) **Validated by:** Perplexity (POSIX ACL mechanism confirmed), live server test (exit code 0) #### Description The directory `/data/src_data/parquet/private/` is intended to be accessible only to members of the `data-private` group (3 privileged users: admin1, admin2, admin3). However, its POSIX ACL also grants access to the `dataread` group, which contains **all 10 analysts**: ``` # getfacl /data/src_data/parquet/private/ user::rwx group::rwx group:dataread:r-x # <-- ALL analysts have read+execute group:data-private:r-x # <-- intended: only 3 privileged users mask::rwx # <-- mask does NOT restrict (rwx & r-x = r-x) other::--- # Default ACL (inherited by new files): default:group:dataread:r-x # <-- new files also readable by all default:group:data-private:r-x default:mask::rwx ``` The POSIX ACL mask (`mask::rwx`) does not restrict the `dataread` entry because `rwx AND r-x = r-x` (mask only limits, it doesn't deny). This means every member of `dataread` has effective `r-x` on the private directory and all its contents. #### Proof of Exploitation Tested directly on server with user `analyst1` (standard analyst, member of `dataread` only, NOT `data-private`): ```bash $ sudo -u analyst1 ls -la /data/src_data/parquet/private/ total 16 drwxrws---+ 2 admin1 data-ops 4096 Jan 21 14:29 . drwxrws---+ 7 admin1 data-ops 4096 Jan 23 18:29 .. # Exit code: 0 (access granted) ``` The directory is currently empty, but the ACL permits access. When private data files are added, all analysts will be able to read them. #### Documentation Contradiction `server.md` (line 347) claims: ``` $ ls ~/server/parquet/private/ ls: cannot open directory 'private/': Permission denied ``` This is incorrect given the current ACL configuration. #### Perplexity Validation Perplexity confirmed the POSIX ACL mechanism: > _"Effective permissions for any user matching a named group ACL are the logical AND of that entry's permissions and the mask. If mask is rwx, a group:dataread:r-x entry yields effective r-x."_ #### Recommendations ```bash # Remove dataread from private/ and its default ACL sudo setfacl -R -x g:dataread /data/src_data/parquet/private/ sudo setfacl -R -d -m g:dataread:--- /data/src_data/parquet/private/ # Or remove and re-set defaults without dataread: sudo setfacl -R -b /data/src_data/parquet/private/ sudo setfacl -R -m u::rwx,g::rwx,g:data-private:r-x,g:data-ops:rwx,o::--- /data/src_data/parquet/private/ sudo setfacl -R -d -m u::rwx,g::rwx,g:data-private:r-x,g:data-ops:rwx,o::--- /data/src_data/parquet/private/ # Verify: sudo -u analyst1 ls /data/src_data/parquet/private/ # Expected: "ls: cannot open directory 'private/': Permission denied" ``` Also fix the `add-analyst` script or whatever sets up ACLs to not include `dataread` on `private/`. --- ### C3: World-Writable Notification Socket Enables Cross-User Spoofing **Severity:** CRITICAL (upgraded from HIGH based on combined impact with H1) **CVSS estimate:** 7.8 (Local / High integrity impact / social engineering amplifier) **Validated by:** Perplexity (dirty_sock CVE-2019-7304 pattern), Codex (HIGH, CRITICAL with H1) #### Description The Telegram bot's Unix socket at `/run/notify-bot/bot.sock` has permissions `0666` (world-readable, world-writable): ``` srw-rw-rw- 1 deploy data-ops 0 Jan 30 19:20 /run/notify-bot/bot.sock ``` The socket accepts HTTP requests without any caller authentication. The `POST /send` endpoint takes a JSON body with: - `user`: target username (any analyst) - `text`: message content (arbitrary Markdown) - `parse_mode`: formatting mode Any local user on the server can send a fake Telegram notification to **any other user** by specifying their username. #### Proof of Concept Any analyst can execute: ```bash curl --unix-socket /run/notify-bot/bot.sock \ -X POST http://localhost/send \ -H 'Content-Type: application/json' \ -d '{ "user": "ceo", "text": "*URGENT: Security incident detected*\nYour account may be compromised.\nReset credentials immediately: https://attacker-controlled.example.com/reset", "parse_mode": "Markdown" }' ``` This sends a convincing-looking urgent security alert to the CEO's Telegram, appearing to come from the official notification bot. #### Combined Impact with H1 (Notification Content Injection) The same mechanism works for the WebSocket dispatch socket (`/run/ws-gateway/ws.sock`), which is correctly restricted to `0770`. However, bot.sock notifications are also dispatched to the WebSocket gateway by `notify-runner` (line 261 of `server/bin/notify-runner`), so **direct bot.sock access bypasses the ws.sock restriction** for Telegram delivery. Attack scenarios: - **Phishing**: fake urgent alerts with malicious URLs - **Social engineering**: impersonate system alerts to manipulate user behavior - **Prompt injection**: if user copies notification text into Claude Code, attacker can inject LLM instructions - **Spam/DoS**: flood another user's Telegram with notifications #### Comparison with ws.sock The WebSocket gateway socket is correctly configured: ``` srwxrwx--- 1 deploy data-ops 0 Jan 30 19:20 /run/ws-gateway/ws.sock ``` Only `deploy` and `data-ops` group members can access it. The bot socket should follow the same pattern. #### Perplexity Validation Perplexity referenced the snapd `dirty_sock` vulnerability (CVE-2019-7304) as a precedent: > _"SocketMode=0666 in systemd units enabled connections from unprivileged processes. Best practice: Use 0600 (owner-only) or 0660 (owner/group) combined with proper ownership. Always verify peer credentials with SO_PEERCRED after connection."_ #### Codex Second Opinion > _"I'd rate High, not Critical, unless Telegram/macOS notifications are used for security-sensitive actions. With H1 (no sanitization) it becomes Critical in practice."_ Given that notifications go to both Telegram and the desktop app, and there is no sender verification, we rate this CRITICAL. #### Recommendations 1. **Immediate**: Change socket permissions to `0660` or `0770` in the bot code (`server/telegram_bot/bot.py`) or systemd service file. The socket is currently set to `0666` by an `ExecStartPost` or in code -- update to restrict to `data-ops` group. 2. **Better**: Add `SO_PEERCRED` validation in the bot's HTTP handler to verify the caller's UID and ensure they can only send notifications for their own username. 3. **Best**: Implement a sender authentication mechanism where the `/send` endpoint validates that the `user` field matches the calling process's system username (obtained via `SO_PEERCRED` or `getpeereid()`). --- ## High Findings ### H1: No Content Sanitization in Notification Pipeline **Severity:** HIGH (CRITICAL when combined with C3) **Validated by:** Codex confirmed #### Description User notification scripts output JSON with `title` and `message` fields containing arbitrary text. This content flows through the entire notification pipeline without any sanitization: 1. **User script** (`~/user/notifications/*.py`) outputs JSON to stdout 2. **notify-runner** (`/usr/local/bin/notify-runner`) parses JSON, sends to bot.sock 3. **Telegram bot** forwards to Telegram API with Markdown formatting 4. **notify-runner** also dispatches to ws.sock for desktop app delivery 5. **WebSocket gateway** forwards to connected macOS app clients 6. **macOS app** renders in SwiftUI views and macOS notification center No component in this chain validates, sanitizes, or escapes the content. Combined with C3 (world-writable socket), any local user can inject arbitrary content into another user's notifications. #### Attack Vectors - **Phishing via Telegram**: Markdown links `[Click here](https://evil.com)` render as clickable - **macOS notification spoofing**: banner notifications show title/message as-is - **Prompt injection**: if user pastes notification content into Claude Code or another LLM tool, the attacker can embed hidden instructions - **UI confusion**: SwiftUI Text views may render certain characters in unexpected ways #### Recommendations 1. Strip or escape URLs in notification content at the bot.sock handler level 2. Limit message length (e.g., 500 characters for title, 2000 for message) 3. Desktop app: escape special characters, render as plain text by default 4. Consider: content-type field (text/plain vs text/markdown) with appropriate rendering --- ### H2: notify-scripts Sudo Rule Allows Execution as Any User Including Root **Severity:** HIGH **Validated by:** Codex (HIGH, potentially CRITICAL) #### Description The sudoers rules for `www-data` and `deploy` allow running `notify-scripts` as **any user** on the system: ``` # /etc/sudoers.d/webapp www-data ALL=(ALL) NOPASSWD: /usr/local/bin/notify-scripts # /etc/sudoers.d/deploy (line 69) deploy ALL=(ALL) NOPASSWD: /usr/local/bin/notify-scripts ``` The `(ALL)` target means these service users can execute: ```bash sudo -u root /usr/local/bin/notify-scripts run some_script.py sudo -u deploy /usr/local/bin/notify-scripts list ``` The `username` parameter comes from webapp request data or Telegram bot callback data without validation against a list of valid analyst usernames. #### Code Analysis In `server/telegram_bot/runner.py` (line 30): ```python result = subprocess.run( ["/usr/bin/sudo", "-u", username, NOTIFY_SCRIPTS_BIN, "run", script_name], ... ) ``` The `username` is passed directly from the Telegram callback data or webapp API request. While `notify-scripts` itself validates that the script file exists in `~/user/notifications/`, the `sudo -u` target is not validated. #### Mitigating Controls - `notify-scripts` validates script path exists in `~/user/notifications/` (non-existent for root) - Scripts must end in `.py` - 60-second timeout enforced #### Recommendations 1. Restrict sudoers to analyst group only: ``` www-data ALL=(dataread) NOPASSWD: /usr/local/bin/notify-scripts deploy ALL=(dataread) NOPASSWD: /usr/local/bin/notify-scripts ``` 2. Add username validation in `runner.py` and webapp API: check that username exists in the `dataread` group before executing. --- ### H3: SMTP Port 25 Open on All Interfaces **Severity:** HIGH **Validated by:** Codex (MEDIUM-HIGH) #### Description Postfix is configured with `inet_interfaces = all`, listening on port 25 on all network interfaces. Combined with: - No iptables rules (empty chains, ACCEPT policy) - No GCP firewall rule explicitly blocking port 25 The server may be reachable on port 25 from the internet, potentially allowing: - Open relay abuse (if Postfix relay restrictions are misconfigured) - Spam origination - Information disclosure via SMTP banner #### Current State ``` # /etc/postfix/main.cf myhostname = your-server.c.your-gcp-project.internal mydestination = $myhostname, localhost inet_interfaces = all # ss -tlnp (port 25 listening on 0.0.0.0) LISTEN 0 100 0.0.0.0:25 ``` #### Recommendations 1. Change to `inet_interfaces = loopback-only` if external mail is not needed 2. Or add iptables rule: `iptables -A INPUT -p tcp --dport 25 -j DROP` 3. Verify GCP firewall does not allow port 25 inbound --- ### H4: No Rate Limiting on API Endpoints (Verification Code Brute-Force) **Severity:** HIGH (upgraded from MEDIUM based on Codex feedback) **Validated by:** Codex (HIGH if auth factor and online) #### Description The Telegram verification code is a 6-digit number (1,000,000 possible combinations) with a 10-minute expiry. The endpoint `POST /api/telegram/verify` has no rate limiting. An attacker could brute-force the code at approximately 1,700 requests/second to exhaust all combinations within the 10-minute window. A successful brute-force would allow the attacker to link their own Telegram account to another user's analyst account, receiving all that user's notifications. #### Recommendations 1. Add rate limiting with `flask-limiter` (e.g., 5 attempts per minute per session) 2. Increase code length to 8 digits 3. Lock account after N failed attempts 4. Add CAPTCHA after 3 failed attempts --- ### H5: Empty iptables + fail2ban Inactive **Severity:** HIGH (combined), individually MEDIUM **Validated by:** Codex (MEDIUM each, HIGH combined) #### Description **iptables:** ``` Chain INPUT (policy ACCEPT) Chain FORWARD (policy ACCEPT) Chain OUTPUT (policy ACCEPT) # No rules in any chain ``` **fail2ban:** inactive/not installed (`systemctl is-active fail2ban` returns inactive). The server relies exclusively on GCP firewall rules for network protection. If a GCP firewall rule is accidentally broadened, all server ports become exposed with no host-level defense. SSH is key-only (mitigates password brute-force), but without fail2ban there is no rate limiting on failed key auth attempts, which could be used for user enumeration or resource exhaustion. #### Recommendations 1. Install and activate fail2ban with sshd and nginx jails 2. Configure iptables/nftables as defense-in-depth: ```bash # Allow established, SSH, HTTP, HTTPS only iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -p tcp --dport 22 -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT iptables -A INPUT -i lo -j ACCEPT iptables -P INPUT DROP iptables -P FORWARD DROP ``` --- ## Medium Findings ### M1: CI/CD Trust Boundary -- Push to Main Equals Full Server Control **Severity:** MEDIUM (de facto part of C1, treated separately for organizational clarity) **Codex rating:** CRITICAL (if branch protections absent) #### Description The GitHub Actions workflow (`.github/workflows/deploy.yml`) triggers on every push to `main` branch. It connects via SSH as the `deploy` user and runs `deploy.sh`, which has root-level effects through sudoers rules. Anyone with push access to the `main` branch controls: - All scripts in `/usr/local/bin/` - All sudoers files in `/etc/sudoers.d/` - All systemd service files - The `.env` file containing secrets (WEBAPP_SECRET_KEY, GOOGLE_CLIENT_SECRET, JWT secret, Telegram bot token) - Python code running as www-data (webapp) and deploy (bot, gateway) #### Recommendations See C1 recommendations. Additionally: - Audit GitHub collaborator list and permissions - Enable GitHub audit log monitoring - Consider separate deployment approval step (manual gate in Actions) ### M2: JWT Secret Without Rotation **Severity:** MEDIUM Desktop app JWT tokens have 30-day expiry with a 7-day grace period for refresh (`DESKTOP_JWT_REFRESH_GRACE_DAYS`). The `DESKTOP_JWT_SECRET` is set once via GitHub Actions and never rotated. A compromised token provides 37 days of access. **Recommendation:** Implement secret rotation mechanism. Consider shorter token expiry (7 days) with automatic refresh. ### M3: Notification Image Endpoint May Allow Path Traversal **Severity:** MEDIUM-HIGH (needs code review of `webapp/notification_images.py`) The endpoint `GET /api/notifications/images/` serves files from `/tmp/`. If the `filename` parameter is not properly sanitized, a request like `GET /api/notifications/images/../../etc/passwd` could read arbitrary files. **Recommendation:** Review `notification_images.py` implementation. Ensure `os.path.basename()` is applied to filename and result is validated to be within `/tmp/`. ### M4: GitHub Actions Uses Tag-Pinned Action (Not SHA) **Severity:** MEDIUM ```yaml uses: appleboy/ssh-action@v1.0.3 # tag, not SHA ``` If the `appleboy/ssh-action` repository is compromised, the tag could be moved to point to malicious code (supply chain attack). **Recommendation:** Pin to full commit SHA: ```yaml uses: appleboy/ssh-action@ ``` ### M5: Telegram Bot Token on Disk **Severity:** MEDIUM The Telegram bot token is stored in two files: - `/opt/data-analyst/.env` (mode 640, root:data-ops) -- readable by data-ops group - `/opt/data-analyst/repo/.env` (mode 640, root:data-ops) -- same If compromised, the token allows sending arbitrary Telegram messages to any linked user via the Telegram Bot API directly (bypassing the bot socket entirely). **Recommendation:** Ensure `.env` files are not readable by analysts (currently correct: 640 root:data-ops). Monitor for unauthorized access. ### M6: Systemd Service Hardening Gaps **Severity:** MEDIUM **Source:** Codex second opinion The `notify-bot.service` has: ```ini NoNewPrivileges=false # required for sudo -u PrivateTmp=false # required to read user image files from /tmp ``` Missing hardening directives: - `CapabilityBoundingSet=` (drop unnecessary capabilities) - `RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6` - `SystemCallFilter=@system-service` **Recommendation:** Add hardening directives where possible. The `NoNewPrivileges=false` is necessary for the current architecture but should be addressed by moving to a queue-based approach (see GitHub issue #51). ### M7: Deploy Home Directory is 755 **Severity:** MEDIUM ``` drwxr-xr-x 4 deploy deploy 4096 Jan 30 19:20 /home/deploy/ ``` All analyst home directories are `750`, but deploy's home is `755` (world-readable). While deploy's home does not contain sensitive data in obvious locations, `.ssh/` is `700` (correct). **Recommendation:** `chmod 750 /home/deploy` ### M8: WebSocket Gateway Lacks Origin Check and Replay Protection **Severity:** MEDIUM **Source:** Codex second opinion The WebSocket gateway (`server/ws_gateway/gateway.py`) validates JWT tokens but does not: - Check the `Origin` header of WebSocket connections - Implement replay protection (a captured auth message could be replayed within token validity) - Bind tokens to specific connections or IP addresses **Recommendation:** Add Origin header validation. Consider adding a nonce to the auth flow. --- ## Low Findings ### L1: /data/downloads World-Readable ``` drwxr-xr-x 2 root root 4096 Jan 30 13:39 /data/downloads/ -rw-r--r-- 1 root data-ops 131219 Jan 30 13:39 DataAnalyst.zip ``` The desktop app download is accessible to all users. Low risk as this is a distributable application. ### L2: Missing auditd No audit daemon configured. Sudo operations are logged in `auth.log` but detailed file access auditing is absent. **Recommendation:** Install and configure `auditd` for monitoring privileged operations and sensitive file access. ### L3: MaxSessions 20 SSH allows 20 concurrent sessions per user, which is higher than typical (default 10). Increases session hijacking surface. ### L4: Cron Error Reporting via SMTP Cron jobs use `MAILTO=admin@your-domain.com`, which depends on the Postfix configuration (see H3). If SMTP is misconfigured, cron errors may be silently lost. --- ## Additional Findings from Second Opinion The following findings were identified by the OpenAI Codex second opinion review and were not in the original audit: | ID | Finding | Severity | Description | |----|---------|----------|-------------| | NEW-1 | Symlink/path-traversal in sudo-executed scripts | MEDIUM | `notify-scripts` and `deploy.sh` may follow symlinks when operating on user directories. If an analyst creates a symlink in `~/user/notifications/` pointing to a sensitive file, execution via `sudo -u` could have unintended effects. | | NEW-2 | Lateral movement via shared directory enumeration | LOW | Analysts can enumerate shared configs and other users via symlinks in `~/server/`. While data is read-only, directory listings may reveal information about other users or system configuration. | | NEW-3 | Environment variable leakage in sudo | MEDIUM | `notify-scripts` runs via `sudo -u `. Depending on sudoers `env_reset` configuration, environment variables from the calling process (www-data, deploy) may leak to the user process. | --- ## Auditor Consensus Matrix | Finding | Claude (primary) | Perplexity | Codex | Consensus | |---------|-----------------|------------|-------|-----------| | C1 (sudoers wildcards) | CRITICAL | CRITICAL (CVE ref) | CRITICAL | **CRITICAL** | | C2 (private data ACL) | CRITICAL | Confirmed mechanism | HIGH-CRITICAL | **CRITICAL** | | C3 (bot.sock 0666) | CRITICAL | CRITICAL (dirty_sock ref) | HIGH, CRITICAL w/ H1 | **CRITICAL** | | H1 (no sanitization) | HIGH | -- | HIGH (CRITICAL w/ C3) | **HIGH** | | H2 (sudo ALL target) | HIGH | -- | HIGH-CRITICAL | **HIGH** | | H3 (SMTP open) | HIGH | -- | MEDIUM-HIGH | **HIGH** | | H4 (rate limiting) | MEDIUM | -- | HIGH | **HIGH** | | H5 (iptables+fail2ban) | HIGH | -- | MEDIUM combined | **HIGH** (combined) | | M1 (CI/CD trust) | MEDIUM | -- | CRITICAL | **HIGH** (merged w/ C1) | **Key disagreement:** Codex rated network hardening findings (H3, H5) lower than Claude, arguing SSH key-only auth and GCP firewall provide adequate baseline. Claude maintains HIGH rating based on defense-in-depth principle. Codex elevated M4 (rate limiting) and M1 (CI/CD trust) higher than Claude's original rating. --- ## Prioritized Remediation Plan ### Immediate (same day) | # | Action | Finding | Risk | Effort | |---|--------|---------|------|--------| | 1 | Fix private/ ACL: remove `g:dataread` | C2 | Data exposure | 5 min | | 2 | Change bot.sock to 0660 | C3 | Spoofing | 5 min | | 3 | Restrict notify-scripts sudo to `(dataread)` | H2 | Privilege escalation | 5 min | ### Short-term (this week) | # | Action | Finding | Risk | Effort | |---|--------|---------|------|--------| | 4 | Replace sudoers wildcards with explicit paths | C1 | Root compromise | 1 hour | | 5 | Add rate limiting to API endpoints | H4 | Account takeover | 2 hours | | 6 | Change Postfix to loopback-only | H3 | Open relay | 5 min | | 7 | Install and configure fail2ban | H5 | Brute force | 30 min | | 8 | Configure basic iptables rules | H5 | Network exposure | 30 min | ### Medium-term (this month) | # | Action | Finding | Risk | Effort | |---|--------|---------|------|--------| | 9 | GitHub branch protection on main | C1/M1 | Supply chain | 30 min | | 10 | Pin GitHub Actions to SHA | M4 | Supply chain | 15 min | | 11 | Add content sanitization to notification pipeline | H1 | Phishing/injection | 4 hours | | 12 | Review notification_images.py for path traversal | M3 | File disclosure | 1 hour | | 13 | Add systemd hardening directives | M6 | Lateral movement | 2 hours | | 14 | Deploy home dir to 750 | M7 | Info disclosure | 1 min | | 15 | Install auditd | L2 | Forensics | 1 hour | --- ## Appendix: Methodology ### Tools and Approach 1. **Repository analysis**: Full source code review of all 124 files in the repository, focusing on: - `server/sudoers-deploy`, `server/sudoers-webapp` (privilege escalation) - `server/deploy.sh` (CI/CD pipeline) - `server/bin/notify-runner`, `server/bin/notify-scripts` (notification pipeline) - `server/telegram_bot/` (bot service, dispatch, runner) - `server/ws_gateway/` (WebSocket gateway, JWT auth) - `webapp/desktop_auth.py`, `webapp/user_service.py` (auth flows) - `.github/workflows/deploy.yml` (CI/CD configuration) 2. **Live server inspection** (read-only, via SSH as admin1): - File permissions: `ls -la`, `stat`, `getfacl` on all critical paths - Socket permissions: `/run/notify-bot/`, `/run/ws-gateway/` - Group memberships: `getent group` for dataread, data-private, data-ops - Service status: `systemctl list-units` - Network: `ss -tlnp`, iptables, SSH config, nginx config - Crontabs: all users checked - Access control test: `sudo -u analyst1 ls` on private directory 3. **Validation**: Perplexity Sonar search for CVE references and best practices on: - Unix socket 0666 security (dirty_sock CVE-2019-7304) - POSIX ACL mask interaction - Sudoers wildcard exploitation (CVE-2026-22536 pattern) 4. **Second opinion**: OpenAI Codex CLI review of all findings with severity validation and identification of additional attack vectors. ### Out of Scope - Penetration testing (no active exploitation attempted) - macOS app binary analysis (source code review only) - Keboola API security - Google OAuth implementation details - GCP project-level IAM configuration - Network-level traffic analysis