Hubzoid
Deploy and operate

Security model

How a Hubzoid deployment is protected, covering network exposure, sign-in, fail-closed authorization, secrets, MCP keys, data locations and egress.

A Hubzoid deployment has one public port, verifies every person through the chat app's sign-in, and decides every restricted tool call in code before the tool runs. Every decision is recorded, and a decision that cannot be recorded does not run. This page describes each boundary as the software implements it, and what remains yours to operate.

When to use this

Read this before exposing a deployment beyond a private network, when preparing a security review, and when deciding which surfaces, tools and backends a hub may use. For how to report a vulnerability, see the end of this page.

  1. TLS proxy
  2. Edge
  3. Chat app sign-in
  4. Bridge key
  5. Agent entry
  6. Tool permission
  7. Decision log
The checks a chat request passes before a restricted tool runs.

Network exposure

ComponentBinds toReachable from
Edge--host (default 127.0.0.1) on PORT (default 3080)Your reverse proxy, which terminates TLS
Chat app127.0.0.1, internal portThe edge only
Bridge127.0.0.1 on BRIDGE_PORTThe same machine only
Inbound server127.0.0.1 on HUBZOID_INBOUND_PORTThe edge only

The edge forwards a short list of paths to a bridge and everything else to the chat app:

Public pathDestinationProtected by
/ and the chat app's API and websocketsChat appChat app sign-in
/portal/Bridge (the Console and the artifact viewer)Chat app session validated on the server, or a personal API key on /portal/api. Admin screens need manage_access. An artifact opens for the people its owner shared it with, or through an expiring public link under /portal/p/.
/artifacts/... (in a gateway, /b/<slug>/artifacts/...)BridgeA per-hub signed link, or a bridge key that is not dev
/mcp (in a gateway, /b/<slug>/mcp)Bridge, only when MCP_SERVER=trueA per-user API key
/webhooks/<slug>/<surface>Inbound server, only when configuredSignature, shared secret or HMAC on every request

The bridge's /v1 chat API, /uploads and /healthz are never forwarded, because exposing /v1 would hand the model to anyone holding a bridge key and bypass the chat app's sign-in. The edge also:

  • refuses any path with a . or .. segment, which could otherwise reach bridge paths outside the forwarded prefixes
  • drops every client-sent X-Hubzoid-* and X-OpenWebUI-* header before forwarding
  • keeps no sign-in cookie between visitors. Releases before 1.0.1 could pass one visitor's session to another, so rotate WEBUI_SECRET_KEY after upgrading from one.
  • relays websockets to the chat app only

hubzoid doctor warns (exposure.bind) when the public port listens on every interface, and fails auth.chat_signin when it does so with sign-in off.

Authentication

Each credential is accepted in one place and proves one thing. None of them stands in for another.

  1. Chat app session

    WEBUI_SECRET_KEY

    Issued when a person signs in.

    Web chat and the Console

    Console admin screens also require manage_access.

  2. Personal API key

    Minted by a person in the chat app.

    MCP and the management API

    /mcp/portal/api

    It opens no chat app endpoint. The management API also requires manage_access.

  3. Connected account token

    From a person signing in to a per-user MCP server.

    That MCP server

    On that person's turns only.

  4. Bridge key

    BRIDGE_API_KEYS

    Held by the chat app, Slack adapter and inbound server.

    The bridge, on loopback

    /v1

    Never forwarded by the edge, and refused on /mcp.

  5. Channel secrets

    WhatsApp signature, Telegram secret token, webhook secret or HMAC.

    The inbound server

    /webhooks/<slug>/<surface>

  6. Provider and tool credentials

    .envrestricted/.env

    Loaded by the Hub at start.

    Your model provider and systems

    The agent's file tools cannot read restricted/.

A bridge key never works on /mcp, and a personal API key never works in the chat app itself.

People. The chat app (Open WebUI) owns accounts and sessions. Sessions are signed with WEBUI_SECRET_KEY, and Hubzoid refuses to start with sign-in on and no key, or with Open WebUI's public fallback key. See authentication for every sign-in method.

The Console. It accepts a session issued by the chat app, validated on the server, and on its management API (/portal/api) also a person's own API key (Bearer sk-...), checked against the chat app's key table as on /mcp. It ignores identity headers sent by a browser, requires writes made with a session to come from the same origin, and admits only people with manage_access, organization-wide or in an agent. A development bypass exists (HUBZOID_PORTAL_DEV with HUBZOID_PORTAL_DEV_USER) and must never be set on a shared or exposed deployment.

Bridges. The chat app, the Slack adapter and the inbound server call a bridge with a key from BRIDGE_API_KEYS. Identity headers are trusted only on requests that carry that key, and end users never talk to the bridge. The default key dev is public: it is refused on download links, the bridge logs a warning when it is configured, and hubzoid doctor fails auth.bridge_keys when it is in use or no key is set.

MCP clients. A person mints a personal API key in the chat app and presents it as a bearer token on /mcp. The bridge resolves the key read-only against the chat app's database, compares keys in constant time, and refuses expired keys, pending accounts and blocked people. Bridge keys are never accepted on this surface. Inside the chat app, those API keys are restricted to no endpoints at all, so they work only as an MCP credential and on the Console's management API. For a hub on managed grants, the MCP front door is the person's use_hub grant. See connect an assistant.

Connected accounts. With per-user MCP servers, a person signs in to the service once. The chat app stores their token encrypted with WEBUI_SECRET_KEY, and the bridge uses it only on that person's turns, and only on surfaces allowed to reach restricted tools, so a shared Slack channel mention never carries the mentioner's token. On a hub managed in the Console, each app also needs the person's connector_<app> grant. On claude-local, connection tokens and a hub server's headers and env go in a per-turn file readable only by Hubzoid's account, never on a process command line. See tools and connectors.

Inbound channels. WhatsApp requests must carry a valid X-Hub-Signature-256 HMAC of the raw body. Telegram requests must carry the configured secret token. Generic webhooks require the shared secret, compared in constant time, or an HMAC signature. A WhatsApp or Telegram sender who is not in the hub's roster gets a fixed reply and never reaches the model, the tools or any data. See webhooks.

Download links. Links to files the agent made carry an HMAC over the chat and file name, keyed with the hub's own secret in .hubzoid/artifact_secret (created with mode 0600), never with the bridge key. HUBZOID_ARTIFACT_LINK_TTL makes new links expire. Deleting the secret revokes every issued link.

Authorization

Agent entry. For a hub on managed grants, every chat, API and MCP request needs the verified person's use_hub grant. The subject is taken only from the identity the chat app forwards, never from the caller-supplied user field of an API request. Access goes to named people. New access for everyone signed in is refused everywhere, and a carried-over Everyone signed in row works only until an organization administrator removes it. The chat app's agent picker applies the same entry decision to every signed-in person, including chat app administrators. A blocked person is refused on every surface.

Restricted tools. A tool placed in the hub's restricted/ folder requires the permission named after its file. Two checks run, in order:

  1. The surface gate. The request's surface must be allowed to reach restricted tools at all. The default list is owui, web, api, mcp and workflow, all of which carry one verified account: the signed-in person, or the account a workflow run acts as. Slack, WhatsApp and Telegram are not in it. HUBZOID_RESTRICTED_SURFACES replaces the list. slack-dm is safe to add, and slack-channel must never be added, because a shared channel thread is answered under one person's identity.
  2. The permission. A managed grant for that account, or, on a hub that still uses group-based access, membership of a chat app group with the permission's name.

On every runtime, tools the caller may not use are hidden from the agent, and the tool re-checks at call time. That call-time check is the boundary: it holds even if a prompt names the tool directly.

Fail closed. Once a hub is on managed grants, any error reading the access store denies the call rather than falling back to groups. A request with no verified identity is denied restricted tools. A restricted call whose decision cannot be written to the decision log is refused. See restrict tools.

Workflow runs. Scheduled workflows and markdown tasks run as an ordinary account: run_as, else HUBZOID_WORKFLOW_USER, else the owner recorded at setup on a hub managed in the Console. That account's grants decide what the run may do. It is rechecked before every protected call and never swapped for another account. Grants to the older workflow:<name> and workflow:md:<task> service identities are kept, but runs that act as an account do not use them.

Proposed changes. An agent cannot change access. With HUBZOID_MANAGEMENT_TOOLS=true it can propose a change for the signed-in manager, and nothing applies until that manager confirms the exact plan in the Console. A proposal works once and expires.

Account replacement. Hubzoid binds each email to the verified account ID. If a different account later appears with the same email, its direct grants are removed, its agent access is blocked and the event is recorded until an administrator reviews it. The last organization administrator can never be removed, demoted or deleted.

Offboarding. An organization administrator's Delete user removes every grant, then deletes the chat account and its chats. Activity history, usage records and published artifacts are kept, and the email is marked as removed, so a later account with it inherits nothing.

Secrets

SecretWhereProtection
Hub settings and provider keys<hub>/.envListed in the .gitignore that hubzoid init creates. Keep it at mode 600. In a gateway, each hub's .env stays with that hub. Only sign-in and chat app settings missing from the gateway's own environment are taken from it, and the gateway names them at start.
Credentials for restricted tools<hub>/restricted/.envLoaded into the bridge process at start. The agent's file tools (read_file, list_files, grep_data) refuse every path under restricted/, so the model sees only a tool's result, never the credential. Agent child processes, such as the claude CLI and its MCP servers, do not inherit these values, Hubzoid's service secrets or AWS credentials, unless CLAUDE_CODE_USE_BEDROCK needs them.
Optional AWS Secrets Manager secretsAWS_SECRET_NAME with AWS_REGION (deployment), HUBZOID_HUB_SECRET_NAME in <hub>/.env, HUBZOID_RESTRICTED_SECRET_NAME in restricted/.envRead at start with boto3's default credential chain. Within a layer the secret wins over the file. A rotation takes effect on restart. hubzoid doctor names each key's layer, never a value.
Download-link signing key<hub>/.hubzoid/artifact_secretCreated with mode 0600.
Gateway manifest<data-dir>/deployment.jsonWritten with mode 0600, and it can contain database credentials. The pointer file in each hub contains no credentials.
Access snapshots<hub>/.hubzoid/backups/Written with mode 0600 by hubzoid access migrate --apply.

The chat and MCP file tools also refuse dotenv files anywhere in the hub, databases, private runtime folders such as .hubzoid/ and .openwebui-data/, and symlinks into any of them. Open WebUI never receives HUBZOID_* settings.

For an especially sensitive credential, such as SSH access to a production system, run the code that uses it in a separate process under a different operating system user, so the kernel keeps the agent away from it.

Backups leave out .env files, signing keys and database passwords unless you pass --include-secrets. The archive itself is written with owner-only permissions but still contains accounts, password hashes and chats. See backup and restore.

Where data lives

Hubzoid keeps its data on your server, or in PostgreSQL when you configure it. Each model call always leaves the server. Everything else leaves only when you turn it on.

Your server

  • Hubzoid databases

    <hub>/.hubzoid/

    Grants, identities, the decision log, usage, workflow state, run history and published artifacts.

  • Chat app data

    <hub>/.openwebui-data/

    Accounts, sessions, chats and uploads.

  • Inbound channels

    <hub>/.inbound/

    Channel state and stored webhook events.

  • Logs and files

    <hub>/logs/<hub>/output/

    Runtime logs and files the agent made for people.

Or PostgreSQL for the stores you move there.

  • Every agent turn

    Your model provider

    The instructions, context, conversation and tool results for that call.

  • Only when you configure it

    Traces, evals, channels and email

    OpenTelemetry, the OpenAI trace dashboard, Langfuse, Slack, WhatsApp, Telegram, SMTP email, Jev, public artifact links and the systems your tools reach.

Usage records never contain message content, and the Console never reads chat history.
LocationContents
<hub>/.hubzoid/Hubzoid's SQLite databases (grants, identities, decision log, usage, workflow state, WhatsApp and Telegram history), the workflow engine's run history, published artifacts, the signing key
<hub>/.openwebui-data/The chat app's accounts, sessions, chats, uploads and log
<hub>/.inbound/Inbound channel state and stored webhook events
<hub>/logs/, <hub>/output/Runtime logs, and files the agent made for people
<data-dir>/ (gateway)The shared chat app database and operational store, and the manifest
PostgreSQL, when configuredWhichever stores you moved there. See topologies.

Usage records hold tokens, cost, model, surface, user and timing, never message content. Chat history lives in the chat app, and the Console never reads it. The chat app is started with administrator access to other people's chats and chat export turned off (ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT). Set either to true in the hub's .env, or the gateway's environment, to allow it.

What leaves the server

DestinationWhenWhat is sent
Your model providerEvery agent turn and workflow model callThe instructions, context, conversation and tool results for that call
Your OpenTelemetry backendOnly with HUBZOID_OTEL_ENDPOINT setTraces. See observability.
OpenAI's trace dashboardOnly with HUBZOID_OPENAI_TRACING=trueOpenAI Agents SDK traces, including prompts and tool data
Langfuse, for eval resultsOnly with Langfuse keys configuredEval cases, outputs and scores
Slack, WhatsApp, TelegramOnly when those channels are configuredMessages and replies on that platform
Your SMTP serverOnly when a workflow or markdown task sends email with HUBZOID_SMTP_* setAn email to the run's own account
OpenRouter, for JevOnly when a workflow calls hub.call_jev, or a person granted Call Jev uses it in chatThe state and questions for that decision
AWS Secrets ManagerOnly with a secret name setA request for that secret at start
Anyone with a public artifact linkOnly when a person granted Share artifacts publicly creates a linkThat one artifact, until the link expires or is revoked
Systems your tools reachWhen a tool, connector or MCP server you enabled calls themWhatever that tool sends

Hubzoid also starts the chat app with its version update check off, its offline mode on so it does not download models at start, its local embedding model unloaded, and community sharing off.

Your responsibilities

The software enforces the boundaries above. Operating them well is the deployment owner's job:

  • Terminate TLS at a reverse proxy and keep the Hubzoid port reachable only through it.
  • Set a long random BRIDGE_API_KEYS, turn sign-in on with a stable WEBUI_SECRET_KEY, and set WEBUI_URL.
  • If a proxy supplies identity headers, make it strip client-sent copies.
  • Protect .env, restricted/.env, the gateway manifest and backups, and copy backups off the machine.
  • Never set HUBZOID_PORTAL_DEV outside local development.
  • Grant named people the least access that works, and review the Console's Activity page.
  • Offboard with Delete user under People in the Console. The chat app keeps a deleted account's stored connection tokens in its database, so also revoke the person's access in services they connected.
  • Choose model providers, telemetry backends and channels that your data governance allows.
  • Run hubzoid doctor after every change and upgrade to the latest release, which is where fixes go.

Reporting a vulnerability

Report security problems privately through GitHub's private vulnerability reporting on the Hubzoid repository (opens in a new tab): the Security tab, then Report a vulnerability. Please do not open a public issue. Include what you found, how to reproduce it, the output of hubzoid version, and if you can, hubzoid doctor <hub> --json with secrets removed. See SECURITY.md (opens in a new tab).

Next steps

Read this page as Markdown

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.