Chio/Docs
LOGIN · JOIN

Daemon credentials

Start the secret broker with its master key and signing seed as two sealed descriptors, then confirm the handoff and the one client it serves.

Deployment chio@5f63e6e

On this page

The end state is chio-secret-brokerd running on Linux under chio security supervise, holding its tenant master key and broker signing seed as two sealed, read-only descriptors, and serving the one client UID you configured. No key byte passes through the broker's arguments, its environment, or a path it opens.

The broker needs both keys before it can authenticate a request or decrypt a stored credential, so whoever can substitute a key can change the service's identity, and whoever can copy one can reuse it elsewhere. The supervisor makes that bootstrap explicit: it is the one producer, the broker is the one consumer, and descriptor numbers name the inherited objects without placing secret bytes on the command line. The operating-system administrator stays trusted for the supervisor and for the broker's process memory.

Start
Sealed handoffChoose an outcome.
supervisorbroker processIPC peeropenssealssealsread onceread onceconnectscpmσbacredentialsLoadCredential=supervisorchio security supervisemaster keymaster-key-fdsigning seedsigning-key-fdsecret brokerchio-secret-brokerdclientauthorized_client_uid✓private file✓four seals✓peer UIDready: the socket's peer is the supervised brokerbroker processIPC peercpmσba✓✓✓ready: the socket's peer is the supervised broker

ccredentialspsupervisormmaster keyσsigning seedbsecret brokeraclient

Two sealed descriptors reach the broker before it serves

The supervisor opens master-key and signing-key, checks that each file is private, and reseals each one as its own read-only memfd. The broker reads 32 bytes from each descriptor once, after checking SEAL | SHRINK | GROW | WRITE, and only then binds its socket. Credential custody.

One secret broker started by the Chio supervisor on a Linux host: the credentials directory, the two sealed descriptors the broker inherits, and the client UID it serves. The systemd unit, readiness timing, and the broker's stored credentials are left out.

1. Rehearse the handoff with dummy keys

Exercise the descriptor handoff with two disposable random 32-byte fixtures before the broker's custody set goes behind a supervisor. The probe reads and closes both inherited descriptors, checks that they are distinct objects, read-only, and carry all four Linux seals, and prints no key bytes. It observes the handoff only; broker readiness comes in step 4.

The descriptor probe, descriptor_probe.py
security-host/descriptor_probe.pypython
#!/usr/bin/env python3
"""Linux dummy-credential probe; consumes inherited descriptors, prints no bytes."""
import argparse
import fcntl
import os
import sys


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--master-key-fd", type=int, required=True)
    parser.add_argument("--signing-key-fd", type=int, required=True)
    args = parser.parse_args()
    if sys.platform != "linux":
        raise RuntimeError("sealed-descriptor observation requires Linux")
    descriptors = [args.master_key_fd, args.signing_key_fd]
    identities = set()
    required = fcntl.F_SEAL_SEAL | fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_GROW | fcntl.F_SEAL_WRITE
    for descriptor in descriptors:
        metadata = os.fstat(descriptor)
        identities.add((metadata.st_dev, metadata.st_ino))
        if fcntl.fcntl(descriptor, fcntl.F_GET_SEALS) & required != required:
            raise RuntimeError("inherited credential is not fully sealed")
        if fcntl.fcntl(descriptor, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY:
            raise RuntimeError("inherited credential is not read-only")
        if len(os.read(descriptor, 33)) != 32:
            raise RuntimeError("dummy credential must contain exactly 32 bytes")
        os.close(descriptor)
    if len(identities) != 2:
        raise RuntimeError("two roles received the same underlying credential")
    print("VERIFY two distinct 32-byte credentials: read-only, sealed, consumed and closed")


if __name__ == "__main__":
    main()

Download descriptor_probe.py and make_custody_fixture.py into the current directory, then run the exercise on Linux with Python 3 and the installed chio. The private temporary directory holds only the newly generated dummy keys and stays in place for inspection:

Terminalbash
CUSTODY_FIXTURE=$(mktemp -d)
python3 make_custody_fixture.py "$CUSTODY_FIXTURE"
chio security supervise \
  --credentials-dir "$CUSTODY_FIXTURE" \
  --credential-fd master-key-fd=master-key \
  --credential-fd signing-key-fd=signing-key \
  -- python3 "$PWD/descriptor_probe.py"
chmod 644 "$CUSTODY_FIXTURE/master-key"
chio security supervise \
  --credentials-dir "$CUSTODY_FIXTURE" \
  --credential-fd master-key-fd=master-key \
  --credential-fd signing-key-fd=signing-key \
  -- python3 "$PWD/descriptor_probe.py"
Output on Linux
VERIFY two distinct 32-byte credentials: read-only, sealed, consumed and closed
error[CHIO-CLI-IO]: supervise: credential master-key cannot be read: a descriptor credential must be private, singly linked, and owned by the service user
  help: give master-key mode 0400, one link, and the service user as owner
  code: urn:chio:error:cli:io
  docs: https://chio.computer/docs/reference/errors#error-cli-io

You should see the probe's line, then the refusal. The first invocation hands the probe two sealed descriptors. Changing the master-key fixture to mode 644 makes the second invocation exit 1 before the child starts: the probe prints nothing, and the error names private descriptor custody. Restore the fixture mode only to repeat this exercise; service credential ownership stays as provisioned, whatever startup reports.

2. Load the two credentials through systemd

Give the broker's unit one LoadCredential= line for master-key and one for signing-key, pointing at two distinct files. systemd places both in the private, per-unit directory it exposes as CREDENTIALS_DIRECTORY. The loader accepts a file there only in the exact shape systemd produces:

  • owned by root and singly linked, with mode 0440;
  • on a read-only tmpfs or ramfs mount;
  • with a POSIX ACL that grants read access to one non-root service UID alone.

The loader checks each file's shape whatever sets CREDENTIALS_DIRECTORY. Outside a systemd unit, pass the validated directory with --credentials-dir; each file there must be owned by the service user, singly linked, and free of any group or other permission bit. Both shapes are recognized on Linux only.

3. Start the broker under the supervisor

The supervisor opens each named credential, checks that it is private, reseals it on Linux as its own memfd, and passes the inherited descriptor number under the broker's matching flag. The reference unit's ExecStart= is this command, with the reference runtime's installed paths:

Terminalbash
/usr/local/bin/chio security supervise \
  --credential-fd master-key-fd=master-key \
  --credential-fd signing-key-fd=signing-key \
  --ready-unix-socket /run/chio-secret-broker/broker.sock \
  --ready-timeout 60 \
  --stop-grace 60 \
  -- /usr/local/bin/chio-secret-brokerd \
  --config /etc/chio/secret-broker/broker.json

Each --credential-fd binding names the broker flag before the = and the credential after it, so master-key-fd and signing-key-fd receive two descriptors backed by two independent memfds. The broker refuses to start without --config, printing chio-secret-brokerd failed closed: invalid_configuration. Credential custody gives the argument shape the broker itself expects once launched.

4. Confirm readiness and the client it serves

--ready-unix-socket reports the broker ready once its socket accepts a connection and the kernel reports the supervised process as that socket's peer. --ready-timeout 60 stops a broker that is still unready after 60 seconds, and --stop-grace 60 gives a stopping broker 60 seconds to drain before SIGKILL.

Once serving, the broker reads each connecting peer's SO_PEERCRED credentials and compares the UID with its configured authorized_client_uid. That UID is provisioned separately from the trusted service identity every inherited key file must match; service roles covers both. Your client, connecting under that UID, gets responses; any other UID gets a closed socket.

The broker serves on Linux. On any platform other than Linux and Android the sealed key read refuses with chio-secret-brokerd failed closed: custody_unavailable, and on Android the IPC endpoint refuses to bind with chio-secret-brokerd failed closed: authority_unavailable, before any peer connects.

The broker reads each key once, from a sealed descriptor

The master key decrypts credential material and the signing seed authenticates the broker's evidence, so each arrives on its own read-only descriptor. The broker consumes each descriptor once, and checks all four seals, SEAL | SHRINK | GROW | WRITE, before reading its 32 bytes:

crates/security/chio-secret-broker/src/encrypted_blob_backend.rs26–30rust
pub struct SealedKeyFd {
    file: File,
    expected_owner: u32,
    consumed: bool,
}

Sealing guarantees that the accepted object cannot be rewritten or resized through an ordinary writable file while the broker consumes it. Custody of the broker's memory still rests with the service identity and host hardening, and the operating-system administrator remains inside that boundary. Where these descriptors sit among Chio's other custodians is on the custody model.

Every handoff failure stops before the broker serves

Row 1 is the supervisor's own refusal, printed on stderr before the broker starts. Rows 2 and 3 belong to chio-secret-brokerd.

RefusalConditionOwner
CredentialError::Unreadable "descriptor credential must be private, singly linked and owned by the service user"
The file is hard-linked, foreign-owned, or group- or other-readable, and is no systemd credential
Supervisor
BrokerError::Custody "chio-secret-brokerd failed closed: custody_unavailable"
On a direct launch, a key descriptor lacks one of SEAL | SHRINK | GROW | WRITE
Broker
BrokerError::AuthorizationDenied "broker IPC client UID is not authorized"
The IPC peer's UID differs from authorized_client_uid
Broker

The supervisor exits 1 before launching the broker

The loader refused a credential file before resealing it, so nothing was delivered. stderr reads error[CHIO-CLI-IO]: supervise: credential <name> cannot be read: a descriptor credential must be private, singly linked, and owned by the service user, followed by help: give <name> mode 0400, one link, and the service user as owner and the code and docs lines. Inspect the unit's service UID, the file's owner, link count, and mode, the credentials directory's shape, and the two key-role bindings, then correct the file.

The broker prints custody_unavailable and exits

BrokerError::Custody is fatal at startup, and every custody failure prints the same fixed category line, under the supervisor or on a direct launch. Under the supervisor each descriptor is always sealed, reopened read-only, and backed by its own memfd, so this line points at a direct launch or a non-Linux host. On a direct launch, a descriptor missing a seal carries "master key descriptor lacks required seals" or "signing key descriptor lacks required seals" as its source, and the descriptor can still be written or grown.

Checks a direct launch adds

A chio-secret-brokerd launch that bypasses the supervisor still guards against one descriptor standing in for both roles. Its argument parser refuses two identical --master-key-fd and --signing-key-fd numbers with BrokerError::Custody "master and signing keys must use distinct inherited descriptors". When two different numbers resolve to the same file, validate_distinct_key_files refuses with "master and signing keys must use distinct sealed descriptors". The seal check in read_sealed_32 reaches only such a launch too. All three print chio-secret-brokerd failed closed: custody_unavailable.

A client's connection closes with no response

BrokerError::AuthorizationDenied is a per-connection outcome: the serving loop moves to the next connection and prints nothing, and the broker keeps running. Compare the client's UID with the configured authorized_client_uid.

Readiness times out

Inspect the broker's log, the inherited role bindings, the original credential stores, and the socket's owner and peer under the service UID before restarting. Repeating the dummy-key fixture cannot show that an unresolved provider operation is safe to send again.

Supervisor flags and source checks

--credential-fd

<flag>=<credential>Repeatable

Opens one named credential from the credentials directory, reseals it on Linux as a sealed memfd, and appends its descriptor number to the invoked command under the long option named before the =. Declared on SuperviseArgs in cli/security_supervise.rs:17-35; the call that loads every binding (:102-103) prefixes a load failure with supervise:.

Example--credential-fd master-key-fd=master-key

chio-secret-broker.service

deploy/reference-runtime/systemd/chio-secret-broker.serviceReference unit

Lines 18 and 19 carry the LoadCredential=master-key:... and LoadCredential=signing-key:... entries, and its ExecStart= (lines 20 to 27) is the supervisor command in step 3.

--credentials-dir

pathOutside a systemd unit

Names the validated credentials directory when CREDENTIALS_DIRECTORY is not set by a unit's LoadCredential= lines.

--ready-unix-socket

pathReadiness

Reports readiness once the socket accepts a connection and the kernel reports the supervised process as its peer.

Example/run/chio-secret-broker/broker.sock

--ready-timeout

secondsReference unit: 60

Stops a broker that is not ready within the given number of seconds.

--stop-grace

secondsReference unit: 60

Gives a stopping broker this long to drain before SIGKILL.

--exec

flagConflicts with --credential-fd

Replaces the supervisor process with the child. It is declared conflicts_with descriptor delivery at the argument level (cli/security_supervise.rs:33), and the command returns before DescriptorCredentials::load runs when it is set, so descriptor delivery and process replacement are mutually exclusive and the supervisor never falls back to exposed keys.

is_systemd_credential

chio-secure-ipc/src/credentials.rs:11Linux only

Recognizes the systemd credential shape: root-owned, singly linked, mode 0440, on a read-only tmpfs or ramfs mount, with a POSIX ACL granting read access to a non-root service UID alone. supervise/descriptor_credentials.rs:93 and supervise/credentials.rs:178 call it before accepting a file. Returns false off Linux (:46).

is_sealed_credential

chio-secure-ipc/src/credentials.rs:72Linux only

Recognizes a read-only sealed memfd. A test at supervise/descriptor_credentials.rs:198 checks the supervisor's own resealed output against it. Returns false off Linux (:87).

CredentialError::Unreadable

supervise/credentials.rs:113Exit 1

The loader's refusal of a hard-linked, foreign-owned, or group- or other-readable file that is not a recognized systemd credential. The file is never resealed or delivered.

sealed_credential

descriptor_credentials.rs:158-185Supervisor

Mints an independent memfd for every --credential-fd binding, so the supervisor's two descriptors are always distinct numbers backed by distinct inodes.

validate_distinct_key_files

daemon_runtime.rs:933-946Broker startup

Called at :486. Refuses two descriptor numbers that resolve to the same file. The argument parser's refusal of identical numbers is at bin/chio-secret-brokerd.rs:87-91.

error_code

bin/chio-secret-brokerd.rs:147-159Broker startup

Chooses the fixed category line the broker prints at startup (:137-145); every BrokerError::Custody maps to custody_unavailable.

validate_broker_peer_uid

service_parts/part_03.rs:1068-1084Per connection

Reads the socket's SO_PEERCRED credentials and compares the UID with authorized_client_uid. On refusal the serving loop moves to the next connection (daemon_runtime.rs:799-800). The Android bind refusal is at :910-914.

Next: restore the broker across a restart