Security model¶
Secrets¶
- API keys and Hugging Face tokens from the environment stay on the server. They are never used as widget values (Streamlit sends widget state to the browser); the sidebar only says that a server key is configured.
DistillationConfigexcludes secrets fromrepr,str, manifests (public_dict) and logs (safe_dict). Tests scan every file of a run folder and the run log for the key.- The server's key only goes to the server's endpoint. If a visitor picks
another endpoint, only a key they typed themselves is sent. Otherwise a
visitor could point the app at their own URL and receive the server's key.
Operators can lock the endpoint with
BRAINBREW_ALLOW_CUSTOM_ENDPOINTS=0. With login on, it is locked unless the operator sets the variable to1: on a shared server, a user-chosen URL makes the server send requests into its own network. - The CLI reads secrets only from the environment and refuses config files that contain them.
- With login on, the server's Hugging Face token only publishes to the
operator's own namespace (
HF_USERNAME). Publishing anywhere else needs the user's own token, so one user cannot write to another's repos with the server's credentials.
Network exposure¶
- The app listens on
127.0.0.1(.streamlit/config.toml). The Docker examples publish the port on localhost only; incompose.yamlvLLM's port is not published at all. - Before exposing Brainbrew to a network, set
BRAINBREW_REQUIRE_LOGIN=1and configure an OIDC provider under[auth]in.streamlit/secrets.toml. The gate runs on every page, and without the configuration the app refuses to start the UI (fail closed). - Model requests never reach link-local or cloud metadata addresses
(
169.254.169.254,fd00:ec2::254,metadata.google.internaland similar). Endpoint URLs naming one are rejected up front. At connect time (engine/netguard.py) the client resolves the host itself, refuses if any answer is such an address, and connects to the address it checked. That covers host names that resolve there, DNS answers that change between checks (rebinding) and redirects. - Behind an HTTP proxy (
HTTPS_PROXY), the proxy resolves the target, so only the URL check applies. Block the metadata address at the proxy or in the network policy as well. - With login on, each user sees only their own runs. A run id in a URL is not enough to open someone else's documents or dataset.
Uploaded documents¶
- Upload size is limited (
maxUploadSize, 50 MB per file); source text over 100 MB is rejected. - Uploaded file names are never used as paths: documents are parsed from
memory and stored as
runs/<run-id>/source.txt. Run ids are generated and validated against a strict pattern before any path is built.
Data you publish¶
- Hugging Face repos are created private unless Make it public is ticked.
- Dataset cards never include source document names or text; a SHA-256 of the source text identifies it.
- Clean & sanitize redacts personal data before export. Redaction is pattern-based (plus Presidio, if installed) and cannot catch everything: review data from sensitive documents before publishing it.
Supply chain¶
- Dependencies are locked in
uv.lock; therequirements*.txtexports are hash-pinned. CI runs pip-audit, gitleaks and CodeQL; GitHub Actions and base images are pinned to digests and kept current by Dependabot. - CI scans the Docker image with Trivy and fails on any fixable HIGH or CRITICAL vulnerability, and generates CycloneDX SBOMs of the locked dependencies.
- Releases (
vX.Y.Ztags) ship the wheel, sdist and SBOMs, each signed with Sigstore by the release workflow's GitHub identity. Verify a download with:
uvx sigstore verify github \
--cert-identity "https://github.com/Yog-Sotho/Brainbrew/.github/workflows/release.yml@refs/tags/vX.Y.Z" \
brainbrew-X.Y.Z-py3-none-any.whl
.env or
.streamlit/secrets.toml.
Report a vulnerability privately through GitHub's security advisories for this repository rather than in a public issue.