Security Architecture
Scion is designed with a multi-layered security model to ensure the integrity and confidentiality of agent operations, user data, and system communications. This document outlines the authentication mechanisms, transport security protocols, and future authorization plans for the Scion platform.
1. Authentication Model
Section titled “1. Authentication Model”Scion operates in multiple contexts, each with specific security requirements. Authentication is managed centrally by the Scion Hub, which resolves identities for users, agents, and infrastructure components.
1.1 Authentication Contexts
Section titled “1.1 Authentication Contexts”| Context | Client Type | Auth Method | Token Storage |
|---|---|---|---|
| Web Dashboard | Browser | OAuth 2.0 + Session Cookie | HTTP-only cookie |
| CLI (Hub Commands) | Terminal | OAuth 2.0 + Device Flow | ~/.scion/credentials.json |
| Agent (sciontool) | Container | Hub-issued JWT | Env Var (SCION_HUB_TOKEN) |
| Runtime Broker | Compute Node | HMAC Signature | ~/.scion/hub-credentials/<name>.json |
| Development | Loopback Hub only | Developer Token (Bearer) | ~/.scion/dev-token |
1.2 User Authentication (OAuth 2.0)
Section titled “1.2 User Authentication (OAuth 2.0)”For both Web and CLI access, Scion relies on standard OAuth 2.0 providers (Google and GitHub).
- Web Flow: Standard Authorization Code flow. The embedded Go web server handles the callback and exchanges the provider token for a session-bound Hub access token.
- CLI Flow: Uses a localhost callback server (defaulting to port
18271). The CLI opens the user’s browser for authentication and receives the authorization code via the local server. - PKCE: The CLI uses Proof Key for Code Exchange (PKCE) to prevent authorization code injection attacks.
- Per-User Session Revocation: Each user carries a
session_generationcounter. Administrators can increment this counter via the API (POST /api/v1/users/:id/revoke-sessions) or the Web Dashboard, immediately invalidating all of the user’s active cookie-based sessions. The Hub middleware checks the counter on every request and forces re-authentication on mismatch. See Authentication & Identity — Session Revocation.
1.3 Agent Authentication (sciontool)
Section titled “1.3 Agent Authentication (sciontool)”Agents running inside containers must report status back to the Hub without possessing user-level credentials.
-
Hub-Issued JWT: During provisioning, the Hub generates a short-lived JWT scoped specifically to that agent instance.
-
Claims: The token includes the
agent_id(sub),project_id,scopes, and therun_idof the agent run it was issued for. Withserver.auth.agent_run_scopeset toobserve, the Hub comparesrun_idwith the agent’s current run and logs mismatches, such as a token from an earlier run; it does not refuse requests on this basis yet. The agent’s current run changes only when the Hub dispatches a create, start or restart, when the Runtime Broker reports the run it launched, when a failed dispatch or move is reverted, or when a Runtime Broker heartbeat reports the agent’s container carrying another run the Hub issued a token for while no container holds the recorded run (logged asagent_run_adopted_from_heartbeat). -
Role-Based Scopes: Instead of raw template scopes (which are deprecated), an agent’s scopes are governed by its assigned Tiered Agent Role (
none,readonly,baseline, orfull). An empty or unspecified agent role is securely enforced to resolve to the least-privilege role (AgentRoleNone) across all authorization paths. Scheduled dispatch children automatically persist this explicit role, and dispatches lacking a creator are refused.project:read(Readonly): Allows reading project state (agents, templates, etc.).project:artifact:read(Readonly): Allows the agent to use the artifact service to read artifacts (artifact.read) when thehub.artifactsexperiment is on; which artifacts it can read is decided by its project and by artifact grants. Artifact access uses dedicated agent scopes. Agents created from a credential issued before artifacts existed, and some agents created before then, do not carry it; refreshing such an agent’s token is not enough. A Hub super-admin can bring it up to date withscion reset-auth <agent> --reissue-scopes(see Reset Auth and Scope Re-issue); an agent created from an access token issued before artifacts existed must be recreated from a current credential.project:artifact:write(Baseline): Allows the agent to use the artifact service to publish artifacts (artifact.create) when thehub.artifactsexperiment is on; where it can publish is decided by its project. Readonly agents do not get it. Like the read scope, some agents created before artifacts existed do not carry it; re-issue their scopes withscion reset-auth <agent> --reissue-scopes, or recreate the agent from a current credential.agent:status:update,agent:token:refresh,project:agent:notify,agent:port:forward(Baseline): Standard operational scopes allowing the agent to report progress, refresh its token, and hold port tunnels.project:agent:create,project:agent:sa_assign,project:agent:lifecycle,project:secret:read,project:template:write(Full): Complete programmatic control allowing the agent to spawn sub-agents, assign GCP service accounts to agents, manage their phases, retrieve project secrets dynamically, and create or update templates within the project.
-
Transmission: The token is injected into the container via the
SCION_HUB_TOKENenvironment variable and is used bysciontoolfor all API calls.
1.4 Runtime Broker Authentication (HMAC)
Section titled “1.4 Runtime Broker Authentication (HMAC)”Runtime Brokers represent high-trust infrastructure. They use HMAC-based request signing for bidirectional authentication with the Hub.
- Shared Secret: Established during initial registration via a short-lived, single-use
joinToken. The token can be redeemed on the same machine (scion runtime-broker register) or created on one machine and redeemed on another (scion hub brokers join-token create, thenscion runtime-broker join), so the broker host needs no Hub user credential. - Signing & Verification: Every request includes headers for
X-Scion-Broker-ID,X-Scion-Timestamp,X-Scion-Nonce, andX-Scion-Signature. The Hub strictly verifies that the authenticated caller identity matches any target broker paths to prevent cross-tenant escalation. - Replay Protection: Nonce-based tracking and timestamp validation (5-minute clock skew tolerance) prevent replay attacks.
- NAT Traversal: Brokers establish a persistent WebSocket control channel. The initial upgrade request is HMAC-authenticated, establishing a trusted session for subsequent commands.
1.5 Broker Registration and Association Credentials
Section titled “1.5 Broker Registration and Association Credentials”User-credentialed Runtime Broker operations admit a fixed set of credential kinds. A Runtime Broker HMAC request that names a user on whose behalf it acts is not a user credential for registering, re-registering, rotating, associating a Runtime Broker with a project or removing an association; Runtime Broker HMAC on its own is admitted only for the Runtime Broker’s self-maintenance (heartbeat, control channel, rotating its own secret, reading and writing its own env vars and secrets, and reporting launches and message-delivery failures).
| Operation | Endpoint | Admitted credentials | Additional checks |
|---|---|---|---|
| Register a new broker | POST /api/v1/brokers |
Interactive session, dev credential, hub-boundary UAT with broker:create |
The user must hold broker.create (hub members). The caller’s user becomes the owner. Turning on auto-provide also requires broker.auto_provide (super-admins). |
| Re-register (re-mint the join token) | POST /api/v1/brokers matching an existing broker |
Same as registration | The user must also be the broker’s owner, or a super-admin with an interactive session or dev credential. |
| Rotate the HMAC secret | POST /api/v1/brokers/{id}/rotate-secret |
Broker HMAC for its own ID; interactive session or dev credential | A user must be the broker’s owner or a super-admin. No UAT is admitted. |
| Associate with a project | POST /api/v1/projects/{id}/providers, brokerId in POST /api/v1/projects/register, explicit broker on agent create |
Interactive session, dev credential, or UAT for the project side | project.update on the project and broker.update on the broker (owner or super-admin). No UAT selector carries broker.update. |
| Remove an association | DELETE /api/v1/projects/{id}/providers/{brokerId} |
Interactive session, dev credential, UAT | project.update on the project, or broker.update on that broker. |
| Use an associated broker | Agent creation | As for agent creation | Allowed for auto-provide brokers, for brokers associated with the project with the owner’s consent, and for holders of broker.dispatch on the broker. |
| Read env vars and secret metadata | Runtime Broker env and secret routes (read) | Runtime Broker HMAC for its own ID; interactive session, dev credential, hub-boundary UAT with broker:read |
broker.read on the Runtime Broker, plus the owner relationship or broker.update. Sensitive values are masked. |
| Write env vars or secrets, update, deregister | Runtime Broker env and secret routes (write), Runtime Broker update and deregistration | Runtime Broker HMAC for its own env vars and secrets (not update or deregistration); interactive session or dev credential | broker.update on the Runtime Broker (broker.delete to deregister). No UAT is admitted. |
| Send a heartbeat as a user | Runtime Broker heartbeat | Runtime Broker HMAC for its own ID; interactive session or dev credential | broker.update on the Runtime Broker from a role binding; the owner relationship does not apply. |
| Pull or remove an image on a named Runtime Broker | Harness-config image operations | As for the operation | A user holding broker.dispatch on that Runtime Broker is admitted on a session or dev credential. A user access token is always refused, even on an auto-provide Runtime Broker, because no token scope carries broker.dispatch. Without broker.dispatch, a session or dev credential is admitted only on an auto-provide Runtime Broker; association with the project does not help. |
Registration never associates a broker with a project, and a project’s default runtime broker must already be one of its providers. Register, re-register, rotate, link and unlink audit events record the credential kind and credential ID (a token ID or broker ID; never a secret) alongside the user.
2. Transport Security
Section titled “2. Transport Security”2.1 TLS and HTTPS Enforcement
Section titled “2.1 TLS and HTTPS Enforcement”In production mode, Scion mandates the use of TLS for all network traffic.
- HTTPS Enforcement: The Hub server rejects non-HTTPS requests (unless configured for local development or behind a trusted TLS-terminating proxy).
- Security Headers: Standard headers such as
Strict-Transport-Security(HSTS),X-Frame-Options, andContent-Security-Policyare enforced. - mTLS (Future): Support for Mutual TLS between Hub and Runtime Brokers is planned for high-security environments.
2.2 WebSocket Security
Section titled “2.2 WebSocket Security”- CLI/Agents: Use standard
Authorizationheaders. - Browser/Web: Since browser WebSocket APIs cannot set custom headers, Scion uses a Ticket-Based Authentication system. The client requests a short-lived, single-use ticket via a POST request (authenticated by cookie) and provides it in the WebSocket query string (
?ticket=...).
2.3 Hub Credential Origin Binding
Section titled “2.3 Hub Credential Origin Binding”apiclient and transportauth attach Hub credentials (Authorization, X-Scion-* headers, and transport credentials such as Cloud Run ID tokens) only to requests matching the configured Hub origin (scheme, lowercase host, and effective port), and strip Authorization, Proxy-Authorization, X-Serverless-Authorization and X-Scion-* headers when an HTTP redirect crosses to any other origin.
3. Authorization and Access Control
Section titled “3. Authorization and Access Control”3.1 Domain Authorization
Section titled “3.1 Domain Authorization”Scion supports restricting authentication to specific email domains via the SCION_AUTHORIZED_DOMAINS configuration. This provides a first-line defense, ensuring only authorized organization members can access the Hub.
3.2 Permissions and Policy Model
Section titled “3.2 Permissions and Policy Model”Scion implements a robust, hierarchical RBAC (Role-Based Access Control) and policy system. For a detailed technical specification of the policy language and agent identity claims, see the Policy & Permissions Reference and Permissions & Policy Guide.
- Principal-Based: Permissions are granted to Users and Groups.
- Hierarchical Groups: Groups can contain other groups, allowing for complex team structures.
- Resource Scopes: Policies are attached to scopes (Hub, Project, or specific Resource) and follow a containment hierarchy.
- Override Model: Lower-level policies (e.g., at the Agent level) override higher-level ones (e.g., at the Project level), allowing for granular delegation of authority.
- Actions: Standardized CRUD actions (
create,read,update,delete,list) plus resource-specific actions (start,stop,attach,message). - Tiered Agent Authorization: Agents are assigned tiered roles (
none,readonly,baseline,full) that restrict their JWT scopes through project and parent-agent creation ceilings plus live delegation checks. Each delegation edge also freezes the creating credential’s permission ceiling and authority provenance at creation; the delegation walk applies every hop’s frozen ceiling, and edges without recorded provenance are denied for sensitive-material permissions (see Permissions). - Delegation Provenance: Delegation edges record the credential that authorized them and a frozen effect ceiling. Edges written before this was recorded are handled by a bounded adoption migration; see Delegation Provenance Adoption.
3.3 GCP Service Account Assignment Gates
Section titled “3.3 GCP Service Account Assignment Gates”To prevent lateral privilege escalation, Scion implements a strict two-layer delegation check when binding a GCP service account to any agent:
- Layer 1: Scion Hub Policy: The Hub’s policy engine checks if the caller holds the
ActionAssignpermission on the target GCP service account resource within Scion. - Layer 2: GCP IAM (
actAs): Whengcp_iam_check_modeis set to"enforce", the Hub performs an out-of-band call via Google’s Policy Troubleshooter v3 API to verify that the caller’s GCP principal possessesiam.serviceAccounts.actAspermission on the target service account.
Key security attributes of the GCP IAM check include:
- Fail-Closed Design: If the Policy Troubleshooter returns an indeterminate result (due to conditional bindings, or due to insufficient Hub reviewer permissions when configured to fail-closed), the check fails closed and assignment is blocked. There is no fallback to insecure alternatives like getIamPolicy.
- Asymmetric Caching: Approved assignments are cached for 60 seconds, and denials are cached for 10 seconds. Indeterminate or error states are never cached.
- Auditing: Every service account assignment check—both allowed and denied—generates a permanent audit log entry detailing the principal, target service account, and Policy Troubleshooter decision.
- Hub-Scoped SAs: Real hub-scoped service accounts can be assigned across projects. However, to prevent privilege bypasses, hub-scoped assignments are immediately rejected if
gcp_iam_check_modeis not set to"enforce".
3.4 Fail-Closed API Authorization and Resource Isolation
Section titled “3.4 Fail-Closed API Authorization and Resource Isolation”To guarantee that no API endpoints or handlers can be accessed without explicit authorization, the Scion Hub enforces a strict fail-closed authorization design:
- Explicit Fail-Closed Handlers (
s.authorize()): All API handlers route through fail-closed authorization checks (s.authorize()). This eliminates legacy fail-open bypass vectors (such as functions relying onGetUserIdentityFromContextwhich could returnnilfor agent or broker callers and silently bypass authorization). Under the fail-closed model, any context lacking a valid user identity, agent token, or broker credentials is automatically denied. - No Fallback When Authorization Is Unavailable: The Hub refuses to start if the route metadata for session-only routes is incomplete. A Hub-admin route that declares a permission is evaluated only through the authorization service; if none is configured, the route answers
500“authorization unavailable” instead of falling back to a plain admin-role check. - Fail-Closed Dispatch Access: The
checkBrokerDispatchAccessguard is strictly fail-closed, ensuring that no agent execution can be triggered on a runtime broker unless dispatch permissions have been verified. - Role Boundary Enforcement (
addGroupMember): Non-user callers (such as automated agents or system services) are strictly capped at the plainmemberrole when executingaddGroupMemberoperations, preventing elevation of privileges across organizational boundaries. - Strict Isolation Ordering (404-before-403): To prevent unauthorized users or agents from discovering the existence of sensitive resources via API probe responses, Scion enforces strict resource isolation ordering. If a caller requests a resource they are not authorized to view, the Hub performs resource existence checks and tenant bounds validation first. This ensures the Hub responds with a
404 Not Foundrather than a403 Forbiddenif the resource does not exist or belongs to another tenant/project, preventing side-channel resource enumeration. - Dispatcher-Level Route Authorization: Route families that share a resource are authorized once, in a single dispatcher, before any handler runs:
- Project workspace routes (files, archive, and pull) check project access first. Any non-read method (for example
PUT,POST,DELETE) requires update access on the project. - Template file routes check access on the specific template before any read or write, and validate file paths with the same rules as the workspace file handlers.
- Agent status updates: an agent can update only its own status. Any non-agent caller needs update access on the agent.
- Harness config routes grant Runtime Brokers read-only access.
- Project GitHub settings require read access on the project for
GET, and update access for any other method.
- Project workspace routes (files, archive, and pull) check project access first. Any non-read method (for example
- Scope Boundary on Resource Reads: User- and project-scoped skills, templates, and harness configs can be read only by their owner, members of the owning project, and Hub admins. Hub-wide member and viewer grants apply only to hub- and global-scoped records; they do not open up another user’s or project’s resources. The same scope check applies when a template is resolved at agent creation and when a template is cloned. There is no separate
visibilitysetting on these resources, so access depends only on scope and grants. - Project Agent Routes: Project-scoped agent list, get, and update, and the resume and restart path, authorize each caller individually. Unless the caller holds
agent.attachon the agent (an agent delegated credential never qualifies), agent responses leave out the environment, MCP server env and headers, service env and telemetry export headers in the agent’s applied config, create inputs,pendingConfig, andeffectiveNextRun(the entries themselves stay listed);GITHUB_TOKENis never returned. - Chat Search Visibility: Chat search returns DM threads only to their participants.
- Project Authority From Role Bindings Only: A project’s
ownerIdfield grants no access. Project authority, including project stop-all, comes only from project-scoped role bindings, so a removed creator whose only claim isownerIdis denied. See Permissions & Policy. - Sandboxed Port-Forward Responses: Responses proxied from an agent’s exposed port always carry the Hub’s sandbox
Content-Security-PolicyandX-Content-Type-Options: nosniff, and the agent’sSet-Cookie, CSP, and CORS headers are never relayed onto the Hub origin. The agent’sX-Frame-Options,X-XSS-Protection,Referrer-PolicyandPermissions-Policyheaders are not relayed either; when the web frontend serves the Hub, its own values for these page headers apply. See Port Forwarding. - Project-Scoped Agent Deletion: When a Runtime Broker deletes an agent, it resolves the agent within the requested project only, on every runtime. It never matches by bare slug across projects, so it cannot remove a same-slug agent’s container, VM, or files in another project. The broker returns
404when nothing matches and refuses the delete if the match is ambiguous. - Agent Name Path Validation: Wherever an agent name resolves to on-disk agent state, it must be a single, clean path element. Empty names,
.,.., and names containing/,\, or NUL are rejected, so a name cannot escape the agents directory. Broker create and start requests that fail this check return a validation error. Broker dispatch and scheduled-event lookups address agents by slug. - Sanitized Broker Failure Reasons: Before a message failure reason reported by a Runtime Broker is stored or echoed into the sending agent’s terminal, the Hub strips control characters and invalid UTF-8 and truncates it to 512 bytes.
- Sanitized Failure Logs: When the Runtime Broker logs why an agent create, asynchronous launch, start or restart failed, or why a provision failed because its template or harness config was not found, it blanks
KEY=VALUEvalues, the values of credential-named keys and credential-shaped strings (authorization credentials, JWTs, URL userinfo, PEM blocks, long encoded runs), and cuts the sanitized text at 1 KiB, adding a marker with its full length. A failure caused by a required skill is the exception: the failure record carries only the skill’s user-visible text and a correlation ID, and separate records carry the skill failure text with its URLs redacted, or with the resolver or install message withheld when the skill reference itself had to be redacted. The error returned to the client is unchanged. - Artifact Stewards: On an artifact owned by an agent, the owners of its home project (the
artifact.stewardpermission, carried only by the built-in project-owner role and super-admins) and the agent’s delegating user (the root of its live, recorded delegation chain, who must also read in the home project through a role) may share it. The authority is computed on every request from the artifact’s current home and owner, never from grants on the artifact, and it is non-amplifying: a steward may give at most write, never an admin grant, and may not move the artifact (403 steward_action_not_allowed), so no stored grant it creates can carry more power than it holds. Artifacts owned by users have no stewards, and agents are never stewards (artifact.stewardhas no agent token scope). A user access token must carryartifact:steward(owner clause) orartifact:read(delegator clause) besidesartifact:manage, and reach the home project; older tokens fail closed. Each steward write is checked again, under the artifact’s lock, against the home and owner it was decided on, so a share that races a move or an ownership transfer answers404and commits nothing. Grant creation, permission changes and share-link creation are recorded in the artifact’s event table with the actor and the steward clause, never the link token. Grants a steward created outlive the steward’s role, like any grant; any current owner, admin grantee or steward can remove them. - Artifact Review Access: On an artifact owned by an agent, callers who are not its stewards, such as an admin of its home project who is not an owner (
project.managethere) and the agent’s delegating user when they hold no role in the home project, may give a user review access without being able to manage the artifact. Review access is a review grant: it lets the user publish versions of kind review and nothing else, and it gives no read access of its own. It is given only to a user who can already read the artifact through the home project, or a grant to one of their projects, and it never replaces a grant the user already holds. The hub then asks again on every review request whether the user can still read it that way. When that access ends, review access ends with it, and the artifact answers404like a missing one. A refused grant request gets the same403with one fixed message, with one exception: a caller who holds this authority and asks for a review grant to a user gets403with codereview_subject_cannot_readwhen that user cannot read the artifact, and403with codereview_replaces_accesswhen the user already holds a read, write or admin grant. Review access given before the review level existed was a write grant, and those grants were not changed. - Artifact Deletion: Deleting an artifact (
DELETE /api/v1/artifacts/{id}, behind thehub.artifactsexperiment) is allowed only for users: its owner, a user with an admin grant on it, a steward of an artifact owned by an agent, or an admin of its current home project (project.managethere: the project owners and admins of the artifact’s home project, and super-admins; hub-admins do not hold it). The caller’s credential must also allowartifact.managein that project, so a user access token stays within its boundary and permission ceiling. Agents can never delete artifacts. The read check runs first: a caller that cannot read the artifact gets the same404as for a missing one, and a reader without the right gets403with one fixed message, so the answer reveals neither existence nor the reason. A delete is a soft delete that also removes every grant and share link in the same transaction; it, and every expiry change, move, grant creation, change and removal, and share-link creation and revoke, is recorded in the artifact’s own append-only event table with the actor and its authority. A periodic owner check marks artifacts whose owning agent or user is gone (ownerDeleted); the mark gives nobody any access. - Regression Checks in CI: To prevent future authorization regressions, an automated
authz-guardcheck is wired into the CI pipeline (via a dedicated Makefile target and GitHub Actions step) that statically analyzes and validates that all API handlers are protected by appropriate authorization helpers.
3.5 Project File Access Containment
Section titled “3.5 Project File Access Containment”The Hub’s project file handlers serve project workspaces and shared directories. These cover list, download, archive, upload, inline write, and delete. Each handler is confined to the directory it serves. The contents of these directories are agent-writable by design: a workspace is a git checkout, and a shared directory is mounted read-write into every agent in the project. A symlink planted inside one must therefore not expose files elsewhere on the host.
- Kernel-enforced containment: File operations go through Go’s
os.Root, which checks containment at every path component. A path that resolves outside the served directory, including through a symlink stored inside it, is refused with400 File not accessibleand a warning is logged. A symlink whose target is outside returns the same response whether or not the target exists, so it cannot be used to probe the host. - In-tree symlinks: A symlink that resolves to another location inside the same directory is followed normally.
- Symlinked base directories: If the served directory itself is a symlink, the request is refused.
- Archives: Directory archive downloads skip symlinks entirely.
- Deletes: Deleting a symlink removes the link only, never its target.
- No implicit creation: Read and delete requests on a missing workspace or shared directory no longer create it. Only uploads and writes do.
- Attachment ingest and staging: Attachments are resolved through an
os.Rootanchored on the project scratchpad, so a symlink at any intermediate directory component cannot redirect them. A shared directory that is itself a symlink is refused. - Chat attachments as artifacts: With the
hub.attachment_artifactsexperiment on, a file uploaded in web chat is stored in the artifact service as an attachment of the message it is sent in. Whether a caller may read it is decided on every request by the artifact service’s single read rule: the caller’s credential must allow reading messages, and the caller must be able to read the message now (a participant of its conversation, a reader of its project for a project thread, or an addressee). Before it is sent, only the uploader can read it. A user access token reads an attachment stored as an artifact only when every clause of its rule holds: it carriesinbox:read, eligible for its boundary; its holder may read the message; the project recorded for the attachment when it was sent is known and within the token’s boundary (no project needs a hub boundary); and, in a direct message with an agent, the token passesagent:readon every agent in the conversation, whichever participant its holder is. Its holder’s unsent upload needs a hub boundary. A token uploads and names attachments only where it may post, withinbox:write. Agents read attachments stored as artifacts only through delivery (below): an agent in good standing that may read the message, as a participant, a reader of its project or an addressee, may receive it, and agents cannot use the download route. A caller who may not read an attachment gets the same answer as for an unknown id, on the download route and on every artifact route. Attachments cannot be shared, versioned or changed, are never listed, and are removed when their message is deleted. They are served like artifact files:X-Content-Type-Options: nosniff,inlineonly for safe image and text types, a sandboxingContent-Security-Policywhen the Hub streams the bytes, and a short-lived signed URL on object storage. - Attachment delivery capabilities: An attachment stored as an artifact reaches an agent as a file in its home (
~/.scion/inbox/<message-id>/), written by the agent’s Runtime Broker. For each dispatch of a message, the Hub asks the attachment read rule for the recipient agent, on a context that carries only the recipient’s identity, and issues a capability only when it allows. A capability is signed with its own Hub key (HMAC-SHA256, separate from every other signing key) and names one attachment, message, agent and broker. It expires 15 minutes after it is issued and can be redeemed once. The broker redeems it atGET /api/v1/attachment-deliveries/<token>with its own HMAC credential. The Hub checks that the broker is the one named in the capability, verifies the signature before it reads the payload, checks the expiry, records the single use before it serves any byte, and checks that the message still exists and the attachment is still bound to it. Every refusal gets the same404. On object storage the file is served through a 60-second signed URL, which the broker fetches without its Hub credential. The broker checks each file’s size and SHA-256 against the values the Hub sent, and writes the files as the agent user with one exec: the files go into a temporary directory withumask 077, a symlink on the inbox path is refused, and the directory is moved into place under an atomic claim. File names are reduced to[A-Za-z0-9._-]by the Hub. Old inbox directories are pruned without following symlinks. - Untrusted content isolation: Workspace and shared-directory files are written by users and agents, not by the Hub. Responses that serve them, including
?view=truepreviews and chat attachments, carry aContent-Security-Policy: sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox allow-downloads; frame-ancestors 'none'header andX-Content-Type-Options: nosniff. These pages, and pages served from an agent’s exposed port, can’t be embedded in other pages or sites. The sandbox omitsallow-same-origin, so a sandboxed document gets an opaque origin:allow-scriptslets its scripts run, so generated HTML reports keep working in the file browser’s preview, but it cannot act with the viewer’s Hub session. - Web page scripts: Inline scripts on the web pages run only with a per-response script nonce. Each page response carries a fresh nonce for the one inline script of the app shell, and no other inline script, inline event handler or
javascript:URL runs. Script files served from the hub’s own address are also allowed. The pages also ask the browser to report, without blocking, strings passed to APIs that parse markup or script. Reports go toPOST /api/v1/csp-reportsand are written to the hub log; they are not persisted in application storage, and the log records follow your log retention. A report’s sample, which can hold page content such as a chat message, is left out by default. See Browser Policy Reports. - NFS shared directories: With shared directories on NFS (
server.project_storage.backend: nfs, or the deprecatedserver.shared_dir_storage.backend: nfs), shared-directory operations use anO_NOFOLLOWcomponent walk anchored on the project tree’s inode. See Shared Directory Storage.
3.6 Workspace Host-Path Validation and Agent Home Confinement
Section titled “3.6 Workspace Host-Path Validation and Agent Home Confinement”Before a Runtime Broker bind-mounts, syncs, uploads, or recursively chowns a workspace or agent-home host path, it resolves the path through any symlinks and validates the result. The check runs on every runtime and fails closed: a path that cannot be resolved, including when the home directory cannot be determined, is refused. The Runtime Broker then acts on the resolved path, not the original one. The following are always refused:
- The filesystem root (
/, or a Windows volume root). - Critical system directories.
- The user’s home directory, or any ancestor of it.
- The scion home (
~/.scion) or any ancestor of it, and anything under~/.scionexcept the subtrees that legitimately hold workspaces and agent homes, such as Hub-managed project workspaces under~/.scion/projects/<slug>.
Where the broker knows the project’s root, the resolved path must also fall under it, so a symlink inside a project that points elsewhere is judged by where it leads.
- Workspace mount derivation and
.scionprotection: The Runtime Broker determines whether a workspace is the agent’s own checkout (<agent-dir>/workspaceor a verified linked worktree recorded under the agent’s broker-owned state directory), derives container mounts only from broker-known paths using git probes bounded byGIT_CEILING_DIRECTORIES, with inheritedGIT_*variables removed (worktree checks also read no global or system git config), only to verify them, and refuses a git common directory outside the project or a timed-out git probe. In shared workspaces,.scionis mounted read-only (on the Applecontainerruntime, which cannot bind-mount a single file, a.scionmarker file that would be shared is refused at start),.scion/agentsis created if missing and masked with an empty tmpfs on Docker and Podman whenever.scionis a directory (the start fails if.scioncannot be inspected, or if.scion/agentscannot be created or is not a real directory), and a hub-managed workspace root with no.scionentry skips the project settings and harness-config file tier rather than creating one. - Host-side git hardening (
pkg/util): Host-side git commands that Scion runs during provisioning clones, shared-workspace pulls, and worktree or shared-clone operations run with a scrubbedGIT_*environment, with hooks, fsmonitor, the pager, signing, submodule recursion and theext::transport disabled, with.lfsconfigendpoints ignored, and with HTTP(S) tokens supplied only through a host-scoped credential helper. Commands in an existing repository first check its.git/configandconfig.worktreeagainst a strict allow-list. See Git that Scion runs on the host. - Agent home confinement (
pkg/agenthome): On the Runtime Broker start path, writes and removals under an agent’s home directory (resetStagedProvisioning,restoreControlPlaneInputs,restoreSecretsRecord, container-script staging,writeAgentTokenFile, andagent-info.jsonreads and writes) resolve relative to an open directory handle on the agent home, usingO_NOFOLLOWon each directory component, atomic temporary-file renames over leaf files so symlinks at the leaf are replaced and never written through, and recursive removals that never descend through symlinks.
4. Secret Management
Section titled “4. Secret Management”Scion provides a typed, scope-aware secret management system. Secret values are never stored in plaintext in the Hub database. For a user-facing guide, see Secret Management.
4.1 Secrets Backend Architecture
Section titled “4.1 Secrets Backend Architecture”The Hub uses a pluggable SecretBackend interface for secret storage:
| Backend | Value Storage | Write Operations | Read Operations |
|---|---|---|---|
gcpsm (GCP Secret Manager) |
Encrypted in GCP SM | Supported | Supported |
local (default) |
Encrypted at rest (AES-256-GCM) in Hub DB | Supported | Supported |
When gcpsm is configured, a hybrid model is used:
- Metadata (name, type, scope, version) is stored in the Hub database.
- Secret values are stored in GCP Secret Manager with automatic versioning.
- GCP SM secret names follow the pattern:
scion-{scope}-{sha256(scopeID)[:12]}-{name}.
The local backend now encrypts secret values at rest using AES-256-GCM, with domain-separated key derivation from the hub signing secret. Legacy plaintext values are transparently readable and will be automatically re-encrypted upon the next write operation.
4.2 Secret Scopes and Resolution
Section titled “4.2 Secret Scopes and Resolution”Secrets are scoped and resolved hierarchically when an agent starts. The following scopes are merged in order (last one wins for the same key):
- Runtime Broker scope (lowest priority): Infrastructure-level defaults for agents on that broker.
- Hub scope: Global defaults.
- Project scope: Project-level secrets.
- User scope (highest priority): Personal secrets for the agent’s owner.
This produces a merged set of secrets for each agent: for the same key, User scope wins over Project scope, Project over Hub, and Hub over Runtime Broker. Environment variables use the same scope order, and for the same key a variable from any scope takes precedence over an environment-type secret.
4.3 Secret Types and Projection
Section titled “4.3 Secret Types and Projection”Secrets are typed to control how they reach the agent container:
environment: Injected as environment variables (default).variable: Written to~/.scion/secrets.jsoninside the container.file: Written to a specified filesystem path (max 64 KiB).
4.4 User Access Tokens (UATs)
Section titled “4.4 User Access Tokens (UATs)”For headless environments (CI/CD, automation), Scion supports user access tokens (UATs).
- Tokens are prefixed with
scion_pat_(a legacy artifact of the older “personal access token” name). - Only the SHA-256 hash of the token is stored in the database; the original value is never persisted.
- Tokens can be scoped to specific permissions and projects, and revoked instantly via the dashboard or CLI.
- Each token row records an explicit boundary (
boundary_kind, defaultproject). A project-boundary token must carry aproject_id, and a database CHECK constraint enforces the pairing. At startup, the Hub logs the IDs (never the token or project) of any rows that break this rule, and such tokens are rejected when used, while valid tokens keep working. On SQLite, a hand-edited row whoseproject_idis not a UUID fails the schema migration, so correct or delete it before upgrading. - Tokens can be minted with an explicit hub boundary (
"boundary": {"kind": "hub"}onPOST /api/v1/auth/tokens), which reaches hub-level resources and every project; a request naming no boundary is rejected rather than read as hub-bound. Hub-only selectors such asbroker:createare mintable only on hub-bound tokens. Broker creation admits a UAT only when it is hub-bound and carriesbroker:create, and such a token re-registers only a broker its own user created (see Runtime Broker). - Every bearer request passes one gate: the boundary is valid, the target’s scope is inside the boundary, the permission is inside the stored ceiling, the holder still has active access to a project target, and the holder’s live authority allows the action. Any evaluation error denies.
- At mint time, each requested scope is checked against the caller’s live authority before the token is written; an ineligible scope is refused with
403 scope_violation. - Operation-specific checks: inbox, conversation and notification operations check the token’s inbox selectors and boundary; project messaging policy, template and project configuration operations each require their own permission; Hub configuration operations admit only hub-bound tokens with the matching selector; and hub pre-start hook scripts are redacted for every credential other than an interactive session. See User Access Tokens.
- A token’s selected scopes are a ceiling, not a grant: minting a token with a scope records it as a restriction on what the token may do, and grants no access by itself. Every request the token later makes is independently authorized against the holder’s current authority on the specific target, including active project access — this is re-checked on every request, not just at mint time.
agent:attachandagent:port_accessare resource-relative: they may be selected for a project before the holder has created a single agent in it, but each later request against a specific agent is authorized separately. Anagent:attachrequest succeeds only for the holder’s own agents and their descendants. Anagent:port_accessrequest also succeeds for any agent in a project where the holder’s role grantsagent.port_access(the built-inproject-ownerandproject-adminroles do). Losing project access denies every subsequent request against that project’s targets immediately, independent of the token’s remaining validity period.
4.5 Credentials Propagation
Section titled “4.5 Credentials Propagation”Scion ensures that sensitive credentials (GCP Service Accounts, API keys for LLMs) are propagated into agent containers securely.
- Docker / Podman: Injected via environment variables or read-only bind mounts for file-type secrets. File secrets are written to a temporary directory and mounted into the container at the target path.
- Kubernetes: Propagated via Kubernetes Secrets or Secret Manager CSI drivers (e.g., GCP Secret Manager).
- Broker Mode Isolation: When agents are dispatched via the Hub, the credential pipeline only uses hub-resolved secrets and environment variables. The broker operator’s host environment and filesystem are never scanned, preventing credential leakage into hub-dispatched agents.
- Isolation: Agent home directories and non-git project data are isolated on the host filesystem and externalized from the workspace to prevent cross-agent data leakage and unauthorized traversal.
- Shadow Mounts: Scion uses
tmpfsshadow mounts to definitively block agents from accessing.scionconfiguration data or other agents’ workspaces within the same project. - Token Reset Off the Command Line:
scion reset-authpasses the fresh Hub token to the container over stdin, not as a command argument, so it never appears in/proc/<pid>/cmdlineon the host. This applies to every runtime. - Lifecycle: Secrets exist only in the agent container’s memory or transient mounts. When an agent is deleted, all projected secrets and transient volumes are purged.
4.6 Hub-Internal Keys
Section titled “4.6 Hub-Internal Keys”JWT signing keys used for agent and user token issuance are stored through the secret backend when GCP Secret Manager is configured. In development mode (local backend), signing keys fall back to direct database storage with a logged warning. These keys use the internal hub scope and are not accessible through the user-facing secrets API.
Skill download signing key. Hubs using local storage for the skill registry sign skill file download URLs with HMAC-SHA256. Cloud storage issues presigned object-store URLs and is unaffected. Each URL is a capability for exactly one file of one skill version (?version=…&exp=…&sig=…), valid for 15 minutes. The Hub signs it only after checking the creator’s read access, and it lets a Runtime Broker download skill files at dispatch without a principal. The key follows the same persistence policy as the signing keys above. When stable keys are required (GCP Secret Manager backend), a failure to load it fails startup. Otherwise the Hub falls back to an in-memory key, which only invalidates URLs that are still outstanding.
4.7 Broker Authentication Secrets
Section titled “4.7 Broker Authentication Secrets”The following broker-related secrets are stored in the Hub database and are not managed through the secrets backend:
- Join tokens: SHA-256 hashed before storage and single-use. Each token has its own lifetime, from 5 minutes to 24 hours (default 1 hour). A broker has at most one token: issuing a new one replaces the unused earlier one. Redeeming a token deletes it in the same transaction that stores the broker’s shared secret, so only one of several concurrent redemptions succeeds, and a failed redemption leaves the token usable. Expired tokens are removed by a scheduled job. The audit log records who created each token, when it expires, and whether it replaced an earlier one; the token itself is never logged.
- Shared secrets: Stored as binary BLOBs in the
broker_secretstable; used for HMAC-SHA256 request signing.
These are infrastructure-level secrets established during broker registration and are managed by the broker authentication subsystem rather than the user-facing secrets API.
4.8 Credential Redaction in Logs and URLs
Section titled “4.8 Credential Redaction in Logs and URLs”The Hub masks the password in its database connection string (DSN) before writing it to logs or stdout. This covers Hub startup and the scion server recover-authz and scion server migrate-storage commands. Both URL-style (postgres://user:pw@host/db) and libpq keyword/value DSNs (password=...) are masked, including Cloud SQL Unix-socket hosts and credentials passed as query parameters (?password=, sslpassword=). A non-SQLite DSN that cannot be parsed is replaced with a placeholder rather than logged. SQLite paths carry no credential and are logged unchanged.
URL user-info redaction (pkg/util/urlredact) is shared across CLI output, API client errors, log text (SanitizeText), Hub endpoint strings, git clone URLs, template update URLs, artifact remote fetches, and tunnel endpoints. When a URL holds an @ and its user info cannot be told apart from its host and path, urlredact fails closed and shows only the URL’s scheme followed by a placeholder (xxxxx in CLI output and API client errors) rather than returning the input unchanged. Skills directory discovery (POST /api/v1/skills/discover-directory) passes through only the ?token=<SECRET_NAME> query parameter ([A-Z][A-Z0-9_]*, a token value that is not such a name is refused with 400) and strips all other query parameters and fragments before normalizing or logging candidate URIs.
5. Development Security
Section titled “5. Development Security”To facilitate local development, Scion provides a Development Authentication mode.
- Developer Token: A persistent token starting with
scion_dev_stored in~/.scion/dev-token(or provided viaSCION_DEV_TOKEN/SCION_DEV_TOKEN_FILE). - Loopback-Only Client Rule: The CLI sends development credentials only to loopback Hub endpoints (
localhost,127.0.0.1,::1). For any non-loopback Hub,SCION_DEV_TOKENandSCION_DEV_TOKEN_FILEare not sent and, when no other credential applies, print a one-line notice to stderr naming the Hub, and~/.scion/dev-tokenis not sent and prints no notice. - Agent Containers and Dispatched Agents: Hub-dispatched agents authenticate with the token the Hub issues for their run (
~/.scion/scion-tokenorSCION_AUTH_TOKEN). The Runtime Broker never passes its ownSCION_DEV_TOKENorSCION_AUTH_TOKENvalues into an agent’s environment. When a Hub and Runtime Broker run co-located with dev auth enabled, the Runtime Broker offers its dev token only to an agent that has no Hub-issued token, whose dispatch comes from the co-located Hub on a loopback endpoint, and whose Hub endpoint comes from the server’s own configuration rather than from project settings or the agent’s environment. - Constraints & Safeguards: Dev mode is disabled by default. Startup validation strictly prohibits binding the
devAuthMiddlewareto non-loopback interfaces (e.g.,0.0.0.0). Because this middleware automatically logs in every cookieless request as an admin, refusing it on public interfaces prevents the accidental exposure of an unauthenticated admin UI. - Warning: The server logs clear warnings when operating in Dev Mode.