Service roles
Give each reference-runtime service its own account and credential, pin the broker's peers, and start the services in authority order.
On this page
Each reference-runtime service runs under its own Unix account. The edge holds four distinct bearer credentials, the broker serves one pinned client UID, and every unit starts only after the authority it depends on can answer.
A role combines a responsibility, a process identity, and access to particular state: the trust control plane issues authority, the edge mediates sessions, the broker owns provider credentials, and the key-log services retain authority history. Accounts, signing keys, and protocol credentials each enforce part of that separation.
StartUnits order two dependencies; readiness checks the rest
The edge declares Requires= on the trust control plane, and each audit monitor starts after all three witnesses. The broker and the trust control plane carry no unit ordering on other services: a missing authority fails their own startup, and Restart=on-failure retries it. Service roles.
1. Create one account per service family
The reference runtime's sysusers.d declarations create four accounts, one per service family, so a process compromised under one account cannot read another's state or credentials. Install them with systemd-sysusers as part of the installed layout.
| Account | Runs | Starts when |
|---|---|---|
chio-trust | Trust control plane | Its own runtime is ready; any keyring is operator-configured |
chio-edge | Remote MCP edge | Trust control is up (Requires=) and preflight passes |
chio-keylog | Key-log witnesses and audit monitors | Witnesses: local storage. Monitors: all three witnesses |
chio-broker | Secret broker | Holds reconcile and custody verifies |
You should see the four accounts in /etc/passwd. The three witness instances and two audit instances share chio-keylog: each has its own state directory and credential, but one UID gives them no isolation from each other. Where witness independence matters, run each witness under its own account or on its own host.
2. Give the edge four distinct bearer roles
The remote MCP edge carries four bearer credentials: the session and admin credentials it accepts, and the control and workload credentials it presents to the trust control plane. chio security preflight and the edge's launch validation both refuse two roles that share a value, and any value that is empty, padded, or carries a control character. Prove it with dummy values first:
The comparison script, compare_bearer_roles.sh
#!/usr/bin/env bash
# A disposable preflight comparison with dummy values, never live credentials.
set -eu
umask 077
: "${CHIO_EVIDENCE:?Set a new private evidence directory}"
if [ -e "$CHIO_EVIDENCE" ]; then
echo 'Refuse to reuse evidence' >&2
exit 2
fi
mkdir -m 0700 "$CHIO_EVIDENCE"
run_case() {
name=$1
admin=$2
if CHIO_AUTH_TOKEN=fixture-session CHIO_ADMIN_TOKEN="$admin" \
CHIO_CONTROL_TOKEN=fixture-control \
CHIO_REMOTE_AUTHORITY_WORKLOAD_TOKEN=fixture-workload \
chio security preflight --json > "$CHIO_EVIDENCE/$name.json" \
2> "$CHIO_EVIDENCE/$name.stderr"; then
status=0
else
status=$?
fi
printf '%s\n' "$status" > "$CHIO_EVIDENCE/$name.exit"
}
run_case distinct fixture-admin
run_case reused fixture-session
python3 "$(dirname "$0")/compare_bearer_roles.py" "$CHIO_EVIDENCE"Download compare_bearer_roles.sh and compare_bearer_roles.py into one directory, then run the script with the installed chio, Bash, and Python 3.11. It runs preflight twice with dummy values, changing only the admin value, and keeps both reports and exit statuses in a new private directory:
export CHIO_EVIDENCE="$HOME/chio-role-evidence"
bash compare_bearer_roles.shOK role probe: four distinct dummy roles REFUSE role probe: reused session/admin value, exit 1 OTHER PROBES retained separately; no launch, store or physical readiness claim
You should see the distinct case pass with "4 bearer roles are usable and distinct" and the reused case exit 1 with "session and admin credentials share one value". The role probe runs on any host; the other probes stay in the retained reports, and an unchecked launch or store makes no claim about them.
3. Pin the broker to its service and client UIDs
The secret broker's daemon config, BrokerDaemonConfig, pins two identities. Set trusted_service_uid to the account that owns the daemon, its config, and its key files, and authorized_client_uid to the one IPC client it serves:
pub struct BrokerDaemonConfig {
pub schema: String,
pub deployment_id: String,
pub broker_instance_id: String,
pub tenant_scope: String,
pub audit_runner_id: String,
pub trusted_audit_runner: PublicKey,
pub ipc_socket_path: PathBuf,
pub authority_socket_path: PathBuf,
pub trusted_capability_issuer: PublicKey,
pub trusted_authority: PublicKey,
pub broker_identity: PublicKey,
pub broker_audience: String,
pub parent_audience: String,
pub provider_adapter_id: String,
pub provider_adapter_version: u32,
pub provider_placement: ProviderPlacementConfig,
pub trusted_service_uid: u32,
pub authorized_client_uid: u32,
pub ipc_read_timeout_ms: u64,
pub ipc_write_timeout_ms: u64,
pub authority_timeout_ms: u64,
pub maximum_clock_skew_seconds: u64,
pub maximum_liveness_snapshot_age_seconds: u64,
pub maximum_revocation_snapshot_age_seconds: u64,
pub databases: BrokerDaemonDatabaseConfig,
pub enterprise_migration: BrokerDaemonMigrationConfig,
pub admin: BrokerDaemonAdminConfig,
pub privileged_audit: BrokerDaemonPrivilegedAuditConfig,
}You should see the broker start only when its config, like every inherited key file, is owned by trusted_service_uid; a foreign owner stops it with custody_unavailable. Once serving, it compares each peer's UID with authorized_client_uid. Credential custody covers the peer-identity check over the broker's socket and the independently configured authority keys.
4. Start the services in the order their authority requires
Each service starts after what it consumes can answer:
- Custody. Prepare sealed key custody before the services that consume it.
- Authorities. Establish authoritative holds and the key-history services before the broker or the trust control plane depends on them. Open each store under its service identity, verify its anchors, and reconcile retained operations.
- Configuration. Install the policy, manifest registry, signing authority, and runtime bundle the entrypoint needs.
- Response authority. Validate the response snapshot and the combined deployment before publishing it.
- Serving host. Let the host verify its executor profile before it receives traffic.
The reference runtime starts the units in this order. Units order only two dependencies: witness units wait on local storage and each audit unit wants and starts after all three witnesses, and chio-mcp-edge.service declares Requires=chio-trust-control.service. The trust control plane and the broker carry no unit ordering on other services; an operator-enabled keyring or an unavailable broker authority fails their own startup, and Restart=on-failure retries.
The shipped trust control unit configures no keyring. Adding --authority-keyring-config requires --authority-seed-file and --authority-keyring-receipt-anchor-root in place of --authority-db, and startup then waits for every configured witness and audit monitor. The active-defense host checks the response authority's signed health first; node lifecycle gives the drain sequence.
5. Verify each unit's identity and readiness
Read the installed unit, not a process name. systemctl cat shows the effective manager configuration, local overrides included; the packaged unit is deploy/reference-runtime/systemd/chio-mcp-edge.service.
systemctl cat chio-mcp-edge.service
systemctl show chio-mcp-edge.service \
-p User -p Group -p Type -p Requires -p After \
-p ActiveState -p SubState -p Result -p ExecMainStatus
sudo journalctl -b -u chio-mcp-edge.service --no-pagerYou should see User=chio-edge, Type=notify, and Requires=chio-trust-control.service. The supervisor announces readiness only after the configured authenticated health request succeeds, and the ExecStartPre= preflight requires enforcement over the signed launch. An active process that has not notified is still starting; the journal names the boundary that failed. A listening socket or a supervisor's running state shows only that something accepted a connection.
Then stop one dependency: the protected path keeps its failure or unknown state and finds no other route, so the provider sees no new call, the file stays unchanged, or the suspended session stays constrained.
A shared host still needs separate keys and files
Distinct bearer values stop role substitution at a protocol boundary, but two processes that can read each other's private files still share authority: rewriting another service's configuration influences it without its admin token. Map each service to the state it reads and changes and the credentials it presents, then check ownership, peer credentials, and configured roots.
| Role | Keep separate | Verify before readiness |
|---|---|---|
| Agent and tool | Provider credentials and authority seeds | Only intended IPC and resource grants are reachable |
| Broker | Tool identity and response authority | Private endpoint, key custody, durable attempts, live authority |
| Response authority | Broker, issuer, and receipt signing keys | Reviewed store and deployment digests, pinned client |
| Key-log services | Operator, witnesses, and auditors | Distinct roots, service identities, storage, fresh challenges |
| Recovery workers | Each other's lease ownership | Valid serving fences and original operation state |
Keep the receipt, issuer, broker, witness, and response-authority keys distinct even when services share a host: each key's artifacts establish a different claim. The reference accounts are a starting composition; choose separate accounts or hosts wherever the threat model requires independent custody. The ownership topology draws these boundaries.
A misconfigured role stops its service at launch
Preflight, the broker, and the edge each refuse a role that breaks separation, before any request is served:
| Refusal | Condition | Owner |
|---|---|---|
Probe security., exit 1: "bearer roles are not separated: the session and admin credentials share one value", naming the two roles | Two of session, admin, control, workload share a value ( Role) | Preflight |
The same probe: "bearer roles are not separated: the control credential is padded, empty or carries a control character", naming the role | A role value is padded, empty, or has a control character ( Role) | Preflight |
"chio- | A custody fault, such as a config or key file not owned by trusted_ (Broker) | Broker |
chio mcp serve-http: "--control-token and --auth-token must be distinct bearer credentials", or naming --admin-token | The control token repeats the session or admin token | Edge |
chio mcp serve-http: "--auth-token and --admin-token must be distinct bearer credentials" | The session and admin tokens are equal | Edge |
chio mcp serve-http: "--auth-token must be non-empty, unpadded, and control-free", naming the flag | A bearer flag is padded, empty, or has a control character | Edge |
chio mcp serve-http: "remote authority workload token must be distinct from service, session, and admin tokens" | The workload token repeats the control (service), session, or admin token | Edge |
Repair a reused or malformed bearer value at its LoadCredential= input and rerun the probe. A broker socket with the wrong owner or pins supplies no trusted peer; check the configured UIDs, the socket's custody, and the authority key. Units that start in order without runtime authority are repaired at the authority owner: signed health, stores, pins, and recovery prerequisites.
Where the role checks live
ROLE_VARIABLES (roles.rs:17) names the environment variable each role reads, from CHIO_AUTH_TOKEN for the session role to CHIO_REMOTE_AUTHORITY_WORKLOAD_TOKEN for the workload role; RoleDefect::Reused and RoleDefect::Unusable are at roles.rs:26. At launch, validate_hosted_bearer_roles (authority_mode.rs:14) refuses a control token that repeats the session or admin token, and the remote-authority check refuses a workload token that repeats the control (service), session, or admin token. BrokerDaemonConfig declares trusted_service_uid and authorized_client_uid at daemon_runtime.rs:213-214, and BrokerDaemonConfig::load refuses a config file not owned by trusted_service_uid at :266-269.