Security practices and configuration for the Heym platform.
Password Policy
All passwords — at registration and when changing your password — must meet these requirements:
| Rule | Requirement |
|---|---|
| Minimum length | 8 characters |
| Uppercase | At least one uppercase letter (A–Z) |
| Lowercase | At least one lowercase letter (a–z) |
| Digit | At least one number (0–9) |
These rules are enforced on both the frontend and the backend (Pydantic validator). A request with a non-compliant password is rejected with a 422 Unprocessable Entity response before any database write occurs.
Example of a valid password: MyWorkflow7!
Session Management
- Access tokens are stored in HttpOnly cookies, not
localStorage. This prevents JavaScript (including XSS payloads) from reading the token. - The refresh token is scoped to the
/api/auth/refreshpath and cannot be sent to other endpoints. - Each
/api/auth/refreshcall rotates the refresh token: the old token is immediately revoked and a new one is issued. Replaying a used refresh token returns401. - Sign out everywhere. Settings → Security → Sign out other sessions ends every other session the account holds, and changing the password does the same. Access tokens issued before that moment are refused at once, even before they expire, and refresh and MCP OAuth tokens are revoked.
- Session lifetimes come from
JWT_ACCESS_TOKEN_EXPIRE_MINUTESandJWT_REFRESH_TOKEN_EXPIRE_DAYS. Because every refresh rotates the token, the refresh lifetime also works as an idle timeout: a session not used for that long has to sign in again.
Two-Factor Authentication
Set HEYM_REQUIRE_MFA=true to require a code from an authenticator app (TOTP, RFC 6238) at every password sign-in. Google Authenticator, Microsoft Authenticator, 1Password and any other TOTP app work.
- Enrolment before access. An account without an authenticator scans a QR code and confirms a code right after its password, before it holds any session. Turning the setting on also ends password sessions that began without a code, so nobody keeps working unenrolled.
- SSO is not affected. Sign-in through SSO never asks for a Heym code; require MFA at your identity provider instead.
- Every password surface. The web login, registration, Heym Work sign-in, and the MCP OAuth consent page all ask for the code.
- Recovery codes. Ten single-use codes are shown once after enrolment. They are stored as SHA-256 digests, so a database copy does not contain them.
- Secrets at rest. The authenticator secret is encrypted with
ENCRYPTION_KEYand re-encrypted by key rotation like credentials. - Replay and guessing. A code is accepted once, within one 30-second step either side of now, and five wrong codes lock that account's sign-in for 15 minutes.
- Lost phone. An administrator resets two-factor sign-in from Settings → Users, and the person sets up a new authenticator at the next sign-in.
Account Deactivation
Administrators deactivate an account from Settings → Users when someone leaves. A deactivated account authenticates nowhere: password, SSO, Heym Work, and the MCP consent page refuse it; its sessions end at once; refresh and MCP OAuth tokens are revoked; its MCP API keys and execution tokens stop working. The automatic triggers of every workflow it owns are paused, the same way Pause triggers pauses one workflow, so nothing keeps running in the name of someone who has left. Reactivation reverses all of it except the revoked sessions. See Settings → Users.
Data Retention
Execution history and LLM traces older than HEYM_DATA_RETENTION_DAYS (default 90) are deleted once a day. A run still waiting on a human review is kept. Set 0 to keep everything. Alert events have their own fixed 90-day window. The weekly execution-history maintenance then returns the freed disk space.
Rate Limiting
The following endpoints are rate-limited per IP:
| Endpoint | Limit | Ban duration |
|---|---|---|
POST /api/auth/login | 10 req / 60 s | 15 min |
POST /api/auth/register | 5 req / 60 s | 10 min |
POST /register (OAuth clients) | 5 req / 60 s | 10 min |
| Portal login | 3 attempts | 24 h |
When REDIS_URL is configured, rate limits are shared across all backend workers. Without it, limits apply per worker process.
Credential Encryption
All API keys, webhook URLs, and bearer tokens stored in the Credentials panel are encrypted at rest with Fernet (AES-128 in CBC mode, authenticated with HMAC-SHA256) before being written to the database. The key is derived from the ENCRYPTION_KEY environment variable. The run.sh and deploy.sh scripts generate a strong value automatically when it is empty; the application refuses to start if ENCRYPTION_KEY is empty or left at a known placeholder.
Before the server starts, Heym also checks ENCRYPTION_KEY against a value stored in the database, and refuses to start with a key the database wasn't encrypted with, instead of failing credential reads later with nothing useful in the error.
Rotating the key
ENCRYPTION_KEY can be replaced without downtime and without re-entering credentials:
- Introduce. Set
ENCRYPTION_KEYS_EXTRAto the new key, keepingENCRYPTION_KEYas it is, and restart every instance. - Promote. Make the new key
ENCRYPTION_KEYand the old oneENCRYPTION_KEYS_EXTRA, and restart every instance. - Re-encrypt. An admin starts it from Settings → Encryption. It runs in the background and pauses itself if any instance still encrypts with another key.
- Retire. Once it shows Completed, remove the old key, unless a backup taken before the rotation may need restoring.
A single container can skip step 1. Credentials no configured key can read, from an earlier key change, are listed in Settings → Encryption; their owners see them as Unreadable, and can enter the value again in place, which keeps workflows pointing at it, or delete them.
Rotation protects backups taken afterwards. A leaked key together with a copy of the database still exposes every credential in that copy, so after a leak, also rotate the credentials at their providers. The full procedure, including the first start after upgrading, is in ENVIRONMENT-VARIABLES.md under "Rotating ENCRYPTION_KEY".
MCP API Key
The MCP API key is used to authenticate external MCP clients connecting to the /api/mcp/sse endpoint. When the SSE connection is established, a short-lived (1-hour) session token is issued and embedded in the message endpoint URL instead of the real API key, preventing the key from appearing in server access logs. See MCP Tab for setup.
OAuth / PKCE
The OAuth 2.0 authorization server (used for MCP clients) supports only the authorization_code grant with PKCE (S256). The consent form uses HMAC-SHA256 CSRF tokens valid for 10 minutes. All values displayed in the consent page are HTML-escaped.
Execution Tokens
Execution tokens are scoped JWTs for calling a workflow's execute endpoint from external systems. Unlike user session tokens, they are:
- Single-workflow scoped — a token is rejected for any other workflow.
- Independently revocable — revoking a token has no effect on the issuing user's session.
- Short or long-lived — choose a TTL from 60 seconds to 10 years.
Tokens are signed with the same application secret (SECRET_KEY) and checked on every request: signature, expiry, wid claim match, and revocation status. See Execution Tokens for setup and API reference.
Content Safety
Use Guardrails on LLM and Agent nodes to block unsafe or policy-violating user messages before they reach the model. Guardrails support nine content categories (violence, hate speech, sexual content, etc.) with configurable sensitivity levels.
Python Tool Sandbox
User-defined Python tools on the Agent node execute untrusted code. By default Heym runs each tool inside a hardened, throwaway Docker container with:
- no network access,
- a read-only root filesystem with a small
tmpfsworking directory, - a non-root user with all Linux capabilities dropped and
no-new-privileges, - strict CPU, memory, and PID limits, and
- no Docker socket mounted, so a tool cannot reach the host Docker daemon.
The execution backend is controlled by the HEYM_PYTHON_TOOL_SANDBOX environment variable:
| Value | Behavior |
|---|---|
auto (default) | Run in the hardened sandbox: a Docker container, or a restricted pod on Kubernetes. If no sandbox backend is available, execution fails closed — it does not silently run untrusted code without OS isolation. |
docker | Same as auto, but never falls back. |
subprocess | Run in a local in-process subprocess guarded by an import allowlist. This is not a security boundary and is intended only for trusted code or local development. run.sh selects it for native dev. |
Set HEYM_PYTHON_TOOL_IMAGE to pin the sandbox image; when empty, Heym auto-detects the running backend image. The in-process restrictions (an import allowlist plus attribute and introspection filtering) are applied as defense in depth, but the container is the real isolation boundary — keep the default auto (or docker) mode for multi-user and production deployments.
Skill Sandbox
Python skills attached to an Agent node also execute untrusted code — a skill can arrive verbatim inside a shared workflow or an everyone-visibility template — so skills run through the same HEYM_PYTHON_TOOL_SANDBOX switch as Python tools and fail closed the same way. The skill container is hardened identically (non-root, all capabilities dropped, no-new-privileges, read-only root filesystem, CPU/memory/PID limits, and no Docker socket), with two deliberate differences because skills legitimately need them:
- Network egress is allowed (skills call APIs and install their own dependencies via
uv), and - a writable workspace is mounted so skills can generate output files and read Heym Drive files.
The workspace is shared with the throwaway sibling container through the same named Docker volume the Codex runner uses (heym-codex-workspaces, mounted at /app/data/codex-workspaces). Only this run's own subdirectory is mounted into the sibling — via a per-run volume-subpath — so one skill never sees another run's or Codex's workspace data. The Docker Compose stack always mounts this volume; the single-container docker run must include -v heym-codex-workspaces:/app/data/codex-workspaces for Python skills as well as Codex. If the volume is missing in auto/docker mode, skill execution fails closed rather than running in the backend. Because per-run subpath mounts require Docker Engine 25.0 or newer, older engines should either upgrade or run trusted skills with HEYM_PYTHON_TOOL_SANDBOX=subprocess.
Only non-secret, portable environment variables (proxy and CA-bundle settings, locale) are forwarded into the container; database URLs, SECRET_KEY/ENCRYPTION_KEY, provider API keys, and OAuth secrets are withheld by an allowlist. Skill file paths are validated to stay inside the workspace, and symlinks planted in the output directory (including the _hitl_request.json sentinel) are never followed. Override the defaults with HEYM_SKILL_IMAGE, HEYM_SKILL_NETWORK, HEYM_SKILL_MEMORY, HEYM_SKILL_CPUS, HEYM_SKILL_PIDS, and HEYM_SKILL_USER if needed.
As with tools, subprocess mode runs the skill in the backend process and is not a security boundary — use it only for trusted single-user or local development (run.sh selects it for native dev).
Code Node Sandbox
The Code node runs arbitrary user Python together with arbitrary third-party dependencies, so it is the strictest of the three sandboxes and the only one with no escape hatch: it reads no sandbox-mode variable, has no subprocess mode, and fails closed when no Docker daemon is reachable. It never falls back to running user code in the backend process.
Execution is one or two throwaway containers, both hardened like the tool and skill sandboxes (non-root, all capabilities dropped, no-new-privileges, read-only root filesystem, CPU/memory/PID limits, and no Docker socket):
- With an empty
requirements.txt, a single container with--network noneruns the code. The runner arrives in the container's stdin payload, so nothing is mounted at all. - With dependencies, a first container installs them with network access into a per-run
.depsdirectory, and a second container mounts that subtree read-only and runs the code. The execution container has no network unless the node'scodeAllowNetworkoption is turned on.
Dependencies are installed with uv, retried with pip, and never cached between runs. When the backend runs in a container, the per-run directory lives on the same heym-codex-workspaces volume the skill sandbox uses, mounted through a per-run volume-subpath so one run never sees another's files; that form needs Docker Engine 25.0 or newer. A native backend (./run.sh) has no such volume and does not need one — its per-run directory is a local temporary path bind-mounted straight into the sibling. Either way the directory is deleted after every execution regardless of outcome.
Only non-secret, portable environment variables are forwarded into the containers — proxy settings, CA bundles, and locale, which dependency installation needs to reach PyPI behind a corporate proxy. Database URLs, SECRET_KEY/ENCRYPTION_KEY, provider API keys, and OAuth secrets are withheld by an allowlist, so a new backend secret is dropped by default rather than leaked until someone updates a denylist. The backend's HOME never overrides the sandbox's own writable path.
The Format button uses the same sandbox. Ruff parses and rewrites the source rather than executing it, but running it on the backend host would hand an untrusted file a process that inherits the backend's whole environment, so it gets its own throwaway --network none container with the same limits and the same allowlisted environment. It fails closed without Docker rather than formatting on the host.
Sandbox limits are constants rather than environment variables — 512 MB of memory, one CPU, 256 processes, a 120-second install timeout, and a 60-second execution timeout. The image is resolved from the existing HEYM_PYTHON_TOOL_IMAGE / HEYM_CODEX_DOCKER_IMAGE chain; the node introduces no configuration of its own.
Related
- Running & Deployment – Configure
SECRET_KEY,ENCRYPTION_KEY,ALLOW_REGISTER, andHEYM_PYTHON_TOOL_SANDBOXat startup - Code Node – Sandboxed Python execution with its own dependencies
- Agent Node – Custom Python tools that run in the sandbox
- Execution Tokens – Scoped JWTs for calling workflows from external systems
- Guardrails – Block unsafe content in LLM and Agent nodes
- Settings – Change your password
- Credentials Tab – Manage API keys
- MCP Tab – MCP API key and OAuth clients
- Portal – Public chat portal access control