# 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@<full-sha-hash>
   ```

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 (`services/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 `services/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/<filename>` 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@<full-40-char-sha>
```

### 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 (`services/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 <user>`. 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)
   - `services/telegram_bot/` (bot service, dispatch, runner)
   - `services/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
