feat(rfc): Pass Eleven — security model, God Mode, sandbox bypass
CRITICAL: P11-2 — The Fs sandbox (P2-2: "correctly unexported") is bypassed by core/agent using unsafe.Pointer to overwrite Fs.root. The security boundary exists in theory but is broken in practice. P11-1: Every service has God Mode — full access to everything P11-2: Fs.root bypassed via unsafe.Pointer (paths.go, detect.go) P11-3: core.Env() exposes all secrets (API keys, tokens) P11-4: ACTION event spoofing — fake AgentCompleted triggers pipeline P11-5: RegisterAction installs spy handler (sees all IPC) P11-6: No audit trail — no logging of security-relevant ops P11-7: ServiceLock has no authentication (anyone can lock) P11-8: No revocation — services join but can never be ejected The conclave trust model: all first-party, no isolation. Acceptable for v0.8.0 (trusted code). Needs capability model for v0.9.0+ (plugins, third-party services). Eleven passes, 88 findings, 3,400+ lines. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
20f3ee30b8
commit
caa1dea83d
1 changed files with 143 additions and 0 deletions
143
docs/RFC.md
143
docs/RFC.md
|
|
@ -3165,6 +3165,149 @@ Passes are referenced as P2-1, P3-2, etc. but they're not in the table of conten
|
|||
|
||||
---
|
||||
|
||||
## Pass Eleven — Security Model
|
||||
|
||||
> Eleventh review. The conclave's threat model. What can a service do?
|
||||
> What's the actual isolation? Where are the secrets?
|
||||
|
||||
### P11-1. Every Service Has God Mode
|
||||
|
||||
A registered service receives `*Core`. With it, it can:
|
||||
|
||||
| Capability | Method | Risk |
|
||||
|-----------|--------|------|
|
||||
| Read all config | `c.Config().Get(anything)` | Secret exposure |
|
||||
| Write all config | `c.Config().Set(anything, anything)` | Config corruption |
|
||||
| Read all data | `c.Data().ReadString(any/path)` | Data exfiltration |
|
||||
| List all services | `c.Services()` | Reconnaissance |
|
||||
| Access any service | `ServiceFor[T](c, name)` | Lateral movement |
|
||||
| Broadcast any event | `c.ACTION(any_message)` | Event spoofing |
|
||||
| Register handlers | `c.RegisterAction(spy)` | Eavesdropping |
|
||||
| Register commands | `c.Command("admin/nuke", fn)` | Capability injection |
|
||||
| Read environment | `c.Env("FORGE_TOKEN")` | Secret theft |
|
||||
| Write filesystem | `c.Fs().Write(any, any)` | Data destruction |
|
||||
| Delete filesystem | `c.Fs().DeleteAll(any)` | Data destruction |
|
||||
|
||||
There's no per-service permission model. Registration grants full access. This is fine for a trusted conclave (all services from your own codebase) but dangerous for plugins or third-party services.
|
||||
|
||||
**Resolution:** For v0.8.0, accept God Mode — all services are first-party. For v0.9.0+, consider capability-based Core views where a service receives a restricted `*Core` that only exposes permitted subsystems.
|
||||
|
||||
### P11-2. The Fs Sandbox Is Bypassed by unsafe.Pointer
|
||||
|
||||
Pass Two (P2-2) said `Fs.root` is "correctly unexported — the security boundary." But core/agent bypasses it:
|
||||
|
||||
```go
|
||||
type fsRoot struct{ root string }
|
||||
f := &core.Fs{}
|
||||
(*fsRoot)(unsafe.Pointer(f)).root = root
|
||||
```
|
||||
|
||||
Two files (`paths.go` and `detect.go`) use `unsafe.Pointer` to overwrite the private `root` field, creating unrestricted Fs instances. The security boundary that P2-2 praised is already broken by the first consumer.
|
||||
|
||||
**Resolution:** Add `Fs.NewUnrestricted()` or `Fs.New("/")` as a legitimate API. If consumers need unrestricted access, give them a door instead of letting them pick the lock. Then `go vet` rules or linting can flag `unsafe.Pointer` usage on Core types.
|
||||
|
||||
### P11-3. core.Env() Exposes All Environment Variables — Including Secrets
|
||||
|
||||
```go
|
||||
c.Env("FORGE_TOKEN") // returns the token
|
||||
c.Env("OPENAI_API_KEY") // returns the key
|
||||
c.Env("ANTHROPIC_API_KEY") // returns the key
|
||||
```
|
||||
|
||||
Any service can read any environment variable. API keys, tokens, database passwords — all accessible via `c.Env()`. There's no secret/non-secret distinction.
|
||||
|
||||
**Resolution:** Consider `c.Secret(name)` that reads from a secure store (encrypted file, vault) rather than plain environment variables. Or `c.Env()` with a redaction list — keys matching `*TOKEN*`, `*KEY*`, `*SECRET*`, `*PASSWORD*` are redacted in logs but still accessible to code.
|
||||
|
||||
The logging system already has `SetRedactKeys` — extend this to Env access logging.
|
||||
|
||||
### P11-4. ACTION Event Spoofing — Any Code Can Emit Any Message
|
||||
|
||||
P6-6 noted this from a design perspective. From a security perspective:
|
||||
|
||||
```go
|
||||
// A rogue service can fake agent completions:
|
||||
c.ACTION(messages.AgentCompleted{
|
||||
Agent: "codex", Repo: "go-io", Status: "completed",
|
||||
})
|
||||
// This triggers the ENTIRE pipeline: QA → PR → Verify → Merge
|
||||
// For an agent that never existed
|
||||
```
|
||||
|
||||
No authentication on messages. No sender identity. Any service can emit any message type and trigger any pipeline.
|
||||
|
||||
**Resolution:** Named Actions (Section 18) help — `c.Action("event.agent.completed").Emit()` requires the action to be registered. But the emitter still isn't authenticated. Consider adding sender identity to IPC:
|
||||
|
||||
```go
|
||||
c.ACTION(msg, core.Sender("agentic")) // identifies who sent it
|
||||
```
|
||||
|
||||
Handlers can then filter by sender. Not cryptographic security — but traceability.
|
||||
|
||||
### P11-5. RegisterAction Can Install a Spy Handler
|
||||
|
||||
```go
|
||||
c.RegisterAction(func(c *core.Core, msg core.Message) core.Result {
|
||||
// sees EVERY message in the conclave
|
||||
// can log, exfiltrate, or modify behaviour
|
||||
log.Printf("SPY: %T %+v", msg, msg)
|
||||
return core.Result{OK: true}
|
||||
})
|
||||
```
|
||||
|
||||
A single `RegisterAction` call installs a handler that receives every IPC message. Combined with P11-1 (God Mode), a service can silently observe all inter-service communication.
|
||||
|
||||
**Resolution:** For trusted conclaves, this is a feature (monitoring, debugging). For untrusted environments, IPC handlers need scope: "this handler only receives messages of type X." Named Actions (Section 18) solve this — each handler is registered for a specific action, not all messages.
|
||||
|
||||
### P11-6. No Audit Trail — No Logging of Security-Relevant Operations
|
||||
|
||||
There's no automatic logging when:
|
||||
- A service registers (who registered what)
|
||||
- A handler is added (who's listening)
|
||||
- Config is modified (who changed what)
|
||||
- Environment variables are read (who accessed secrets)
|
||||
- Filesystem is written (who wrote where)
|
||||
|
||||
All operations are silent. The `core.Security()` log level exists but nothing calls it.
|
||||
|
||||
**Resolution:** Core should emit `core.Security()` logs for:
|
||||
- Service registration: `Security("service.registered", "name", name, "caller", caller)`
|
||||
- Config writes: `Security("config.set", "key", key, "caller", caller)`
|
||||
- Secret access: `Security("secret.accessed", "key", key, "caller", caller)`
|
||||
- Fs writes outside Data mounts: `Security("fs.write", "path", path, "caller", caller)`
|
||||
|
||||
### P11-7. ServiceLock Has No Authentication
|
||||
|
||||
```go
|
||||
c.LockApply() // anyone can call this
|
||||
c.LockEnable() // anyone can call this
|
||||
```
|
||||
|
||||
ServiceLock prevents late registration. But any code with `*Core` can call `LockApply()` prematurely, locking out legitimate services that haven't registered yet. Or call `LockEnable()` to arm the lock before it should be.
|
||||
|
||||
**Resolution:** `LockApply` should only be callable from `New()` (during construction). Post-construction, the lock is managed by Core, not by consumers. With Registry[T], `Lock()` becomes a Registry method — and Registry access can be scoped.
|
||||
|
||||
### P11-8. The Permission-by-Registration Model Has No Revocation
|
||||
|
||||
Section 17.7 says "no handler = no capability." But once a service registers, there's no way to revoke its capabilities:
|
||||
|
||||
- Can't unregister a service
|
||||
- Can't remove an ACTION handler
|
||||
- Can't revoke filesystem access
|
||||
- Can't deregister an Action
|
||||
|
||||
The conclave is append-only. Services join but never leave (except on shutdown). For hot-reload (P3-8), a misbehaving service can't be ejected.
|
||||
|
||||
**Resolution:** Registry needs `Delete(name)` and `Disable(name)`:
|
||||
|
||||
```go
|
||||
c.Registry("actions").Disable("rogue.action") // stops dispatch, keeps registered
|
||||
c.Registry("services").Delete("rogue") // removes entirely
|
||||
```
|
||||
|
||||
`Disable` is soft — the action exists but doesn't execute. `Delete` is hard — the action is gone. Both require the caller to have sufficient authority — which loops back to P11-1 (God Mode).
|
||||
|
||||
---
|
||||
|
||||
## Versioning
|
||||
|
||||
### Release Model
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue