- Backend bind address is
127.0.0.1; no public listener is configured by default. - Production authentication requires tailnet reachability through Tailscale Serve plus a bearer token.
- Development auth bypass requires both
service.development_mode=trueandauth.mode=development. - Audio and transcripts are not retained; temporary files are mode 0600 in a mode 0700 directory and removed on all normal session paths plus stale startup cleanup.
- Normal logs contain lifecycle/errors and timings only, never audio or transcript content.
- Queue, session count, request body, WebSocket frame, duration, and helper response limits are bounded.
speechd-admin token create generates 256-bit random URL-safe tokens, prints the plaintext once, and stores only a SHA-256 digest plus metadata in a mode-0600 JSON file. Authentication compares fixed-length digest bytes in constant time. Revoke and rotate preserve metadata without recovering plaintext. Tokens are never placed in configuration or Git.
| Area | Control | Evidence |
|---|---|---|
| Network exposure | Loopback listener; Tailscale Serve script refuses Funnel | SpeechHTTPServer, scripts/configure-tailscale.sh |
| TLS | Tailscale Serve owns tailnet HTTPS certificate | ARCHITECTURE.md, script dry run |
| Authentication | Bearer required on API/streaming routes | SpeechService.authorize, tests |
| Token storage | SHA-256 digests, restrictive permissions, rotation/revocation | Auth.swift |
| Input bounds | Content length, multipart size, PCM alignment, session bytes/time | HTTPServer.swift, Audio.swift, Service.swift |
| WebSocket bounds | Masked client frames, max payload, valid opcodes/control JSON | HTTPServer.swift |
| Temporary files | Random session names, 0600 files, removal on finish/error/cancel/disconnect | Audio.swift, Service.swift |
| Logs | No content fields in logger paths | SpeechDMain, service responses |
| Command injection | Helper executable and arguments come from config as argv, never shell-evaluated | PersistentJSONHelper |
| Path traversal | Upload filename is never used as a path; model/temp paths are expanded config paths | multipart parser/config |
| Queue exhaustion | bounded inference queue rejects excess work | InferenceQueue |
| Model integrity | model IDs/revisions and optional SHA-256 fields are explicit; weights are untracked | config/scripts |
| launchd | installer writes user-owned plist with restrictive directories and no secret overwrite | scripts/install.sh |
| Secret leakage | .gitignore excludes tokens, weights, temp files; final Git scan required |
.gitignore, release checklist |
The FluidAudio Cohere CoreML bundle, S1 helper binary, and model weights are deliberately not committed. Production readiness remains false until the operator installs and verifies the pinned Cohere bundle and S1 helper. The service fails closed rather than silently falling back to an alternate model or cloud inference; the Cohere model card's CC-BY-NC-4.0 terms remain an operator deployment obligation.