Please do not open a public GitHub issue for security vulnerabilities.
Open a private security advisory via GitHub (Security → Advisories → New draft security advisory), or contact the maintainers directly. You should get a response within 72 hours.
Indy-Dots is a self-hosted control plane holding real credentials (LLM API keys) and a governance ledger. Its posture:
- Every operator route (
/api/*) requiresAuthorization: Bearer <AUTH_TOKEN>. Only/healthis open (for container healthchecks). - In
ENVIRONMENT=productionthe gateway refuses to boot with a missing, default, or short (<32 char) token. - The web dashboard holds the token in
localStorage(build-timeVITE_AUTH_TOKENor user-entered). Treat the dashboard as bearing the token's authority.
- In production,
indy-gatewayandindy-webbind to127.0.0.1only — all public traffic terminates at Caddy with automatic HTTPS. - UFW is configured during bootstrap, but note: Docker bypasses UFW for published ports. The loopback binds above are the actual protection — do not change them to
0.0.0.0. - CORS is restricted to
ALLOWED_ORIGINS(no wildcard + credentials).
- Red Lines (
.envaccess,id_rsa,rm -rf,DROP TABLE, force-push, pipe-to-shell, sudoers/authorized_keys tampering) are auto-rejected with a ledger entry — including for "read" requests. - Yellow Lines (publishing, emailing, ticket mutations, deletions, deploys) pause as
PENDING_APPROVALwith a dry-run receipt until an operator resolves them via the API/UI. - The ledger (
gate_ledger.jsonl) is append-only and auditable; a single shared policy module (server/app/policy.py) backs both the gateway and the CLI runner so rules cannot drift.
- Provider keys resolve env var > encrypted store > "". Keys saved via
POST /api/settings/keysare Fernet-encrypted (APP_ENCRYPTION_KEY, or a generatedDATA_DIR/.encryption_key) intoDATA_DIR/.provider_keys.json. Both files arechmod 600. Plaintext keys are never logged or echoed —GET /api/settings/keysreturns names +configuredbools only, and a blank save preserves the stored value. - Threat model: anyone with the box (or
APP_ENCRYPTION_KEY) can decrypt the store. This protects backups and casual file reads, not a root compromise.
- Single static bearer token — no per-user identity, rotation, or rate limiting yet.
- The approval API and the orchestrator run in the same process; an RCE in the gateway bypasses the gate.
- The opt-in computer runtime (P4) is not a hardened sandbox for hostile web:
the gateway and approver share one process (see above), the docker profile's
read-only rootfs + dropped caps + resource limits are containment intent, not a
browser-exploit boundary, and the default
fakeprovider is inert. Only point it at untrusted pages from an isolated host you can afford to lose. - The verification gate calls your configured LLM; prompt-injection resistance of that verifier is not guaranteed.
- SSE endpoint is POST-based; ensure proxies in front do not cache it.
- The routine scheduler runs in-process only (no multi-worker/durable queue):
do not scale the gateway past one replica with
ROUTINES_ENABLED=1, or routines will run once per replica. It is OFF by default.
scripts/setup-hetzner.sh <domain>(generates a 64-hex token into.envwithchmod 600, never printed)- Put real model API keys in
.env; keepENVIRONMENT=production docker compose -f docker-compose.prod.yml up -d --build- Confirm
curl http://127.0.0.1:8642/api/profilesreturns 401 from the VPS itself - Confirm
https://<domain>/api/approvalsreturns 401 without a token