Security model
How a Hubzoid deployment is protected: one public port, sign-in, fail-closed tool checks, the deployment key, stored data and what leaves the server.
A Hubzoid deployment has one public port, verifies every person through its own sign-in, and decides every controlled tool call in code before the tool runs. Every decision is recorded, a decision that cannot be recorded does not run, and what stays yours to operate is listed at the end of this page.
When to use it
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. To report a vulnerability, see the end of this page.
The smallest real example
hubzoid doctor checks the boundaries of a running Hub without changing anything. On a laptop Hub it reports:
✓ 1 bridge key(s) set (auth.bridge_keys)
· Sign-in is off: local mode, whoever opens the web app is the hub's owner (admin@localhost). Fine on this machine. (auth.chat_signin)
✓ Local mode: the web app stays on 127.0.0.1 (exposure.local_mode)
✓ The public port listens on 127.0.0.1; bridges stay on 127.0.0.1 (exposure.bind)On a server the same checks fail when the bridge key is the public dev key, or when the public port listens on the network with sign-in off. hubzoid run refuses to start in that second case.
How it works
- TLS proxy
- Edge
- Sign-in
- Agent entry
- Tool permission
- Decision log
Network exposure
| Component | Binds to | Reachable from |
|---|---|---|
| Edge | --host (default 127.0.0.1) on PORT (default 3080) | Your reverse proxy, which terminates TLS |
| Bridge | 127.0.0.1, one port per Hub | The same machine only |
| Inbound server | 127.0.0.1 on HUBZOID_INBOUND_PORT | The edge only |
The edge forwards requests to the Hub's bridge (with several Hubs, to the bridge a /b/<slug>/ path names) and inbound channel webhooks to the inbound server.
Every public path, what protects it, and listening with sign-in off
| Public path | Serves | Protected by |
|---|---|---|
/, /auth, /account and the web app's /api/... | The web app: chat, sign-in, accounts and connections | A Hubzoid session. Conversations open only for their owner. |
/settings, /portal/... | Settings, its management API at /portal/api/ and the report viewer | A Hubzoid session. Administration needs manage_access. A report opens for the people its owner shared it with, through a company link after company sign-in, or through an expiring public link under /portal/p/. |
/oauth/<provider>/callback, /oauth/connectors/... | Returns from sign-in providers and connector sign-in | The state and PKCE values of a sign-in that started on this deployment |
/artifacts/... (with several Hubs, /b/<slug>/artifacts/...) | Download links for files the agent made | A per-Hub signed link, which expires after 7 days by default, the signed-in owner of the conversation, or a bridge key that is not dev |
/mcp (with several Hubs, /b/<slug>/mcp) | The hosted MCP server, when it is on | Hubzoid-issued OAuth tokens only |
/v1/chat/completions, /v1/models | The OpenAI-compatible API | A personal API key (hzp_...) only |
/webhooks/<slug>/<name> | Named workflow webhooks | A shared secret header, a timestamped HMAC or GitHub's signature on every request |
/webhooks/<slug>/<surface> | WhatsApp, Telegram and the generic inbound webhook | A signature, secret token, shared secret or HMAC on every request |
/healthz, /healthz/workflows | Health checks | Nothing. /healthz names the Hub, its agent and model. /healthz/workflows names no Hub. |
The rest of the bridge's internal API (/v1, /uploads and /otel) is never forwarded: the edge answers 404. The edge also refuses any path with a . or .. segment, drops every client-sent X-Hubzoid-* identity header and any X-Forwarded-Prefix, and keeps no cookie between visitors.
With sign-in off, hubzoid run refuses to listen on anything but loopback unless HUBZOID_ALLOW_UNAUTHENTICATED_NETWORK=true. hubzoid doctor warns (exposure.bind) when the public port listens on every interface, and reports exposure.local_mode 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.
Hubzoid session
hz_sessionIssued when a person signs in. Stored only as a digest.
Who the person is
The web app and Settings
Administration in Settings also requires
manage_access.MCP OAuth token
hub:accessIssued by Hubzoid after sign-in and consent.
That person, to an assistant
The hosted MCP server
/mcpIt opens nothing else.
Connected account token
From a person signing in to a connector. Encrypted with the deployment key.
The agent may act as them there
That connector
On that person's turns only.
Bridge key
BRIDGE_API_KEYSHeld by Hubzoid's own processes, such as the Slack adapter and the inbound server.
A trusted Hubzoid process
The bridge
/v1On loopback only. It also opens download links, and is refused on
/mcp.Identity assertion
X-Hubzoid-AssertionSigned with the deployment key, valid for 60 seconds.
Which person another Hubzoid process is acting for
The bridge, on loopback
Together with a bridge key.
Channel secrets
WhatsApp signature, Telegram secret token, webhook secret or HMAC.
The delivery is genuine
The inbound server and webhook routes
/webhooks/<slug>/...Provider and tool credentials
.envrestricted/.envLoaded by the Hub at start.
The Hub itself
Your model provider and systems
The agent's file tools cannot read
restricted/.
How people, Settings and bridges are checked
People. Hubzoid owns accounts and sessions. Passwords are hashed with Argon2id. A session cookie holds random bytes, and the database keeps only their digest. Sessions last at most 30 days and end after 7 idle days. Administrators must use two-step sign-in by default, and failed sign-ins are rate limited per address and per email. Changes from a browser must come from the deployment's own address. See authentication.
Settings. It uses the same session, validated on the server. Every signed-in person can open /settings, and people who manage no agent see only the personal sections: Account, Connected accounts, Sources, Memory and My schedules. Administration needs manage_access, organization-wide or in an agent. Settings ignores identity headers sent by a browser, and the management API (/portal/api) accepts no API keys. Changing agent files from the browser is off by default, so Settings shows connectors, channels, settings, approval rules and budgets read-only with where to change them. A development bypass (HUBZOID_PORTAL_DEV) exists and must never be set on a shared deployment.
Bridges. Hubzoid's own processes call a bridge with a key from BRIDGE_API_KEYS. The key alone does not vouch for who is asking: identity headers count only with an assertion signed by the deployment key for this Hub in the last 60 seconds. The default key dev is public, and hubzoid doctor fails auth.bridge_keys when it is in use or no key is set.
How assistants, connected accounts, channels and download links are checked
MCP clients. The hosted MCP server accepts only OAuth credentials that Hubzoid issues after the person signs in and consents (scope hub:access). Access tokens last up to 10 minutes, and refresh tokens and grants up to 30 days. A person revokes a connection at /mcp/oauth/connections. Account status and the person's use_hub grant are checked on every request, and tools they may not use are hidden and refused if called. See connect an assistant.
Connected accounts. With a connector each person signs in to, Hubzoid stores their token encrypted with the deployment key and uses it only on that person's turns, on surfaces allowed to reach controlled tools, and only where they hold the connector's connector_<id> grant. A shared channel mention never carries the mentioner's token. A Shared key connector holds one company key, encrypted, used for the people granted it. See tools and connectors.
Inbound channels. WhatsApp requests must carry a valid X-Hub-Signature-256 HMAC of the raw body, Telegram requests the configured secret token, and webhooks a shared secret or HMAC signature. A WhatsApp or Telegram sender whose number neither Settings nor the Hub's roster knows gets a fixed reply and never reaches the model, the tools or any data. See webhooks.
Download links. Links carry an HMAC over the chat, the file name and the expiry, keyed with the Hub's own secret in .hubzoid/artifact_secret, never with the bridge key. A link expires after 7 days, and HUBZOID_ARTIFACT_LINK_TTL changes that. Deleting the secret revokes every issued link.
Authorization
One access model. Every agent's access is managed in Settings. A person may use an agent only with a grant, and roster groups grant nothing. New access for everyone signed in is refused everywhere. See access administration.
Agent entry. Every chat, API and MCP request needs the verified person's use_hub grant.
Where the caller's identity comes from
The subject is taken only from a verified session, token or signed assertion, never from a field the caller supplies. A bridge call without a verified person is refused, and a blocked person is refused on every surface.
Restricted tools. A tool in the Hub's restricted/ folder requires the permission named after its file. Built-in controlled tools such as remember, call_jev, the workflow tools and the access tools require their own capabilities. Two checks run, in order:
- The surface gate. The request's surface must be allowed to reach restricted tools. The default list is
web,api,mcpandworkflow, each of which carries one verified account. Slack, WhatsApp and Telegram are not in it. - The permission. A grant for that account in that Hub.
Changing which surfaces reach restricted tools
HUBZOID_RESTRICTED_SURFACES replaces the surface list. slack-dm is safe to add, and slack-channel must never be added, because a shared thread is answered under one person's identity.
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. Any error reading the access store denies the call. 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, proposed changes and offboarding
Workflow runs. Scheduled workflows and markdown tasks run as an ordinary account: run_as, else HUBZOID_WORKFLOW_USER, else the owner recorded at setup. That account's grants decide what the run may do, rechecked before every protected call, and never swapped for another account.
Proposed changes. An agent cannot change access. A manager granted Manage access from chat can ask it to propose a change, and nothing applies until that manager confirms the exact plan in Settings. A proposal works once and expires.
Offboarding. Delete user removes every grant and ends the person's sessions, API keys and assistant connections. Activity history, usage records and published artifacts are kept, and the email is marked as removed, so a later account with it inherits nothing. The last organization administrator can never be removed, demoted or deleted.
Secrets
Secrets live in the Hub's .env files, the deployment key and, optionally, AWS Secrets Manager.
Where each secret is kept and how it is protected
| Secret | Where | Protection |
|---|---|---|
| Hub settings and provider keys | <hub>/.env | In the .gitignore that hubzoid init writes. Keep it at mode 600. |
| Credentials for restricted tools | <hub>/restricted/.env | Loaded into the bridge at start. The agent's file tools refuse every path under restricted/, so the model sees only a tool's result. Agent child processes, such as the claude CLI and its MCP servers, do not inherit these values, Hubzoid's service secrets or AWS credentials. |
| The deployment key | HUBZOID_SECRET_KEY, or secret.key next to the deployment manifest, or <hub>/.hubzoid/secret.key for a standalone Hub | Created with mode 0600, reported by fingerprint only. Encrypts connector tokens, Shared keys, client secrets and authenticator secrets, and signs identity assertions. HUBZOID_SECRET_KEY takes several comma-separated keys, so a new key can be added first and the old one kept for reading. Backups leave it out. |
| Optional AWS Secrets Manager secrets | AWS_SECRET_NAME (deployment), HUBZOID_HUB_SECRET_NAME in <hub>/.env, HUBZOID_RESTRICTED_SECRET_NAME in restricted/.env | Read at start with boto3's default credential chain. Within a layer the secret wins over the file. hubzoid doctor names each key's layer, never a value. |
| Download-link signing key | <hub>/.hubzoid/artifact_secret | Created with mode 0600 |
| Deployment manifest (several Hubs) | <data-dir>/deployment.json | Written with mode 0600, because it can contain database credentials |
The chat and MCP file tools also refuse dotenv files anywhere in the Hub, databases, private runtime folders such as .hubzoid/, and symlinks into any of them. These guards cover the built-in tools. Hub-local Python, workflow code and external integrations are trusted code you install, not a sandbox, so they must enforce their own data access. For an especially sensitive credential, such as SSH access to production, run the code that uses it in a separate process under a different operating system user.
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/Accounts, sessions, conversations, grants, the decision log, usage, workflow state and run history.
Files
<hub>/.hubzoid/chats/<hub>/output/Chat uploads, files the agent made and published reports.
Inbound channels
<hub>/.inbound/Channel state and stored webhook events.
Logs
<hub>/logs/Runtime logs.
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, alerts, channels and email
OpenTelemetry, the OpenAI trace dashboard, Langfuse, alert webhooks, Slack, WhatsApp, Telegram, SMTP email, Jev, connectors, public report links and the systems your tools reach.
Usage records, conversation privacy and retention
Usage records hold tokens, cost, model, surface, user and timing, never message content. A conversation opens only for the person who owns it, and Settings has no conversation reader. A shared conversation link opens read-only for signed-in people of the deployment. Webhook event and alert records have no automatic retention yet. See topologies for each store's file.
What leaves the server
Model calls leave the server on every agent turn. Every other destination is used only when you configure it.
Every destination and what is sent
| Destination | When | What is sent |
|---|---|---|
| Your model provider | Every agent turn and workflow model call | The instructions, context, conversation and tool results for that call |
| Your sign-in provider | When people sign in with Google, Microsoft, OpenID Connect or SAML | The standard sign-in exchange for that person |
| Your OpenTelemetry backend | Only with HUBZOID_OTEL_ENDPOINT set | Traces. See observability. |
| OpenAI's trace dashboard | Only with HUBZOID_OPENAI_TRACING=true | OpenAI Agents SDK traces, including prompts and tool data |
| Langfuse, for eval results | Only with Langfuse keys configured | Eval cases, outputs and scores |
| Alert destinations | Only when alerts are configured | The Hub, the kind of alert, the workflow, the run id, a count and a Settings link. Never outputs or exception text. |
| Slack, WhatsApp, Telegram | Only when those channels are configured | Messages and replies on that platform |
| Your SMTP server | Only when a workflow, markdown task or alert sends email with HUBZOID_SMTP_* set | An email to the run's own account, to a list declared in the Hub's workflow settings, or an alert |
| OpenRouter, for Jev | Only when a workflow calls hub.call_jev, or a person granted Call Jev uses it in chat | The state and questions for that decision |
| Connectors | Only when a connector is configured and offered in an agent | Tool calls on the turns of people who connected or were granted it |
| AWS Secrets Manager | Only with a secret name set | A request for that secret at start |
| Anyone with a public report link | Only when a person granted Share artifacts publicly creates a link | That one report, until the link expires or is revoked |
| Systems your tools reach | When a tool or MCP server you enabled calls them | Whatever that tool sends |
Your responsibilities
The software enforces the boundaries above. Operating them well is the deployment owner's job:
- Terminate TLS at a reverse proxy that sets
X-Forwarded-For, and keep the Hubzoid port reachable only through it. List a public CDN inHUBZOID_TRUSTED_PROXIES. - Turn sign-in on with
HUBZOID_AUTH=trueandHUBZOID_PUBLIC_URL, and set a long randomBRIDGE_API_KEYS. - Keep two-step sign-in required for administrators, or require it for everyone if your policy asks.
- Back up the deployment key apart from the data, and protect
.env,restricted/.env, the deployment manifest and backups. Copy backups off the machine. - Redact one-time link tokens and OAuth codes from your proxy's logs, or treat those logs as sensitive.
- Never set
HUBZOID_PORTAL_DEVoutside local development. - Grant named people the least access that works, and review Activity → Audit log in Settings.
- Offboard with Delete user under People. Revoke the person's access in services they connected if that matters, because a service may honour an issued token until it expires.
- Choose model providers, telemetry backends, alert destinations and channels that your data governance allows.
- Run
hubzoid doctorafter 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).

