| Version | Supported |
|---|---|
| main | ✅ |
This project follows a rolling release model. The main branch is supported.
If you discover a security vulnerability, please report it responsibly:
- Do not open a public issue for security vulnerabilities.
- Email the maintainers or use GitHub Security Advisories if available.
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will acknowledge receipt and aim to respond within a reasonable timeframe.
Secrets (production, since the 2026-07-09 cutover): operator secret values live in a gitignored
out/secrets.env, rendered from the keys-onlyout/secrets.env.exampleand kept separate from derived config (.envstays config-only). Services that need secrets readsecrets.envas a secondenv_file(required: false). Verify withordo preflight --secrets out/secrets.env. Never commitout/secrets.env(nor the operator-realordo.yaml, which carries host paths + tailnet identity). The SOPS + age at-rest model undersecrets/still backs the encrypted material. The.env/runtime/.envnotes below describe the legacy V1 secret flow.
- Open WebUI: The default
WEBUI_AUTH=Falsedisables login. This is intended for local/single-user use only. If you expose the stack to a network (e.g., via port forwarding or LAN access), enable authentication by settingWEBUI_AUTH=Truein the environment. - Dashboard: No per-service auth — it publishes no host port and is reached only through the Caddy edge (oauth2-proxy + Google SSO) at
${CADDY_TAILNET_HOSTNAME}:8444, which is the sole gate; internal callers reach it over the stack network. ops-controller: SetOPS_CONTROLLER_TOKEN(generate withopenssl rand -hex 32) — it stays token-gated and its port is never exposed. - n8n: No built-in auth of its own and publishes no host port — port 5678 is reachable only inside the
ordo-netstack network. The Caddy edge gates the n8n UI behind oauth2-proxy + Google SSO on its own port (${CADDY_TAILNET_HOSTNAME}:8445); the only unauthenticated surface is n8n's public webhook/OAuth base, which stays on:443(/n8n/rest/oauth2-credential/callback,/n8n/webhook/*) so external URLs never change (seeauth/caddy/Caddyfile).
Only Caddy publishes host ports — seven in the port-per-service model (2026-07-24): :443 (front door — landing page, /oauth2 Google callback, /llm/* and /mcp APIs, n8n webhook/OAuth passthroughs, and 302s from every legacy subpath) plus one dedicated SSO-gated port per UI service (:8443 Open WebUI, :8444 Dashboard + /grafana/ embed, :8445 n8n, :8446 ComfyUI, :8447 Hermes, :8448 codebase-memory), all bound to CADDY_BIND. The rendered compose's ${CADDY_BIND:?…} failsafe rejects an empty/unset value (it does not police the address itself); the default guidance is the host's tailnet IP, and an operator may deliberately bind 0.0.0.0 to accept LAN exposure (with NAT/no-port-forward as the internet boundary). One Google sign-in covers all seven ports and the clean per-service tailnet names via a domain-scoped oauth2-proxy cookie (the OAuth callback stays on :443; the SSO gate's rd= carries {host} — portless — so a single wildcard --whitelist-domain=.<domain> + --cookie-domain=.<domain> in the edge plugin covers every port and every sidecar name) — no new Google OAuth redirect URIs are needed. All other services publish no host ports and are reachable only over the internal ordo-net network, gated by the Caddy edge (oauth2-proxy + Google SSO).
- Never commit
out/.envorout/secrets.env. They are gitignored, along with the operator-realordo.yaml. - Use
ordo.example.yamlandout/secrets.env.exampleas templates; copy toordo.yaml/out/secrets.env, fill in values locally, then runordo renderto produceout/.envandout/docker-compose.yml. - API keys (OpenAI, Anthropic, etc.) and tokens should only live in
out/secrets.env, never in the repository. - Never commit
data/— it is gitignored and contains user-specific runtime state (Hermes session data, Discord guild/user IDs, MCP config, etc.). All secrets and setup-specific values belong indata/orout/secrets.env, not in shared code.
All runtime data is stored under BASE_PATH/data/ via bind mounts. Ensure appropriate filesystem permissions and backups. The data/ directory is gitignored and must remain untracked.
-
OPS_CONTROLLER_TOKENset (generate:openssl rand -hex 32) -
out/.env/out/secrets.envnot committed (in.gitignore) - Ops controller port (9000) not exposed to host/network
- Dashboard bound to localhost or Tailscale-only when multi-user
| Threat | Check |
|---|---|
| docker.sock exposure | Only ops-controller and mcp-gateway mount it; dashboard does not |
| Controller compromise | Token in env; no default; never expose port |
| MCP SSRF (browser worker) | Egress blocks for 100.64/10, RFC1918, 169.254.169.254 — ./scripts/ssrf-egress-block.sh --target all |
| Secret exfiltration (general) | Controller-only API keys; dashboard /api/services strips tokens from returned URLs |
| Unauthenticated admin | Dashboard reached only via the Caddy edge (oauth2-proxy + Google SSO); no host port |
- Audit log path writable:
data/ops-controller/
- Reset OPS_CONTROLLER_TOKEN: Generate new token, update
out/secrets.env, then re-rundocker compose -p ordo … upfromout/to restart dashboard + ops-controller - Restore data: Restore
data/from a local backup - Disable MCP tools: Clear
data/mcp/servers.txtor set to a single safe server - Safe mode: Stop
mcp-gatewayandhermes-gateway; usellamacpp+open-webuionly