security: gitignore + .env.example + boot warns
Problem: the LITELLM master key was committed in plain text across five
files. The .gitignore did not block .env files, leaving the door open
for future accidental commits of real credentials. Admin DELETE on the
voice-bridge cache and the hints engine sessions endpoints fall back to
no-auth when their ADMIN_KEY env is unset, with no warning to operators.
Approach: keep the placeholder default so the LAN-only dev workflow
keeps working without any breaking change, but make every operator
aware of the risk. Added gitignore patterns (.env*, *.secret,
secrets.{yaml,yml,json}, credentials.json, *-secret.*) with a
!.env.example whitelist for the template. Created
tools/macstudio/.env.example documenting the four secrets (master key,
two admin keys, CORS) and recommending ~/.zacus.env as the local store.
Voice-bridge and hints engine now log a boot WARN when the default key
or no admin key is in use. README gained a Security section listing the
LAN-only assumption, the no-auth surfaces, and a four-step
pre-deployment checklist (rotate key, source env, audit Cloudflare
tunnel routes, verify boot logs).
Tradeoffs: chose loud-warning over fail-fast because smoke runs and
tests would otherwise need the real key wired before they can boot at
all; loud warnings surface the issue without breaking dev. Did not
rewrite git history to scrub the placeholder — its value is now
treated as known-public, and any deployment outside the closed LAN
must rotate it before going live.
This commit is contained in:
+14
@@ -96,3 +96,17 @@ hotline_tts/
|
||||
|
||||
# Hardcoded TTS assets (not generated)
|
||||
!tools/macstudio/voice-bridge/service_down.wav
|
||||
|
||||
# Secrets — NEVER commit. Use *.env files locally and pass via env vars.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.secret
|
||||
secrets.yaml
|
||||
secrets.yml
|
||||
secrets.json
|
||||
**/credentials.json
|
||||
**/*-secret.*
|
||||
|
||||
# Tailscale auth-keys / OAuth client files
|
||||
tailscale-*.key
|
||||
|
||||
@@ -1027,6 +1027,17 @@ def create_app(
|
||||
3: int(os.environ.get("HINTS_PENALTY_L3", str(DEFAULT_PENALTY_L3))),
|
||||
}
|
||||
app.state.admin_key = os.environ.get("HINTS_ADMIN_KEY") # None → no auth
|
||||
|
||||
# Security warning: the placeholder LITELLM master key is publicly known
|
||||
# (committed to the public repo as a default). Acceptable for a closed
|
||||
# LAN, never for a public deployment. See tools/macstudio/.env.example.
|
||||
if app.state.litellm_key == DEFAULT_LITELLM_KEY:
|
||||
log.warning("HINTS using default LITELLM_MASTER_KEY — set a real one "
|
||||
"via env (see tools/macstudio/.env.example)")
|
||||
if not app.state.admin_key:
|
||||
log.warning("HINTS_ADMIN_KEY unset — /hints/sessions admin endpoints "
|
||||
"are open. Set HINTS_ADMIN_KEY to gate them.")
|
||||
|
||||
app.state.clock = clock or Clock()
|
||||
app.state.tracker = SessionTracker(
|
||||
cooldown_s=cooldown_s,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Voice-bridge / hints engine secrets — copy to ~/.zacus.env or
|
||||
# tools/macstudio/.env (gitignored) and source before launching.
|
||||
#
|
||||
# IMPORTANT
|
||||
# ---------
|
||||
# - Never commit a real key.
|
||||
# - Rotate the placeholder below for any deployment outside the closed LAN
|
||||
# (Tailscale studio + LAN 192.168.x is acceptable; Cloudflare tunnel /
|
||||
# public internet is NOT — see README.md "Security" section).
|
||||
# - The voice-bridge logs a WARNING at boot if the default placeholder is
|
||||
# still in use, so check the log on first run.
|
||||
|
||||
# LiteLLM master key (required by voice-bridge + hints engine).
|
||||
# Generate locally with: openssl rand -hex 24 | sed 's/^/sk-zacus-/'
|
||||
LITELLM_MASTER_KEY=sk-zacus-CHANGE-ME-IN-LOCAL-ENV
|
||||
|
||||
# Voice-bridge admin key (controls DELETE /tts/cache). Optional but
|
||||
# strongly recommended once non-dev clients reach the bridge.
|
||||
VOICE_BRIDGE_ADMIN_KEY=
|
||||
|
||||
# Hints engine admin key (controls /hints/sessions admin endpoints).
|
||||
HINTS_ADMIN_KEY=
|
||||
|
||||
# Allowed CORS origins for the dashboard / atelier frontends (comma-separated).
|
||||
# Default covers Vite dev + preview ports. Add prod origins explicitly here.
|
||||
CORS_ORIGINS=http://localhost:5174,http://localhost:5173,http://localhost:4173
|
||||
@@ -5,6 +5,34 @@ Deployment configs for the MacStudio M3 Ultra (`ssh studio`,
|
||||
whisper.cpp, F5-TTS, and the LiteLLM proxy. Spec:
|
||||
`docs/superpowers/specs/2026-05-03-tts-stt-llm-macstudio-design.md`.
|
||||
|
||||
## ⚠️ Security model — read first
|
||||
|
||||
The voice-bridge (`:8200`), LiteLLM proxy (`:4000`), MLX-LM servers (`:8500`/
|
||||
`:8501`), whisper (`:8300`) and ollama (`:11434`) are **LAN-only** and bind on
|
||||
`0.0.0.0` for the home/Tailscale network. **Never expose them to the public
|
||||
internet** (Cloudflare tunnel, port-forward, etc.):
|
||||
|
||||
- The committed `LITELLM_MASTER_KEY` (`sk-zacus-local-dev-do-not-share`) is
|
||||
a public placeholder. Anyone with internet access to `:4000` could burn
|
||||
inference compute on it.
|
||||
- `/voice/ws`, `/tts`, `/voice/intent` have **no auth** beyond the rate-limit
|
||||
on `/tts`. Closed LAN is acceptable; public exposure is not.
|
||||
- `DELETE /tts/cache` and `/hints/sessions` admin endpoints fall back to
|
||||
open-by-default if the corresponding `*_ADMIN_KEY` env is unset.
|
||||
|
||||
Before any non-LAN deployment:
|
||||
|
||||
1. Copy `tools/macstudio/.env.example` to `~/.zacus.env` (gitignored) and
|
||||
set `LITELLM_MASTER_KEY`, `VOICE_BRIDGE_ADMIN_KEY`, `HINTS_ADMIN_KEY` to
|
||||
strong random values (`openssl rand -hex 24`).
|
||||
2. Source `~/.zacus.env` before launching the daemons (or load it from the
|
||||
crontab `@reboot` lines via `bash -c 'source ~/.zacus.env && exec ...'`).
|
||||
3. Audit Cloudflare tunnel (`cloudflared`) routes — confirm only
|
||||
`atelier.zacus.saillant.cc` and `dashboard.zacus.saillant.cc` are
|
||||
exposed, never `:8200`/`:4000`/`:8500`/`:8501`/`:8300`.
|
||||
4. Re-run smoke (`make smoke-voice`) with the new key — services boot
|
||||
without `boot_warn_default_litellm_key` log lines.
|
||||
|
||||
## Stack live (post-P1 part1-9)
|
||||
|
||||
Inference and voice stack runs entirely on studio. Tower no longer participates
|
||||
|
||||
@@ -86,7 +86,8 @@ from slowapi.util import get_remote_address
|
||||
# ── config (env-overridable) ────────────────────────────────────────────────
|
||||
WHISPER_URL = os.getenv("WHISPER_URL", "http://localhost:8300")
|
||||
LITELLM_URL = os.getenv("LITELLM_URL", "http://localhost:4000")
|
||||
LITELLM_KEY = os.environ.get("LITELLM_MASTER_KEY", "sk-zacus-local-dev-do-not-share")
|
||||
LITELLM_DEFAULT_KEY = "sk-zacus-local-dev-do-not-share" # placeholder, log warns at boot
|
||||
LITELLM_KEY = os.environ.get("LITELLM_MASTER_KEY", LITELLM_DEFAULT_KEY)
|
||||
PIPER_URL = os.getenv("PIPER_URL", "http://192.168.0.120:8001")
|
||||
F5_TIMEOUT_S = float(os.getenv("F5_TIMEOUT_S", "8.0"))
|
||||
F5_MODEL = os.getenv("F5_MODEL", "lucasnewman/f5-tts-mlx")
|
||||
@@ -401,6 +402,16 @@ async def _boot() -> None:
|
||||
_F5_LOCK = asyncio.Lock()
|
||||
_CACHE_LOCK = asyncio.Lock()
|
||||
|
||||
# Security warning: the placeholder LITELLM master key is publicly
|
||||
# known (committed to the public repo as the default). Acceptable on a
|
||||
# closed LAN, never on a public deployment. See tools/macstudio/.env.example.
|
||||
if LITELLM_KEY == LITELLM_DEFAULT_KEY:
|
||||
_jlog("boot_warn_default_litellm_key",
|
||||
hint="set LITELLM_MASTER_KEY in ~/.zacus.env (see .env.example)")
|
||||
if not ADMIN_KEY:
|
||||
_jlog("boot_warn_no_admin_key",
|
||||
hint="set VOICE_BRIDGE_ADMIN_KEY to protect DELETE /tts/cache")
|
||||
|
||||
# Cache directory + initial index (cheap; just lists .wav stems).
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_CACHE_INDEX = _scan_cache_dir()
|
||||
|
||||
Reference in New Issue
Block a user