a73x

8ac98377

docs: add exfiltration detection design spec

a73x   2026-03-29 16:00

Commit message
docs: add exfiltration detection design spec

docs/superpowers/specs/2026-03-29-exfil-detection-design.md
Old New
@@ -0,0 +1,106 @@
1 # Exfiltration Detection for nono-proxy
2
3 ## Problem
4
5 The nono proxy currently does host-based allowlisting only. A sandboxed process can still exfiltrate sensitive data (SSH keys, passwords, API tokens) to an approved host. We need content inspection to detect and block this.
6
7 ## Approach
8
9 Inline MITM in the existing proxy. Extend the single `nono-proxy` binary to do TLS interception on CONNECT requests, enabling full request body scanning for both HTTP and HTTPS traffic.
10
11 ## CA Certificate Management
12
13 On first startup, `nono-proxy` checks for `~/.local/share/nono/ca.key` and `~/.local/share/nono/ca.pem`. If missing, it generates:
14
15 - An ECDSA P-256 CA private key
16 - A self-signed CA certificate ("Nono Proxy CA", 10-year validity)
17
18 Saved to disk and reused on subsequent runs. Per-host leaf certificates are generated on-the-fly at CONNECT time, signed by this CA, and cached in-memory (keyed by hostname) for the process lifetime.
19
20 The `nono` wrapper script is updated to:
21
22 - Bind-mount `ca.pem` into the sandbox (read-only)
23 - Set `SSL_CERT_FILE` and `NODE_EXTRA_CA_CERTS` so tools inside the sandbox trust it
24
25 ## MITM CONNECT Handling
26
27 The current `handleConnect` does a blind TCP tunnel. The new flow:
28
29 1. Hijack the client connection, send `200 Connection Established`
30 2. Generate (or fetch from cache) a leaf cert for the requested hostname, signed by the nono CA
31 3. Wrap the client connection in a `tls.Server` using that leaf cert
32 4. Establish a real `tls.Client` connection to the target host
33 5. Read HTTP requests from the client-side TLS connection, run them through the scanner, and if clean, forward to the target
34 6. Relay the response back to the client
35
36 For non-HTTP protocols over CONNECT (e.g. WebSockets upgrade after initial HTTP), forward the upgraded connection as a raw tunnel after the initial request passes scanning.
37
38 ## Request Body Scanner
39
40 A `scanner` package with a `Scan(body []byte) []Finding` function. Each `Finding` has a `Rule` name and a `Match` snippet (truncated for logging, not the full secret).
41
42 ### Default Rules
43
44 | Rule | Pattern |
45 |------|---------|
46 | `ssh-private-key` | `-----BEGIN (OPENSSH\|RSA\|DSA\|EC\|ED25519) PRIVATE KEY-----` |
47 | `pgp-private-key` | `-----BEGIN PGP PRIVATE KEY BLOCK-----` |
48 | `basic-auth` | `Authorization: Basic` header |
49 | `bearer-token` | `Authorization: Bearer` header |
50 | `aws-access-key` | `AKIA[0-9A-Z]{16}` |
51 | `github-token` | `gh[ps]_[A-Za-z0-9_]{36,}` |
52 | `openai-key` | `sk-[A-Za-z0-9]{32,}` |
53 | `password-field` | `password=` or `"password":` in body |
54 | `env-file` | 3+ consecutive lines matching `[A-Z_]+=.+` |
55
56 ### Configurable Rules
57
58 Rules are loaded from `~/.local/share/nono/rules.yaml`. On first run, `nono-proxy` writes a default file with the built-in rules if one doesn't exist. Users can add, remove, or modify rules.
59
60 Format:
61
62 ```yaml
63 rules:
64 - name: ssh-private-key
65 pattern: "-----BEGIN (OPENSSH|RSA|DSA|EC|ED25519) PRIVATE KEY-----"
66 - name: github-token
67 pattern: "gh[ps]_[A-Za-z0-9_]{36,}"
68 ```
69
70 Each rule is a name + regex pattern. The scanner compiles them at startup and returns an error if any pattern is invalid.
71
72 ### Behavior
73
74 - Scans outbound request bodies only (not responses)
75 - Reads the full request body via `io.ReadAll`, scans, and if clean replays via `bytes.Reader`
76 - On match: logs `BLOCKED <method> <host> [rule1, rule2]`, returns 403 with message like `"request blocked: contains sensitive data (ssh-private-key)"`
77
78 ## Request Flow
79
80 ```
81 Client in sandbox
82 -> plain HTTP or CONNECT to nono-proxy (port 9854)
83 -> host allowlist check (existing logic, unchanged)
84 -> if CONNECT: MITM TLS termination, read inner HTTP request
85 -> read request body, run scanner rules
86 -> if findings: log BLOCKED, return 403
87 -> if clean: forward to target, relay response
88 ```
89
90 ## Code Changes
91
92 - `proxy/proxy.go` — `Proxy` struct gains `caKey`/`caCert` fields and `certCache map[string]*tls.Certificate`. `New()` takes a CA path in addition to the hosts file. `handleConnect` replaced with MITM flow. `handleHTTP` gets scanner check before forwarding.
93 - `scanner/` — new package with `Rule`, `Finding`, `Scanner` (loads rules from YAML, compiles regexes, exposes `Scan([]byte) []Finding`)
94 - `ca/` — new package with `LoadOrCreateCA(dir string)` and `GenerateLeafCert(host string, ca)` functions
95 - `cmd/nono-proxy/main.go` — loads CA at startup, passes to `proxy.New()`, writes default `rules.yaml` if missing
96 - `nono` script — adds `--ro-bind` for `ca.pem`, sets `SSL_CERT_FILE` and `NODE_EXTRA_CA_CERTS`
97
98 ## New Dependencies
99
100 - `gopkg.in/yaml.v3` for rules config
101 - Everything else is stdlib (`crypto/x509`, `crypto/tls`, `crypto/ecdsa`)
102
103 ## Scan Direction
104
105 - Request bodies only (outbound exfiltration detection)
106 - No size cap — large uploads are themselves suspicious