Skip to main content

Security threat model

This model applies to the supported self-hosted Compose topology in this documentation revision. It covers the four application trust boundaries below. These separate control planes are outside its scope:

  • Browser and identity-provider internals.
  • Host and database administration.
  • Backup custody.
  • The CI/release supply chain.
  • Edge DDoS protection.

The Security Policy, Integration settings, Backup & Restore, and Release Image Lock own those control planes.

Security objectives and protected assets​

Hephaestus must meet these security objectives:

  1. Prevent one workspace from reading or changing another workspace's data.
  2. Keep reusable integration, session, encryption, and model-provider credentials out of untrusted content and sandbox environments.
  3. Contain execution influenced by repositories and models without trusting their instructions or output.
  4. Authenticate public ingress.
  5. Bound public ingress.
  6. Restrict outbound destinations and side effects.
AssetExamples
Reusable credentialsProvider keys, OAuth secrets, integration tokens, webhook secrets, encryption keys
Workspace-confidential dataRepository content, issues, messages, documents, feedback, membership
Untrusted contentWebhook bodies, diffs, files, comments, chat, documents, model output
Operational dataJob and delivery identifiers, metrics, sanitized error categories

System and data flow​

Arrows show permitted logical flows, not network-level reachability. The architecture and network diagram shows the containers and their connections. Network egress lists every outbound destination.

Only application-worker starts containers through the Docker socket and joins each internal sandbox network. That container carries the worker role. The application server runs without the worker role and joins none of those networks. reverse-proxy mounts the socket read-only to discover its routes.

Without internet access, a sandbox's network has only the worker's sandbox gateway as a listener. The worker's application and management listeners bind to other addresses. No other service or external route attaches to that network. A sandbox with internet access uses an ordinary bridge (SB-3). The sandbox isolation probe checks this on the supported install in CI and before each release.

Sandbox inputs arrive and results leave through the worker's sandbox gateway, which also hosts the LLM proxy. No worker disk paths mount into a sandbox. Runtime-role separation reduces reachable surface but is not authorization. See Runtime Roles.

Attacker assumptions​

An attacker can be any of these actors:

  • An unauthenticated internet client that calls all routes exposed by the reverse proxy.
  • A legitimate member who attempts horizontal or vertical privilege escalation.
  • A compromised integration account that sends correctly signed malicious content.
  • A repository contributor who puts prompt injection, malicious source, symlinks, or resource-exhaustion inputs in synchronized artifacts.
  • A malicious or compromised model endpoint that returns crafted output.
  • Code inside a sandbox, with full knowledge of the image and application design.

The model trusts these principals:

  • Docker daemon and host root.
  • Running application processes.
  • PostgreSQL administrator.
  • Instance encryption key.
  • Configured instance admins.

Their compromise is outside the isolation guarantees described here.

Mitigations index​

Each table links threats to enforcement, verification, and residual risk.

Boundary 1: public webhook ingress​

ID and threatEnforced controlEnforcementVerificationResidual risk
WH-1 — forged or unsupported provider eventResolve a known integration kind and verify the provider-signed raw bytes before parsing. Missing routing, verifier, or subject derivation fails closed.IntegrationKindRouting, WebhookIngestPipelineIntegrationKindRoutingTest, WebhookIngestPipelineTest, provider signature-verifier tests for GitHub, GitLab, Slack, and OutlineA valid signature does not make content safe and cannot detect a compromised provider account. Replay resistance differs by provider. GitHub and GitLab rely on bounded deduplication. After that bound expires, consumers can receive a replay again.
WH-2 — oversized input or queue overloadRequire a declared request size within the configured limit and fail startup when the broker cannot accept that size.WebhookPayloadSizeFilter, WebhookPayloadCapacityCheckWebhookPayloadSizeFilterTest, WebhookPayloadCapacityCheckTestVolumetric filtering and rate limiting remain edge responsibilities.
WH-3 — acknowledged loss or duplicate processingEvents selected for durable processing return success only after JetStream accepts them. Per-kind stable keys use provider delivery identifiers where available.WebhookIngestPipelineWebhookIngestPipelineTest, WebhookIngestionCannotFailSilentlyTestProtocol challenges and deliberately filtered events respond without publishing. Deduplication expires with broker retention, so consumers must tolerate redelivery.
  1. Route only /webhooks/** to a separately exposed webhook role.
  2. Operate its capacity and retry signals as Webhook Ingestion Operations describes.

Boundary 2: LLM agent sandbox​

Repository, chat, and model content is untrusted and can influence tool use. A practice review's agent has bash among its tools. The image includes Bash and Git.

  1. Treat a sandbox as capable of running any shell command that untrusted content can cause.

Conversation turns run in one long-lived conversation host per worker. The host serves the turns of many developers and workspaces at the same time. It runs no shell, file, or code tool itself. Node runs the host under its permission model, without child processes. The host has its own internal network, unless its worker runs outside any container (SB-3).

The host's tools are search_conversation, link_result, link_problem, and show_feedback. When the worker can start turn sandboxes, the host also has read, bash and load_context. The worker answers each tool call for that turn only.

Conversation turn sandbox​

read, bash and load_context run in a turn sandbox on the worker, not in the host. Treat each bash command as untrusted code that the model wrote. The turn sandbox container is the security boundary, as it is for the shell of a review.

  • Boundary. One container for each turn attempt, from the agent image, under the same ContainerSecurityPolicy as a review sandbox.
  • Lifetime. The worker removes the container with its files when the attempt ends. It never gives it to another attempt.
  • Contents. The context folder of one developer in one workspace: INDEX.md, the context documents, and repositories that Hephaestus loaded.
  • Write access. The folder is read-only. Commands write only to /tmp, which the container removes with it. On Docker, /tmp is a memory file system. On Kubernetes, it is a volume on the node's disk with a size limit. The folder is on the node's disk too (Kubernetes § Sandbox prerequisites).
  • Network. None. The worker stages each file through the Docker API.
  • Credentials. None: no Git token, no turn token, and no environment from the host or the worker.
  • Limits. 1 CPU, 1 GiB of memory (512 MiB on Kubernetes, Sandbox sizes), 256 processes, and 60 seconds for each command.

A load checks the workspace, the account and the purpose of the turn. It loads only a repository that the workspace monitors on an active connection and that no team hides. An unknown, hidden, or other workspace's repository gets the same error. An erasure, a hidden repository, a change of monitored repositories, or a changed AI choice removes every turn sandbox of that workspace.

These risks remain:

  • Prompt injection. Repository files, Git history, Slack messages and documents can carry instructions. Hephaestus treats them as data, but a model can still follow them. The injected content can change Hephaestus's answer to the developer. It cannot reach another workspace or the network.
  • Shared kernel. The turn sandbox shares the host kernel and Docker daemon with the other containers, as a review sandbox does (SB-1).
  • One user ID. Each command runs as the one unprivileged user of the agent image. A command can stop or read the other processes of its own attempt, but no other attempt.

The Conversation runtime gives the endpoints and the limits.

show_feedback is the only tool that delivers feedback. The worker accepts it only for ready conversation feedback about the developer of the running turn. It refuses it after an erasure of that developer's person data is requested.

Model output remains untrusted. Any operation that stores it or creates an external write must apply that operation's validation and authorization.

ID and threatEnforced controlEnforcementVerificationResidual risk
SB-1 — privilege escalation, host escape, or resource exhaustionThe policy requires non-privileged execution, dropped capabilities, and no-new-privileges. It uses private cgroup/IPC namespaces and seccomp. It applies CPU/memory/PID/file limits. It disables core dumps. It bounds nosuid,nodev tmpfs mounts and uses noexec where compatible. On Kubernetes, every sandbox volume on the node's disk has a size limit. Every container has a disk limit. Thus, no sandbox can fill the node's disk (Kubernetes § Sandbox sizes).ContainerSecurityPolicySandboxArchitectureTest, DockerSandboxBackendTest, DockerSandboxLiveTestThe root filesystem and captured inputs are mounted read-only. Scratch space remains writable. Standard Docker shares the host kernel and daemon. Use gVisor and dedicated, patched worker hosts for hostile multi-tenant workloads. Never mount the Docker socket or secret-bearing host paths into a sandbox. On Kubernetes, the kubelet enforces disk limits by eviction after it measures the use. A sandbox can pass its limit for some seconds. Keep enough free disk on sandbox nodes, or enable file system project quotas.
SB-2 — provider-secret theft or proxy abuseKeep the provider key in the application catalog. The proxy accepts job credentials only while the job runs. Each review or conversation turn attempt gets an attempt token that is valid only for that attempt and its workspace. The token names no conversation or account: the worker takes both from the turn that it claimed. The worker accepts it for model calls only while it holds the attempt's lease. It accepts it for the turn's own endpoints only while the attempt runs on that worker. The token expires shortly after the attempt's deadline. A conversation host's session token reaches only its own runtime: the discovery and workspace download of its initializer, and its command channel. The proxy binds requests to the admitted connection, model, API, and path. It serves requests only on the worker's sandbox gateway connector. It bounds each authenticated principal's request rate and declared request size.ADR 0006, JobTokenAuthenticationFilter, JobTokenAuthenticationProvider, LlmProxyController, LlmProxySecurityConfigJobTokenAuthenticationFilterTest, LlmProxyIntegrationTest, TurnTokenAuthenticationIntegrationTest, LlmProxySecurityConfigTest, SandboxGatewayRateLimitFilterTest, PayloadSizeFilterTest, SandboxEnvBlocklistTestThe worker-side application process holds catalog credentials. A third-party worker is outside the supported topology. ADR 0041 owns that decision.
SB-3 — network exfiltration or cross-execution accessCreate per-execution Docker resources for each review attempt, and one set for each conversation host. A conversation turn sandbox has no network. Conversation turns of different workspaces share their worker's host, and each turn's credential limits its tool calls to its own turn. With internet access disabled, use an internal network with no external route. Configure an unusable DNS resolver.SandboxNetworkManager, ContainerSecurityPolicysandbox isolation probe, SandboxNetworkManagerTest, DockerSandboxLiveTestWithout internet access, the sandbox gateway is the job network's only listener. All its routes must authenticate and authorize sandbox-originated requests. Only a conversation host that a worker outside any container starts uses internetAccess=true. A worker in a container that it cannot identify starts no host. This creates an ordinary bridge, not a hostname allow-list. The isolation probe does not check it. From there a sandbox can reach whatever that bridge and the host's own firewall allow.
SB-4 — persistence after executionThe reconciler reconciles managed containers, networks, and attempt volumes against durable job state. It removes a conversation host and its network and volumes after their recorded creator worker container is gone. It removes ended attempts' folders and repository snapshots from worker storage. The worker removes a conversation turn sandbox when its attempt ends. At startup and every 60 seconds, it also removes turn sandboxes whose attempt no longer runs or whose worker is gone.SandboxReconciler, JobEvidenceFiles, TurnSandboxesDockerSandboxReconciliationTest, JobEvidenceFilesTest, TurnSandboxesTestHost or daemon compromise is outside this control.
SB-5 — hostile repository content during Git preparationEclipse JGit parses repository bytes inside the application process. It is memory-safe and uses no git binary. It never executes a hook. JGit receives the fetch credential in memory for that fetch only. The credential never enters the mirror's configuration. http.followRedirects=false. A snapshot is a fresh repository under the fabric root, fetched from the local mirror with core.symlinks=false. It has no remote and no credentials. Before any write, the snapshot must pass the GIT_MAX_SNAPSHOT_BYTES check. An oversized snapshot fails as a whole. The process excludes submodule entries and unsafe paths and records these limitations. It reads the checkout into an attempt folder that never mounts into a sandbox. The sandbox receives it only through the workspace tar (SB-6).GitRepositoryManager, GitRepositoryLockManager, JobEvidenceFilesGitRepositoryManagerTest, JobEvidenceFilesTestA JGit vulnerability executes with the fetching role's privileges, not inside a sandbox. The server fetches for commit ingestion. The worker fetches for review preparation. GIT_MAX_SNAPSHOT_BYTES bounds only the snapshot, not the fetch. The mirror can exceed that bound under the container's fabric root. The bound refuses the snapshot taken from the mirror. The mirror is shared by every operation on that repository in one container. An in-process lock serializes a fetch against readers.
SB-6 — tampered or oversized workspace and result transportExchange inputs and results only through the sandbox gateway. A per-attempt credential registers one session. A workspace initializer container checks and extracts the staged tar as described in the workspace ABI. The runtime container then mounts /workspace read-only. It has separate writable work/, out/, .pi/ and .sessions/ volumes. These volumes are removed with the attempt. At most one result upload commits per session. The worker validates its archive and SHA-256 Content-Digest in memory. A duplicate 409 proves convergence only when its ETag matches the runner archive. A wrong credential and an unknown session are indistinguishable.SandboxAttemptRunner, DockerSandboxBackend, DockerAttemptWorkspace, SandboxGatewaySessions, SandboxWorkspaceController, SandboxOutputArchive, gateway-init.tsSandboxGatewaySessionsTest, SandboxWorkspaceControllerTest, HephHostTest, SandboxOutputArchiveTestThe initializer runs the agent image under the same policy as the runtime. What makes it trusted is that only gateway-init.ts runs before the runtime container exists. The gateway is plain HTTP on the internal sandbox network. A compromised daemon or host can read the transfer. The staged tar and extracted files coexist during initialization, and the default Docker volume has no per-attempt disk quota.
SB-7 — a Kubernetes cluster that does not hold the sandbox boundaryOn Kubernetes, every sandbox is a Pod in the operator's sandbox namespace for one release, under the Restricted Pod Security Standard. It runs as the agent image's unprivileged user with a read-only root, no capability, no service account token and RuntimeDefault or the shipped Localhost seccomp profile. It runs in a RuntimeClass and in a user namespace of its own when the cluster has them. A deny-all policy covers the namespace. One more policy lets review sandboxes and conversation hosts reach only the workers' gateway port. Turn sandboxes have no network. Each sandbox Pod waits until its policy is in force before its first process starts. Each worker proves these controls with real Pods at its start and every 15 minutes, and starts no sandbox while one fails.KubernetesIsolationProbe, KubernetesPods, worker.yamlKubernetesIsolationProbeTest, KubernetesSandboxRuntimeTest, kubernetes-chart-render.test.ts, ci-kubernetes.ymlWithout a RuntimeClass such as gVisor, a sandbox shares the node kernel. Kubernetes has no noexec for an emptyDir. The probe shows enforcement at the time it runs, not that the network plugin keeps it. Gateway traffic between nodes is plain text unless the network plugin encrypts it.

Boundary 3: outbound egress​

Destination policy limits where a client may connect. Delivery policy separately limits whether Hephaestus may create an external side effect.

ID and threatEnforced controlEnforcementVerificationResidual risk
EG-1 — LLM-provider SSRF or DNS rebindingRequire a public HTTPS provider URL without userinfo, query, or fragment, unless the operator lists the private host. Apply the optional hostname allow-list. Repeat the private-address check on the DNS result used to connect.EgressPolicy, LlmProxyWebClientConfigEgressPolicyTest, LlmProxyWebClientConfigTestAn allowed public service may itself proxy arbitrary destinations or become compromised. Keep the allow-list narrow. A private-hosts range admits every host and port in it. A range that covers the server's own address stops startup. The Kubernetes Service range is not detected. Keep that list to single hosts or narrow ranges. A failed connection test of a private host shows no status or cause.
EG-2 — unsafe integration originReject non-HTTPS origins, embedded credentials, paths, queries, fragments, and literal non-public addresses for user-configured SCM, login-provider, and Outline origins.ServerUrlValidatorServerUrlValidatorTestThis validator performs no DNS lookup. GitLab preflight, Outline API, and Outline identity calls use guarded connect-time resolution. This is not a uniform guarantee for every configurable login-provider client.
EG-3 — unapproved external writeRoute supported delivery adapters through the instance silent-mode guard and reject unregistered adapter shapes in architecture tests.OutboundEgressGuard, @OutboundEgressGatewayOutboundEgressGuardTest, OutboundEgressArchitectureTestSilent mode governs supported writes. It is not a network kill switch and does not suppress reads or authentication traffic.
EG-4: disclosure of stored integration credentialsVersioned credential keys and row/purpose-bound AES-GCM protect stored integration tokens.CredentialBundleConverter. See credential key rotationCredentialBundleConverterTest, EncryptionContextTestApplication or key compromise exposes decryptable values. Rotate upstream credentials after exposure. Key rotation alone cannot recall them.

Application destination controls do not constrain a sandbox granted broad internet access. Enforce any such exception through an independent network egress policy.

Boundary 4: SQL-layer workspace tenancy​

Workspace authorization is the primary boundary. SQL inspection is a defect detector for recognized query shapes, not row-level authorization.

ID and threatEnforced controlEnforcementVerificationResidual risk
TN-1: object-ID substitution or role escalationWorkspace-scoped authorization checks tenant access. View as user permits GET-only reads, requires recent sign-in, and commits a USER_VIEW audit before access.WorkspaceSecurityExpressions, UserViewAccessService. See user viewsCrossTenantIsolationIntegrationTest, UserViewArchitectureTestInstance admins have an audited elevated read path. This does not make them workspace members or permit mutations as the viewed member.
TN-2 — query accidentally omits tenant scopeWith enforcement set to throw, reject recognized SQL shapes that reference dynamically inventoried workspace-scoped tables without a recognized tenancy shape.ADR 0004, TenancyConfiguration, WorkspaceStatementInspectorWorkspaceStatementInspectorTest, WorkspaceScopedTablesTestInserts, accepted primary-key shapes, explicit bypasses, and analysis failures are not rejected. Any SQL containing the word workspace_id is accepted without proving predicate binding. Object authorization and tenant-bound repository methods remain required.
TN-3 — unnecessary cross-workspace bypassRequire an explicit @WorkspaceAgnostic scope for reviewed instance-wide work.@WorkspaceAgnostic, WorkspaceAgnosticAspectTenancyBypassTest, MultiTenancyArchitectureTestThe annotation grants a deliberate bypass. Review its call path and query whenever it is added or changed.

log allows a detected violation to execute and off disables inspection. Neither is a production setting. Database backups and direct SQL access bypass application authorization and need separate access control and encryption.

References and maintenance​

Update this page whenever a depicted flow, attacker assumption, boundary control, enforcement setting, or residual risk changes.