Deploy and operate

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:

Output
✓ 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

  1. TLS proxy
  2. Edge
  3. Sign-in
  4. Agent entry
  5. Tool permission
  6. 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
Bridge127.0.0.1, one port per HubThe same machine only
Inbound server127.0.0.1 on HUBZOID_INBOUND_PORTThe 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 pathServesProtected by
/, /auth, /account and the web app's /api/...The web app: chat, sign-in, accounts and connectionsA Hubzoid session. Conversations open only for their owner.
/settings, /portal/...Settings, its management API at /portal/api/ and the report viewerA 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-inThe 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 madeA 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 onHubzoid-issued OAuth tokens only
/v1/chat/completions, /v1/modelsThe OpenAI-compatible APIA personal API key (hzp_...) only
/webhooks/<slug>/<name>Named workflow webhooksA shared secret header, a timestamped HMAC or GitHub's signature on every request
/webhooks/<slug>/<surface>WhatsApp, Telegram and the generic inbound webhookA signature, secret token, shared secret or HMAC on every request
/healthz, /healthz/workflowsHealth checksNothing. /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.

  1. Hubzoid session

    hz_session

    Issued when a person signs in. Stored only as a digest.

    The web app and Settings

    Administration in Settings also requires manage_access.

  2. MCP OAuth token

    hub:access

    Issued by Hubzoid after sign-in and consent.

    The hosted MCP server

    /mcp

    It opens nothing else.

  3. Connected account token

    From a person signing in to a connector. Encrypted with the deployment key.

    That connector

    On that person's turns only.

  4. Bridge key

    BRIDGE_API_KEYS

    Held by Hubzoid's own processes, such as the Slack adapter and the inbound server.

    The bridge

    /v1

    On loopback only. It also opens download links, and is refused on /mcp.

  5. Identity assertion

    X-Hubzoid-Assertion

    Signed with the deployment key, valid for 60 seconds.

    The bridge, on loopback

    Together with a bridge key.

  6. Channel secrets

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

    The inbound server and webhook routes

    /webhooks/<slug>/...

  7. 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 an MCP token never works anywhere else.
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:

  1. The surface gate. The request's surface must be allowed to reach restricted tools. The default list is web, api, mcp and workflow, each of which carries one verified account. Slack, WhatsApp and Telegram are not in it.
  2. 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
SecretWhereProtection
Hub settings and provider keys<hub>/.envIn the .gitignore that hubzoid init writes. Keep it at mode 600.
Credentials for restricted tools<hub>/restricted/.envLoaded 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 keyHUBZOID_SECRET_KEY, or secret.key next to the deployment manifest, or <hub>/.hubzoid/secret.key for a standalone HubCreated 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 secretsAWS_SECRET_NAME (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. hubzoid doctor names each key's layer, never a value.
Download-link signing key<hub>/.hubzoid/artifact_secretCreated with mode 0600
Deployment manifest (several Hubs)<data-dir>/deployment.jsonWritten 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 never contain message content, and Settings never reads anyone's conversations.
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
DestinationWhenWhat is sent
Your model providerEvery agent turn and workflow model callThe instructions, context, conversation and tool results for that call
Your sign-in providerWhen people sign in with Google, Microsoft, OpenID Connect or SAMLThe standard sign-in exchange for that person
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
Alert destinationsOnly when alerts are configuredThe Hub, the kind of alert, the workflow, the run id, a count and a Settings link. Never outputs or exception text.
Slack, WhatsApp, TelegramOnly when those channels are configuredMessages and replies on that platform
Your SMTP serverOnly when a workflow, markdown task or alert sends email with HUBZOID_SMTP_* setAn email to the run's own account, to a list declared in the Hub's workflow settings, or an alert
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
ConnectorsOnly when a connector is configured and offered in an agentTool calls on the turns of people who connected or were granted it
AWS Secrets ManagerOnly with a secret name setA request for that secret at start
Anyone with a public report linkOnly when a person granted Share artifacts publicly creates a linkThat one report, until the link expires or is revoked
Systems your tools reachWhen a tool or MCP server you enabled calls themWhatever 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 in HUBZOID_TRUSTED_PROXIES.
  • Turn sign-in on with HUBZOID_AUTH=true and HUBZOID_PUBLIC_URL, and set a long random BRIDGE_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_DEV outside 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 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).

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.