All references to server/telegram_bot/, server/ws_gateway/, server/corporate_memory/, server/session_collector* updated to their new locations under services/.
32 KiB
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
- Server Overview
- Findings Summary
- Critical Findings
- High Findings
- Medium Findings
- Low Findings
- Additional Findings from Second Opinion
- Auditor Consensus Matrix
- Prioritized Remediation Plan
- 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:
- All analysts can read private data due to an ACL misconfiguration (C2)
- Any local user can send fake notifications to any other user via a world-writable Unix socket (C3)
- 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
- 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)
- Attacker creates
server/sudoers-backdoorwith content:ALL ALL=(ALL) NOPASSWD: ALL - Attacker creates
server/bin/backdoorwith a reverse shell or privilege escalation payload - Push to
maintriggers GitHub Actions deploy workflow deploy.shexecutesgit reset --hard origin/main-- attacker's code is now on disk- Script runs
sudo /usr/bin/cp /opt/data-analyst/repo/server/sudoers-backdoor /etc/sudoers.d/backdoor(matches wildcard) - Script runs
sudo /usr/bin/chmod 440 /etc/sudoers.d/backdoor(valid sudoers file) - 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 -cfvalidates syntax before copy (prevents broken sudoers, not malicious ones)- Deploy user is not in
sudogroup (can only run explicitly allowed commands)
Recommendations
-
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/webappSame for
server/bin/*-- list each script explicitly. -
GitHub branch protection on
main: require pull request reviews, no force push, require signed commits, restrict who can push. -
Pin GitHub Actions to SHA instead of tag:
# Instead of: uses: appleboy/ssh-action@v1.0.3 # Use: uses: appleboy/ssh-action@<full-sha-hash> -
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):
$ 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
# 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:
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
-
Immediate: Change socket permissions to
0660or0770in the bot code (services/telegram_bot/bot.py) or systemd service file. The socket is currently set to0666by anExecStartPostor in code -- update to restrict todata-opsgroup. -
Better: Add
SO_PEERCREDvalidation in the bot's HTTP handler to verify the caller's UID and ensure they can only send notifications for their own username. -
Best: Implement a sender authentication mechanism where the
/sendendpoint validates that theuserfield matches the calling process's system username (obtained viaSO_PEERCREDorgetpeereid()).
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:
- User script (
~/user/notifications/*.py) outputs JSON to stdout - notify-runner (
/usr/local/bin/notify-runner) parses JSON, sends to bot.sock - Telegram bot forwards to Telegram API with Markdown formatting
- notify-runner also dispatches to ws.sock for desktop app delivery
- WebSocket gateway forwards to connected macOS app clients
- 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
- Strip or escape URLs in notification content at the bot.sock handler level
- Limit message length (e.g., 500 characters for title, 2000 for message)
- Desktop app: escape special characters, render as plain text by default
- 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:
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):
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-scriptsvalidates script path exists in~/user/notifications/(non-existent for root)- Scripts must end in
.py - 60-second timeout enforced
Recommendations
-
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 -
Add username validation in
runner.pyand webapp API: check that username exists in thedatareadgroup 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
- Change to
inet_interfaces = loopback-onlyif external mail is not needed - Or add iptables rule:
iptables -A INPUT -p tcp --dport 25 -j DROP - 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
- Add rate limiting with
flask-limiter(e.g., 5 attempts per minute per session) - Increase code length to 8 digits
- Lock account after N failed attempts
- 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
- Install and activate fail2ban with sshd and nginx jails
- Configure iptables/nftables as defense-in-depth:
# 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
.envfile 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
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:
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:
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_INET6SystemCallFilter=@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
Originheader 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
-
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)
-
Live server inspection (read-only, via SSH as admin1):
- File permissions:
ls -la,stat,getfaclon all critical paths - Socket permissions:
/run/notify-bot/,/run/ws-gateway/ - Group memberships:
getent groupfor 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 lson private directory
- File permissions:
-
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)
-
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