You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add --identity-stdin; document Mac SE and Actions runner modes
--identity-stdin (serve):
Reads the age identity from stdin instead of a file. The private key is
parsed into memory, the source buffer is immediately scrambled with
memguard.ScrambleBytes, and the key never touches disk. Mutually
exclusive with --identity; exactly one is required.
CI integration test updated to start the proxy via --identity-stdin.
README — two deployment mode sections with Mermaid diagrams:
Mode 1: Mac persistent proxy with Secure Enclave key
- age-plugin-se keygen --access-control none for silent, device-bound
decryption (no Touch ID at runtime)
- botlockbox seal --recipient age1se1q…
- launchd user-session agent keeps the proxy running at login
- secrets.age is hardware-bound and useless on any other Mac
- Covers one-time setup, launchd install, and secret rotation via reload
Mode 2: GitHub Actions self-hosted runner (ephemeral key)
- Fresh age keypair generated each run; private key piped via stdin
(--identity-stdin) so it never touches disk
- Credentials sourced from GitHub Actions secrets at seal time
- Agent runs as an unprivileged user; proxy injects credentials the
agent cannot read from env or disk
- Threat table comparing with/without botlockbox
- Process isolation pattern (two-UID Docker entrypoint)
CLI reference updated for --identity / --recipient / --identity-stdin
flags and their mutual-exclusion rules.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
### Mode 1: Mac persistent proxy with Secure Enclave key
118
+
119
+
Use [`age-plugin-se`](https://github.com/remko/age-plugin-se) to bind `secrets.age` to a specific Mac's Secure Enclave. The private key is generated inside the chip and **cannot be exported or used on any other machine**. With `--access-control none`, decryption is silent — no Touch ID prompt — making it suitable for a `launchd` user-session agent that starts at login.
120
+
121
+
```mermaid
122
+
flowchart TD
123
+
subgraph setup["One-time setup"]
124
+
direction LR
125
+
KG["age-plugin-se keygen\n--access-control none"]
126
+
SE["Secure Enclave\nT2 / M-series"]
127
+
SE -->|"generates & stores\nprivate key in hardware"| KG
128
+
KG -->|"identity reference\n(AGE-PLUGIN-SE-1…)"| IDFILE["~/.botlockbox/identity.txt"]
The runner hosts AI agents that call external APIs. Credentials live in GitHub Actions secrets and are never placed in agent environment variables. An ephemeral age key is generated fresh for each workflow run and piped directly to `botlockbox serve` via `--identity-stdin` — **the private key never touches disk**.
| Agent reads `$OPENAI_KEY` from env | ✗ exposed | ✓ not in env |
266
+
| Agent reads credentials from disk | ✗ if written to file | ✓ never on disk |
267
+
| Agent logs or exfiltrates request body | ✗ credential visible | ✓ scrubbed from responses |
268
+
| Leaked `secrets.age` file after run | ✗ decryptable with key | ✓ key is ephemeral, gone after job |
269
+
| Agent calls an unlisted host | ✗ no enforcement | ✓ sealed allowlist blocks injection |
270
+
271
+
**Process isolation (recommended):**
272
+
273
+
For stronger isolation, run botlockbox as a privileged user and the agent as a separate unprivileged user. The agent can reach the proxy over TCP but cannot read botlockbox's memory or files.
In Docker, this is a two-stage entrypoint: start the proxy as root, then `exec su -c "python agent.py" agent`.
281
+
282
+
---
283
+
113
284
## CLI reference
114
285
115
286
### `botlockbox seal`
116
287
117
288
Reads plaintext secrets from stdin, binds them to the host allowlist derived from the config, and writes an `age`-encrypted envelope to `secrets_file`. Also sets the config to read-only (`0444`) to prevent post-seal tampering.
118
289
119
290
```
120
-
botlockbox seal --config <path> --identity <path>
291
+
botlockbox seal --config <path> (--identity <path> | --recipient <pubkey>)
121
292
```
122
293
123
294
| Flag | Default | Description |
124
295
|------|---------|-------------|
125
296
| `--config` | `botlockbox.yaml` | Path to `botlockbox.yaml` |
126
-
|`--identity`|_(required)_| Path to an `age` identity file (e.g. `~/.age/identity.txt`) |
297
+
| `--identity` | — | Path to an age X25519 identity file; derives the recipient from the key. Mutually exclusive with `--recipient`. |
298
+
| `--recipient` | — | Age public key string (`age1…` or `age1se1…`). Use this for plugin keys such as `age-plugin-se`. Mutually exclusive with `--identity`. |
299
+
300
+
Exactly one of `--identity` or `--recipient` is required.
127
301
128
-
**Stdin format** — YAML key/value pairs, one secret per line:
|`--config`|`botlockbox.yaml`| Path to `botlockbox.yaml`|
159
-
| `--identity` | _(required)_ | Path to an `age` identity file |
326
+
|`--identity`| — | Path to an age identity file. Mutually exclusive with `--identity-stdin`. |
327
+
|`--identity-stdin`|`false`| Read age identity from stdin; the key is never written to disk. Mutually exclusive with `--identity`. |
328
+
|`--pidfile`| — | Write the proxy PID here; used by `botlockbox reload`. |
329
+
|`--ca-cert`| — | Write the ephemeral MITM CA public certificate PEM here so clients can trust it. |
330
+
331
+
Exactly one of `--identity` or `--identity-stdin` is required.
160
332
161
333
**Startup sequence:**
162
334
163
335
1. Load and parse `botlockbox.yaml`
164
336
2. Decrypt `secrets_file` using the age identity
165
-
3. Validate the sealed envelope against the live config — any secret or host present in the config that was not committed at seal time causes an immediate `os.Exit(1)` with a descriptive error
337
+
3. Validate the sealed envelope against the live config — any secret or host present in the config that was not committed at seal time causes an immediate `os.Exit(1)`
166
338
4. Load each secret into a `memguard` encrypted enclave; scramble the plaintext bytes immediately
167
339
5. Apply OS hardening (`PR_SET_DUMPABLE=0`, `mlockall`, `RLIMIT_CORE=0` on Linux)
168
340
6. Generate an ephemeral in-memory ECDSA P-256 MITM CA (24 h lifetime, never written to disk)
169
-
7. Begin accepting connections
341
+
7. Write CA cert PEM and PID file if requested
342
+
8. Begin accepting connections
170
343
171
-
**Output on successful start:**
344
+
---
172
345
173
-
```
174
-
Host binding verified
175
-
botlockbox listening on 127.0.0.1:8080
176
-
```
346
+
### `botlockbox reload`
177
347
178
-
**Security violation example** (config edited after seal):
348
+
Sends SIGHUP to a running `serve` process, triggering a live secret reload. The proxy keeps serving with old secrets if the reload fails for any reason.
179
349
180
350
```
181
-
SECURITY VIOLATION: secret "github_token" is referenced in botlockbox.yaml but was not present at seal time -- re-seal to add new secrets
182
-
exit status 1
351
+
botlockbox reload --pidfile <path>
183
352
```
184
353
354
+
| Flag | Default | Description |
355
+
|------|---------|-------------|
356
+
|`--pidfile`|_(required)_| Path to the PID file written by `botlockbox serve`. |
0 commit comments