mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
docs(plans): cloud deployment plan (family testing phase) — draft
Plan-stage document for Oracle Cloud VM deployment with family-scale hardening. Covers architecture overview, TLS termination, iptables / OCI Security List policy, OAuth credential transfer, auth model (allow_anonymous off, per-key tier mapping), audit visibility, and the Phase 7 sandbox-runtime prerequisite. Status: Draft, pending Phase 7 (sandbox-runtime integration) completion. This commit makes the plan referenceable from ADR 0009 Amendment 1 § Caveats #3, the 2026-05-27 incident memory (cc-rules), and the forthcoming Phase 7 charter. NOT a code change — no ALIGNMENT.md authority citation required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
# OLP Cloud Deployment Plan — Family Testing Phase
|
||||
|
||||
**Status:** Draft — pending current Phase 6 completion
|
||||
**Target:** Oracle Cloud VM (existing infrastructure)
|
||||
**Audience:** Project maintainer deployment reference
|
||||
**Scope:** Single-VM deployment for family (3–5 users), spawn-binary architecture, public internet exposure with hardened auth
|
||||
|
||||
---
|
||||
|
||||
## 0. Prerequisites
|
||||
|
||||
- OLP current phase (Phase 6) is closed and tagged
|
||||
- Oracle Cloud VM accessible via SSH (existing `opc` user)
|
||||
- Domain name (optional but strongly recommended for TLS)
|
||||
- Provider CLI OAuth completed on at least one machine (credentials transferable)
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ Family Devices (anywhere on internet) │
|
||||
│ │
|
||||
│ Wife iPad / Kid Laptop / Maintainer MacBook / ... │
|
||||
│ IDE: Cline / Continue.dev / Cursor / Aider / OpenClaw │
|
||||
│ Config: OPENAI_BASE_URL=https://olp.example.com/v1 │
|
||||
│ OPENAI_API_KEY=olp_<personal-key> │
|
||||
└──────────────────────────┬─────────────────────────────────────────┘
|
||||
│ HTTPS (TLS 1.3)
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ Oracle Cloud VM │
|
||||
│ │
|
||||
│ ┌─ iptables / OCI Security List ──────────────────────────────┐ │
|
||||
│ │ ALLOW: TCP 443 (HTTPS) from 0.0.0.0/0 │ │
|
||||
│ │ ALLOW: TCP 22 (SSH) from maintainer IP only │ │
|
||||
│ │ DENY: everything else │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Nginx (reverse proxy + TLS termination) ───────────────────┐ │
|
||||
│ │ :443 → TLS (Let's Encrypt auto-renew via certbot) │ │
|
||||
│ │ proxy_pass → http://127.0.0.1:4567 │ │
|
||||
│ │ Rate limit: 30 req/min per IP (burst 10) │ │
|
||||
│ │ Request body limit: 1MB │ │
|
||||
│ │ Connection timeout: 300s (streaming needs long timeout) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ OLP server.mjs ───────────────────────────────────────────┐ │
|
||||
│ │ OLP_BIND=127.0.0.1 (loopback only — Nginx fronts it) │ │
|
||||
│ │ OLP_PORT=4567 │ │
|
||||
│ │ auth.allow_anonymous: false │ │
|
||||
│ │ auth.advertise_anonymous_key: false │ │
|
||||
│ │ Per-key audit logging to ~/.olp/logs/audit.ndjson │ │
|
||||
│ │ Owner key: maintainer only │ │
|
||||
│ │ Guest keys: one per family member │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Provider CLIs (installed on this VM) ──────────────────────┐ │
|
||||
│ │ claude → ~/.claude/.credentials.json (OAuth) │ │
|
||||
│ │ codex → ~/.codex/auth.json (OAuth) │ │
|
||||
│ │ vibe → ~/.vibe/.env (API key) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ systemd service ──────────────────────────────────────────┐ │
|
||||
│ │ olp.service: auto-start, auto-restart on crash │ │
|
||||
│ │ Runs as dedicated `olp` user (not root, not opc) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Provider CLIs spawn outbound HTTPS calls
|
||||
▼
|
||||
Anthropic API / OpenAI API / Mistral API
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Security Design (7 Layers)
|
||||
|
||||
### Layer 1 — Network Perimeter (OCI Security List + iptables)
|
||||
|
||||
**Principle:** Minimum attack surface. Only two ports reachable from the internet.
|
||||
|
||||
```
|
||||
OCI Security List (stateful ingress rules):
|
||||
┌──────────┬────────────┬───────────────────────────────┐
|
||||
│ Port │ Protocol │ Source │
|
||||
├──────────┼────────────┼───────────────────────────────┤
|
||||
│ 443 │ TCP │ 0.0.0.0/0 (public HTTPS) │
|
||||
│ 22 │ TCP │ <maintainer-IP>/32 only │
|
||||
└──────────┴────────────┴───────────────────────────────┘
|
||||
|
||||
NOT exposed:
|
||||
- Port 4567 (OLP direct) — Nginx fronts it
|
||||
- Port 80 (HTTP) — only for certbot ACME challenge, redirect to 443
|
||||
```
|
||||
|
||||
**iptables backup** (defense in depth — OCI Security List is primary, iptables is secondary):
|
||||
|
||||
```bash
|
||||
# Drop everything by default
|
||||
sudo iptables -P INPUT DROP
|
||||
sudo iptables -P FORWARD DROP
|
||||
|
||||
# Allow established connections
|
||||
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# Allow loopback
|
||||
sudo iptables -A INPUT -i lo -j ACCEPT
|
||||
|
||||
# Allow SSH from maintainer IP only
|
||||
sudo iptables -A INPUT -p tcp --dport 22 -s <MAINTAINER_IP> -j ACCEPT
|
||||
|
||||
# Allow HTTPS from anywhere
|
||||
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
|
||||
|
||||
# Allow HTTP (certbot ACME only — Nginx redirects everything else)
|
||||
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
|
||||
|
||||
# Persist
|
||||
sudo iptables-save | sudo tee /etc/iptables/rules.v4
|
||||
```
|
||||
|
||||
### Layer 2 — TLS Termination (Nginx + Let's Encrypt)
|
||||
|
||||
**Principle:** All client traffic encrypted. OLP itself runs plain HTTP on loopback — simpler, no cert management in Node.
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/olp.conf
|
||||
|
||||
# Redirect HTTP → HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name olp.example.com;
|
||||
|
||||
# Let's Encrypt ACME challenge
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS — TLS 1.3 only
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name olp.example.com;
|
||||
|
||||
# TLS config
|
||||
ssl_certificate /etc/letsencrypt/live/olp.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/olp.example.com/privkey.pem;
|
||||
ssl_protocols TLSv1.3; # TLS 1.3 only
|
||||
ssl_prefer_server_ciphers off; # TLS 1.3 manages its own
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-Frame-Options DENY;
|
||||
|
||||
# Rate limiting (per IP)
|
||||
limit_req zone=olp_limit burst=10 nodelay;
|
||||
|
||||
# Request body size (LLM prompts can be large but cap at 1MB)
|
||||
client_max_body_size 1m;
|
||||
|
||||
# Proxy to OLP
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:4567;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# SSE streaming support (critical for /v1/chat/completions)
|
||||
proxy_set_header Connection '';
|
||||
proxy_buffering off; # Don't buffer SSE
|
||||
proxy_cache off;
|
||||
chunked_transfer_encoding on;
|
||||
|
||||
# Long timeouts for LLM inference
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_read_timeout 300s; # 5 min — long reasoning
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
}
|
||||
|
||||
# Rate limit zone definition (in http {} block of nginx.conf)
|
||||
# limit_req_zone $binary_remote_addr zone=olp_limit:10m rate=30r/m;
|
||||
```
|
||||
|
||||
**Certbot auto-renewal:**
|
||||
|
||||
```bash
|
||||
sudo certbot certonly --webroot -w /var/www/certbot -d olp.example.com
|
||||
# Auto-renew via systemd timer (certbot installs this automatically)
|
||||
```
|
||||
|
||||
### Layer 3 — Application Auth (OLP Multi-Key)
|
||||
|
||||
**Principle:** Every request must carry a valid API key. No anonymous access. Per-key audit trail.
|
||||
|
||||
```json
|
||||
// ~/.olp/config.json on the cloud VM
|
||||
{
|
||||
"auth": {
|
||||
"allow_anonymous": false,
|
||||
"advertise_anonymous_key": false,
|
||||
"owner_only_endpoints": [
|
||||
"/health",
|
||||
"/v0/management/dashboard-data",
|
||||
"/v0/management/quota",
|
||||
"/v0/management/status",
|
||||
"/cache/stats",
|
||||
"/dashboard"
|
||||
],
|
||||
"fallback_detail_header_policy": "owner_only"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key provisioning plan:**
|
||||
|
||||
```
|
||||
┌───────────────┬──────────┬─────────────────────────────────────┐
|
||||
│ Key name │ Tier │ providers_enabled │
|
||||
├───────────────┼──────────┼─────────────────────────────────────┤
|
||||
│ cloud-owner │ owner │ all (dashboard + management access) │
|
||||
│ wife-ipad │ guest │ anthropic, openai │
|
||||
│ kid-laptop │ guest │ anthropic only (cost control) │
|
||||
│ maintainer-mb │ guest │ all (daily driver, not owner tier) │
|
||||
└───────────────┴──────────┴─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Why maintainer uses a guest key for daily driving:** owner key gives access to management endpoints. Routine IDE usage should not carry owner privilege. Owner key is used only for dashboard access and administration.
|
||||
|
||||
**Key lifecycle:**
|
||||
- Keys generated on the cloud VM via `olp-keys keygen`
|
||||
- Plaintext token communicated to family member via secure channel (Signal / iMessage, not email)
|
||||
- Each key logged independently in audit.ndjson (per-key `key_id` field)
|
||||
- Revocation: `olp-keys revoke --id=<key-id>` — immediate, no grace period
|
||||
|
||||
### Layer 4 — Process Isolation (Dedicated User + systemd)
|
||||
|
||||
**Principle:** OLP runs as a non-root, non-login user. Crash recovery is automatic.
|
||||
|
||||
```bash
|
||||
# Create dedicated user
|
||||
sudo useradd --system --shell /usr/sbin/nologin --home-dir /opt/olp olp
|
||||
|
||||
# OLP code
|
||||
sudo mkdir -p /opt/olp
|
||||
sudo git clone https://github.com/dtzp555-max/olp.git /opt/olp/app
|
||||
sudo chown -R olp:olp /opt/olp
|
||||
|
||||
# OLP data (keys, config, logs, cache)
|
||||
sudo mkdir -p /home/olp/.olp/{keys,logs,cache}
|
||||
sudo chown -R olp:olp /home/olp
|
||||
```
|
||||
|
||||
**systemd unit:**
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/olp.service
|
||||
[Unit]
|
||||
Description=OLP — Open LLM Proxy
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=olp
|
||||
Group=olp
|
||||
|
||||
WorkingDirectory=/opt/olp/app
|
||||
ExecStart=/usr/bin/node server.mjs
|
||||
|
||||
# Environment
|
||||
Environment=OLP_BIND=127.0.0.1
|
||||
Environment=OLP_PORT=4567
|
||||
Environment=NODE_ENV=production
|
||||
Environment=HOME=/home/olp
|
||||
|
||||
# Auto-restart on crash
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=5
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=false
|
||||
ReadWritePaths=/home/olp/.olp
|
||||
PrivateTmp=true
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
MemoryMax=1G
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=olp
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Layer 5 — Credential Protection (Provider OAuth Tokens)
|
||||
|
||||
**Principle:** OAuth tokens are the crown jewels. Stolen tokens = someone else using your Claude/OpenAI subscription.
|
||||
|
||||
```
|
||||
Credential storage on cloud VM:
|
||||
|
||||
~olp/
|
||||
├── .claude/
|
||||
│ └── .credentials.json # chmod 600, owner=olp
|
||||
├── .codex/
|
||||
│ └── auth.json # chmod 600, owner=olp
|
||||
└── .vibe/
|
||||
└── .env # chmod 600, owner=olp
|
||||
|
||||
Security measures:
|
||||
1. chmod 600 on all credential files (olp user only)
|
||||
2. Credential files NOT in the git repo (already .gitignored)
|
||||
3. No credential in env vars (OLP reads from filesystem)
|
||||
4. Credential transfer: scp from local machine, then delete local copy of the scp command from shell history
|
||||
5. Periodic rotation: re-auth quarterly (or on any suspicion of compromise)
|
||||
```
|
||||
|
||||
**Credential transfer procedure:**
|
||||
|
||||
```bash
|
||||
# FROM maintainer's Mac mini (one-time):
|
||||
|
||||
# 1. Claude credentials
|
||||
scp ~/.claude/.credentials.json opc@<cloud-ip>:/tmp/claude-cred.json
|
||||
ssh opc@<cloud-ip> "sudo mv /tmp/claude-cred.json /home/olp/.claude/.credentials.json && sudo chown olp:olp /home/olp/.claude/.credentials.json && sudo chmod 600 /home/olp/.claude/.credentials.json"
|
||||
|
||||
# 2. Codex credentials
|
||||
scp ~/.codex/auth.json opc@<cloud-ip>:/tmp/codex-cred.json
|
||||
ssh opc@<cloud-ip> "sudo mv /tmp/codex-cred.json /home/olp/.codex/auth.json && sudo chown olp:olp /home/olp/.codex/auth.json && sudo chmod 600 /home/olp/.codex/auth.json"
|
||||
|
||||
# 3. Mistral API key
|
||||
ssh opc@<cloud-ip> "sudo -u olp bash -c 'echo MISTRAL_API_KEY=sk-xxx > ~/.vibe/.env && chmod 600 ~/.vibe/.env'"
|
||||
|
||||
# 4. Verify
|
||||
ssh opc@<cloud-ip> "sudo -u olp node /opt/olp/app/bin/olp.mjs doctor --json" | jq '.checks[] | select(.name | contains("auth"))'
|
||||
```
|
||||
|
||||
### Layer 6 — Audit and Monitoring
|
||||
|
||||
**Principle:** Every request logged. Anomalies detectable. No silent failures.
|
||||
|
||||
**Audit (already built into OLP):**
|
||||
- `~/.olp/logs/audit.ndjson` — append-only, per-request, includes `key_id`, provider, model, cache hit/miss, fallback hops
|
||||
- Daily rotation: `audit-YYYY-MM-DD.ndjson` (built-in, triggers on first append after UTC midnight)
|
||||
- External rotation tool: `olp-audit-rotate` (idempotent, cron-safe)
|
||||
|
||||
**Additional monitoring for cloud deployment:**
|
||||
|
||||
```bash
|
||||
# Cron: daily audit rotation (belt-and-suspenders alongside in-server rotation)
|
||||
0 0 * * * /usr/bin/node /opt/olp/app/bin/olp-audit-rotate.mjs
|
||||
|
||||
# Cron: daily health check + alert
|
||||
*/5 * * * * curl -sf -H "Authorization: Bearer $OLP_OWNER_KEY" https://olp.example.com/health > /dev/null || echo "OLP health check failed at $(date)" >> /home/olp/alerts.log
|
||||
|
||||
# Cron: audit log size check (alert if >100MB — suggests anomalous traffic)
|
||||
0 6 * * * find /home/olp/.olp/logs -name 'audit*.ndjson' -size +100M -exec echo "Large audit log: {}" \; >> /home/olp/alerts.log
|
||||
|
||||
# Cron: disk usage check
|
||||
0 6 * * * df -h / | awk 'NR==2 && $5+0 > 80 {print "Disk usage above 80%: "$5}' >> /home/olp/alerts.log
|
||||
```
|
||||
|
||||
**What to watch for (manually, weekly):**
|
||||
1. `olp-keys list` — any unexpected keys?
|
||||
2. Dashboard (`/dashboard`) — unusual request volume? Unknown providers being hit?
|
||||
3. `journalctl -u olp --since "7 days ago" | grep -c ERROR` — error spike?
|
||||
4. Audit log: `grep "fallback" ~/.olp/logs/audit.ndjson | wc -l` — fallback frequency (high = provider instability)
|
||||
|
||||
### Layer 7 — Update and Recovery
|
||||
|
||||
**Principle:** Rollback within 60 seconds. No data loss on failed update.
|
||||
|
||||
**Update procedure:**
|
||||
|
||||
```bash
|
||||
# SSH to cloud VM as opc
|
||||
|
||||
# 1. Snapshot before update (Oracle Cloud console or CLI)
|
||||
# OCI CLI: oci compute boot-volume-backup create ...
|
||||
|
||||
# 2. Pull latest code
|
||||
cd /opt/olp/app
|
||||
sudo -u olp git fetch origin main
|
||||
sudo -u olp git log --oneline HEAD..origin/main # review what's coming
|
||||
|
||||
# 3. Run tests BEFORE deploying
|
||||
sudo -u olp git checkout main
|
||||
sudo -u olp git pull
|
||||
sudo -u olp node test-features.mjs
|
||||
# STOP if tests fail
|
||||
|
||||
# 4. Restart service
|
||||
sudo systemctl restart olp
|
||||
sleep 3
|
||||
sudo systemctl status olp # verify running
|
||||
|
||||
# 5. Smoke test
|
||||
curl -sf -H "Authorization: Bearer $OLP_OWNER_KEY" https://olp.example.com/health | jq .ok
|
||||
# Expect: true
|
||||
```
|
||||
|
||||
**Rollback:**
|
||||
|
||||
```bash
|
||||
# If update breaks things:
|
||||
cd /opt/olp/app
|
||||
sudo -u olp git checkout <previous-tag> # e.g. v0.6.0
|
||||
sudo systemctl restart olp
|
||||
```
|
||||
|
||||
**Backup (automated):**
|
||||
|
||||
```bash
|
||||
# Cron: daily backup of OLP state (keys + config + recent audit)
|
||||
0 3 * * * tar czf /home/opc/backups/olp-state-$(date +\%Y\%m\%d).tar.gz -C /home/olp .olp/keys .olp/config.json .olp/logs/audit.ndjson 2>/dev/null; find /home/opc/backups -name 'olp-state-*' -mtime +30 -delete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Checklist
|
||||
|
||||
Execute in order. Each step has a verification gate — do not proceed if the gate fails.
|
||||
|
||||
### Phase A — VM Preparation
|
||||
|
||||
```
|
||||
[ ] A1. SSH to Oracle Cloud VM, verify Node.js >= 18
|
||||
Gate: `node --version` prints v18+
|
||||
|
||||
[ ] A2. Create `olp` system user
|
||||
Gate: `id olp` shows the user exists
|
||||
|
||||
[ ] A3. Clone OLP repo to /opt/olp/app
|
||||
Gate: `sudo -u olp node /opt/olp/app/test-features.mjs` — all tests pass
|
||||
|
||||
[ ] A4. Install provider CLIs (as olp user)
|
||||
- npm install -g @anthropic-ai/claude-code
|
||||
- npm install -g @openai/codex
|
||||
- (mistral vibe if needed)
|
||||
Gate: `which claude && which codex` both resolve
|
||||
|
||||
[ ] A5. Transfer OAuth credentials (Layer 5 procedure)
|
||||
Gate: `sudo -u olp claude auth status` shows authenticated
|
||||
```
|
||||
|
||||
### Phase B — Security Hardening
|
||||
|
||||
```
|
||||
[ ] B1. Configure OCI Security List (Layer 1)
|
||||
Gate: nmap from external IP shows only 22 and 443 open
|
||||
|
||||
[ ] B2. Configure iptables backup (Layer 1)
|
||||
Gate: `sudo iptables -L -n` matches the plan
|
||||
|
||||
[ ] B3. Install + configure Nginx (Layer 2)
|
||||
Gate: `curl -I http://olp.example.com` returns 301 → HTTPS
|
||||
|
||||
[ ] B4. Obtain Let's Encrypt certificate
|
||||
Gate: `curl -I https://olp.example.com` returns valid cert
|
||||
|
||||
[ ] B5. Verify Nginx SSE passthrough
|
||||
Gate: test streaming request completes without timeout
|
||||
```
|
||||
|
||||
### Phase C — OLP Configuration
|
||||
|
||||
```
|
||||
[ ] C1. Write ~/.olp/config.json (Layer 3 — auth config)
|
||||
Gate: config validates (no startup warnings in journal)
|
||||
|
||||
[ ] C2. Generate owner key
|
||||
Gate: `olp-keys list --owner-only` shows 1 owner key
|
||||
|
||||
[ ] C3. Generate family guest keys (one per person)
|
||||
Gate: `olp-keys list` shows correct count
|
||||
|
||||
[ ] C4. Install systemd unit (Layer 4)
|
||||
Gate: `systemctl status olp` shows active (running)
|
||||
|
||||
[ ] C5. Verify /health with owner key
|
||||
Gate: `curl -H "Authorization: Bearer $OWNER_KEY" https://olp.example.com/health | jq .ok` → true
|
||||
|
||||
[ ] C6. Verify /health rejects unauthenticated
|
||||
Gate: `curl https://olp.example.com/health` → 401
|
||||
|
||||
[ ] C7. Verify guest key cannot access /dashboard
|
||||
Gate: `curl -H "Authorization: Bearer $GUEST_KEY" https://olp.example.com/dashboard` → 403
|
||||
|
||||
[ ] C8. End-to-end LLM request with guest key
|
||||
Gate: streaming chat completion returns a valid response
|
||||
```
|
||||
|
||||
### Phase D — Monitoring Setup
|
||||
|
||||
```
|
||||
[ ] D1. Install cron jobs (Layer 6)
|
||||
Gate: `crontab -l` shows all 4 jobs
|
||||
|
||||
[ ] D2. Verify daily backup cron
|
||||
Gate: manual trigger produces valid tar.gz
|
||||
|
||||
[ ] D3. Test health-check alert
|
||||
Gate: stop OLP, wait 5min, check alerts.log has entry
|
||||
```
|
||||
|
||||
### Phase E — Family Onboarding
|
||||
|
||||
```
|
||||
[ ] E1. Send each family member their API key via Signal/iMessage
|
||||
(NOT via email, NOT via any cloud-stored medium)
|
||||
|
||||
[ ] E2. Each family member configures their IDE:
|
||||
export OPENAI_BASE_URL=https://olp.example.com/v1
|
||||
export OPENAI_API_KEY=olp_<their-key>
|
||||
|
||||
[ ] E3. Each family member runs a test prompt
|
||||
Gate: audit.ndjson shows their key_id in the log
|
||||
|
||||
[ ] E4. Verify per-key provider scoping
|
||||
Gate: kid's key cannot hit providers outside their scope
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Security Threat Model
|
||||
|
||||
| Threat | Mitigation | Residual Risk |
|
||||
|---|---|---|
|
||||
| **Brute-force API key** | 32-byte entropy = 2^256 keyspace; Nginx rate limit 30r/m | Negligible |
|
||||
| **TLS downgrade** | TLS 1.3 only; HSTS header | None with modern clients |
|
||||
| **Credential theft (OAuth tokens on VM)** | chmod 600 + dedicated user + no root access to OLP dirs | VM root compromise (mitigated by OCI IAM) |
|
||||
| **Stolen guest key** | Single-key revocation via `olp-keys revoke`; per-key audit trail for forensics | Window between theft and detection |
|
||||
| **DDoS** | OCI DDoS protection (free tier) + Nginx rate limit + Nginx connection limit | Sustained volumetric attack may overwhelm free-tier VM |
|
||||
| **Provider credential abuse** | OLP is the only consumer; anomalous spend visible on provider dashboard | Provider-side detection lag |
|
||||
| **Supply chain (OLP code tampered)** | Git clone from known repo; `npm test` before deploy; no npm dependencies | Compromised maintainer GitHub account |
|
||||
| **Log exfiltration** | audit.ndjson contains no message content (PII guard per ADR 0008); only metadata | Key IDs in logs (low sensitivity) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Operational Runbooks
|
||||
|
||||
### Runbook: OAuth Token Expired
|
||||
|
||||
```
|
||||
Symptom: /health shows provider auth.ok=false; fallback firing on every request
|
||||
Diagnosis: sudo -u olp claude auth status → "not authenticated" or expired
|
||||
|
||||
Fix:
|
||||
1. sudo -u olp claude setup-token
|
||||
2. Complete OAuth flow (browser URL → paste code)
|
||||
3. Verify: sudo -u olp claude auth status → authenticated
|
||||
4. No OLP restart needed — next spawn picks up new credentials
|
||||
```
|
||||
|
||||
### Runbook: Revoke a Compromised Key
|
||||
|
||||
```
|
||||
Symptom: suspicious traffic in audit.ndjson from a specific key_id
|
||||
grep "<suspected-key-id>" ~/.olp/logs/audit.ndjson | tail -20
|
||||
|
||||
Fix:
|
||||
1. olp-keys revoke --id=<key-id>
|
||||
2. Notify family member: "Your key was revoked. Here's a new one."
|
||||
3. olp-keys keygen --name=<new-name> --providers=<same-providers>
|
||||
4. Send new key via secure channel
|
||||
```
|
||||
|
||||
### Runbook: VM Disk Full
|
||||
|
||||
```
|
||||
Symptom: OLP stops writing audit logs; new requests may fail
|
||||
Diagnosis: df -h /
|
||||
|
||||
Fix:
|
||||
1. Purge old audit logs: find ~/.olp/logs -name 'audit-202*.ndjson' -mtime +90 -delete
|
||||
2. Purge old backups: find /home/opc/backups -name 'olp-state-*' -mtime +60 -delete
|
||||
3. Purge cache if needed: rm -rf ~/.olp/cache/*
|
||||
4. Verify: df -h / shows >20% free
|
||||
```
|
||||
|
||||
### Runbook: OLP Process Crash Loop
|
||||
|
||||
```
|
||||
Symptom: systemctl status olp shows "activating (auto-restart)"
|
||||
Diagnosis: journalctl -u olp --since "10 min ago" | tail -50
|
||||
|
||||
Common causes:
|
||||
- Port conflict → check `lsof -nP -iTCP:4567`
|
||||
- Corrupt config.json → validate JSON syntax
|
||||
- Node.js version drift → `node --version`
|
||||
|
||||
Fix:
|
||||
1. Fix root cause
|
||||
2. sudo systemctl restart olp
|
||||
3. Gate: `curl -H "Authorization: Bearer $OWNER_KEY" https://olp.example.com/health | jq .ok`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Cost Estimate (Oracle Cloud Free Tier)
|
||||
|
||||
| Resource | Spec | Cost |
|
||||
|---|---|---|
|
||||
| VM | ARM Ampere A1 (4 OCPU, 24GB RAM) | **Free** (Always Free tier) |
|
||||
| Boot volume | 200GB | **Free** (up to 200GB) |
|
||||
| Outbound bandwidth | 10TB/month | **Free** (first 10TB) |
|
||||
| Public IP | 1 reserved | **Free** |
|
||||
| Domain | olp.example.com | ~$10/year (external registrar) |
|
||||
| TLS cert | Let's Encrypt | **Free** |
|
||||
| **Total** | | **~$10/year** (domain only) |
|
||||
|
||||
Oracle Cloud's Always Free ARM VM is overprovisioned for this use case. OLP + Nginx + 3 provider CLIs will use <1GB RAM and negligible CPU (the LLM inference happens at the provider, not here).
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration Path to Commercial
|
||||
|
||||
This family deployment is a stepping stone. When commercial service is ready:
|
||||
|
||||
| Aspect | Family (this plan) | Commercial (future) |
|
||||
|---|---|---|
|
||||
| Upstream | spawn CLI (subscription) | direct API (commercial key) |
|
||||
| Auth | OLP multi-key (filesystem) | Registration + billing system |
|
||||
| TLS | Let's Encrypt (single domain) | Managed cert (Cloudflare / AWS ACM) |
|
||||
| Compute | Single VM (Oracle Free) | Container cluster (auto-scale) |
|
||||
| Monitoring | Cron + manual | Prometheus + Grafana + PagerDuty |
|
||||
| Rate limit | Nginx per-IP | Per-key token bucket in OLP |
|
||||
| Data | ~/.olp/ filesystem | PostgreSQL + S3 |
|
||||
|
||||
The deployment experience from this plan directly informs the commercial architecture. Every operational runbook becomes a feature requirement for the commercial platform.
|
||||
|
||||
---
|
||||
|
||||
**Authors:** project maintainer (with AI drafting assistance)
|
||||
**Created:** 2026-05-27
|
||||
Reference in New Issue
Block a user