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](../../Orchestrator/docs/security.md). 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](../../Orchestrator/docs/security.md#open-gap-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](api.md#concurrency-the-ui-is-not-on-the-same-queue).
- **`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.