ArchiveAI-TTS / docs /security.md
billingsmoore's picture
Rewrite docs for terser, more austere style (no content changes)
255731f
|
Raw History Blame Contribute Delete
5.02 kB

Security

App-specific notes. The shared controls β€” firewall, container isolation, the OAuth gate, host hardening β€” are documented once, for the whole droplet, in the Orchestrator's security doc. Read that first; this covers what's particular to ArchiveAI-TTS.

Two deployments

DO droplet HuggingFace Space
UI access Google OAuth, 2-address allowlist HF Space visibility settings
API access X-API-Token, OAuth-exempt path Same app check, no Caddy in front
Project storage Local files under TTS_DATA_DIR on the volume HF dataset tts-turn-projects
Glossary storage HF dataset tts-glossary β€” on both Same

The fail-closed API_TOKEN behavior applies to both.

This is the one app that writes to HuggingFace during normal droplet operation

Exception to how the rest of the droplet works. TTS_DATA_DIR=/data is set, so projects and audio go to the attached volume. The pronunciation glossary is stored on a HuggingFace dataset regardless (GarchenArchive/tts-glossary, overridable with GLOSSARY_DATASET_REPO), because it's shared with the HF Space deployment.

Consequences for credentials:

  • This container's HF_TOKEN needs HuggingFace access scoped to write on the dataset GarchenArchive/tts-glossary, and nothing else. Glossary reads fall back to a local cache, so a read-only or absent token degrades gracefully; saves and removals need write.
  • Today it's the account-owner token, which also grants org-admin on GarchenArchive and khyentsevision plus write on BDRC and openpecha, far beyond what this app needs. Full scoping table: Orchestrator security β†’ the HuggingFace token.
  • The same value doubles as the POST /api/drain operator secret. If the token is scoped down, drain's bearer value becomes this app's glossary-write token, meaning anyone who can drain also holds glossary write. Acceptable as-is; split into a separate secret if that risk is unacceptable.

Third-party API keys

This app holds the most valuable non-HF credentials on the droplet, because both cost money per call:

Key Used for Notes
ELEVENLABS_API_KEY Speech synthesis Metered per character. A leak is a direct billing risk.
GEMINI_API_KEY Turn-plan generation Metered per call.

Handling:

  • Both are passed via --env-file, never -e on the command line, so they don't appear in the host's ps output.
  • Callers may override either per job (elevenlabs_api_key, gemini_api_key). Caller-supplied values are sanitised at the job-store layer on write, so they never reach a response body β€” relevant because GET /api/jobs lists every job, not just the caller's. Because it's done on write, any new response builder inherits the protection.
  • Nothing logs either key. Preserve this if adding logging around the ElevenLabs or Gemini calls.

The /api/* OAuth carve-out

This app's Caddy site block uses authed_app_with_api, exempting /api/* from forward_auth. The rule that keeps that safe:

Every route under /api/* must authenticate, and must fail closed. A route that doesn't is published to the internet.

/api/ping is the one intentional exception.

Input validation

  • project_id is validated against ^[A-Za-z0-9_-]+$ on both POST /api/jobs and GET /api/projects/{project_id}. This matters most in this app: its local-file backend builds real filesystem paths from the value (projects/{project_id}/project.json), and at the time the gap was found the container was still running as root. The UI only ever generates IDs via slugify+uuid, so nothing was exploitable through the browser, only through the API.
  • Webhook URLs are re-resolved and IP-pinned immediately before delivery. Synthesis jobs are long-running, so the window between submission-time validation and actual delivery is wide β€” the DNS-rebinding gap this closes.
  • Uploaded caption and turns files are parsed, not executed. turns_file is JSON, parsed with json.loads; there is no pickle or YAML path anywhere in the upload handling.

Known gaps

  • The UI's synthesis handlers bypass the job semaphore. Three separate call sites plus a sweeper thread were out of scope when the API was added. Security-relevant consequence: /api/drain does not stop a UI batch already running, and active_jobs undercounts real work, so "drained" is not a guarantee that nothing is writing. See api.md.
  • HF_TOKEN is over-privileged (needs only tts-glossary write). Fix requires a manual change in the HF web UI.
  • Job state is in-memory. A restart loses queued and running jobs; projects and their audio are durable.
  • Single-tenant trust model. Any holder of API_TOKEN can read every project and job. Fine for one CMS plus the Orchestrator; would need per-caller scoping before wider exposure.