Security Notes¶
This library targets confidentiality for agent secrets at rest and in memory. It does not assume a hostile OS for v1.
Reporting a vulnerability
Report security issues privately to security@agntcy.org, not via the public issue tracker. The project tracks and publishes advisories in the GitHub Security Advisories space — see SECURITY.md for the full disclosure and coordination process.
OpenPGP key handling is performed in-process via sequoia-openpgp rather than
shelling out to gpg.
Agent key derivation¶
SHADI derives local agent keys from human identity material using:
- KDF:
HKDF-SHA256 - Salt:
shadi-agent-derive - IKM: human source bytes — see the sources below
- Info: agent name
The 32-byte HKDF output becomes an Ed25519 private key seed. SHADI stores the
derived public key and computes did:key from that key.
Human identity sources¶
--source |
IKM | Human DID |
|---|---|---|
gpg |
OpenPGP secret-key material as stored | did-from-gpg, or did-from-github (Ed25519 GPG key only) |
ssh |
the key's 32-byte Ed25519 seed | did-from-ssh, or did-from-github --key-type ssh |
seed |
the stored bytes verbatim | none — the root is not a published key |
Sources are not interchangeable: each produces different agent DIDs for the same agent name, because the IKM differs.
The ssh source uses the key's seed rather than the key file's bytes,
because the OpenSSH container is not a stable encoding — re-encrypting with a new
passphrase rewrites the file while the key is unchanged. Deriving from the file
would silently change every agent DID. (gpg predates this and hashes its stored
material, so re-exporting a GPG key can change its derived agents.)
Only ssh-ed25519 is accepted. Hardware-backed sk-ssh-ed25519 keys expose no
private key and cannot root a derivation.
An SSH passphrase is read from the secret store
(--ssh-passphrase-secret <ref>) or SHADI_SSH_PASSPHRASE, never from a command
-line argument, which would be visible to any local process via ps.
Because an SSH key is normally published at github.com/<user>.keys, the human
DID derived from its public half is verifiable by anyone against the account that
claims it — the private half never leaves the machine. That public binding is
what "sign in with GitHub" contributes; it is not secret material. What it does
not yet establish is that a given agent belongs to that human at SLIM
admission time — see the note below.
Human-to-agent linkage verification¶
Use shadictl verify-agent-identity to recompute the expected agent key and
DID from the same human source and compare them to stored values.
If a human DID binding is stored ({prefix}/{agent}/human_did), verification
can additionally assert that the binding matches a specific human DID key.
This check is local and offline. On the wire, SLIM admission verifies the agent
DID against the SLIM_MEMBER_DIDS allow-list; SLIM_HUMAN_DID is reported for
display and is not verified, so a peer cannot currently prove which human an
agent belongs to. Tracked in agntcy/shadi#141.
This is the trust bootstrap path end to end — from human identity material to gated secret and memory access:
flowchart LR
Source[Human identity source] --> Derive[Derive agent identity]
Derive --> Register[Register trusted secrets]
Register --> Verify[Verify session and operator context]
Verify --> Access[Authorize secret access]
Access --> Protect[Release memory key]
Protect --> Persist[Persist encrypted state]
- Human identity material is imported or derived.
- SHADI stores agent keys, DIDs, and optional provenance bindings in the secret store.
- Session verification gates later reads.
- The SQLCipher memory key is retrieved through the same secret-control path rather than living in plaintext config.
Node auth versus application auth¶
Two different things are called authentication on the SLIM path. Mixing them is how a mesh member can present another agent's DID. Tracked in agntcy/shadi#182; the handoff proof is agntcy/shadi#183.
Node auth answers: may this process join the mesh? SLIM 2.3 provides
that — slim login (OIDC device flow), SPIRE workload identity, or a
legacy shared secret. A node credential says the process is allowed on the
wire. It does not say which agent sent a message.
Application auth answers: which agent is speaking, and whose is it?
SLIM does not provide this. SHADI does, with did:key agent DIDs derived
from the human's key. Agentbridge must verify that DID on a handoff,
delegate, or coordinate task — not the node credential.
| Layer | Question | Credential | Who checks it |
|---|---|---|---|
| Node | May this process join the mesh? | slim login / SPIRE SVID / shared secret |
SLIM |
| Application | Which agent sent this message? | Agent did:key signed over the payload |
SHADI / agentbridge |
| Human binding | Which human does that agent belong to? | Attestation over the agent DID | Not on the wire yet (#141) |
Rules that must stay true when SLIM node auth is adopted:
- Agentbridge verifies SHADI's agent DID chain before accepting a handoff. A node SVID is not a substitute.
- The node credential and the agent DID travel together; they are not interchangeable.
SessionContext.didis proven from a signature on the message on the agentbridge SLIM path (SessionContext.did_proven). A locally setsession.verifiedbool is not application auth.- Shared-secret node auth remains valid only for single-mesh setups and
is rejected by
agentbridge register --slim-endpoint, because a listener forwards incoming A2A tasks to a local CLI.
Adopting SLIM's TokenProvider / Verifier (agntcy/shadi#179) and
slim login / SPIRE (agntcy/shadi#180) must not collapse these layers.
1Password backend¶
When SHADI_SECRET_BACKEND=onepassword is set, secrets are stored in a
1Password vault instead of the OS keychain. The backend shells out to the op
CLI (1Password CLI v2). No secrets are cached in memory beyond the lifetime of
each operation.
- Items are stored as Secure Notes with base64-encoded content, tagged
shadi. - For CI/headless environments, set
OP_SERVICE_ACCOUNT_TOKEN(never stored by SHADI; consumed directly byop). - The vault name defaults to
shadiand can be overridden viaSHADI_OP_VAULT.
Secret delivery and disclosure modes¶
SHADI now separates secret storage from secret delivery.
The important distinction is not only whether a secret exists in the store, but which executable is allowed to receive it and by what mechanism.
Current launch-time modes:
--inject-keychainandprocess_inject_keychain: explicit disclosure as an environment variable.process_trusted_secret: process-scoped trusted delivery to the launched executable.process_secret_policy: action-based rules, includingdelegate-to-childfor Unix/macOS final-consumer delivery.
Secret actions¶
Policy should be expressed in terms of actions on a secret rather than a coarse secret category.
disclose: the process may read the secret value directly.use: the policy may represent a narrower operation that depends on the secret without implying ambient plaintext disclosure.delegate-to-child: the process may request that SHADI deliver the secret to a specific child process, but the parent does not receive the secret.signorauthenticate: the process may invoke a narrower operation backed by the secret without receiving the raw secret when the platform integration supports that shape.
Final-consumer delivery rule¶
The core enforcement rule is:
A secret must be delivered by SHADI directly to the final authorized consumer process, not inherited through a parent process and not placed into a general environment visible to unrelated descendants.
This rule applies even when the process tree is parent-driven:
- An agent or parent tool requests launch of child tool
T. - SHADI validates that
Tis allowed to receive actionAon secretS. - SHADI verifies the exact executable identity for
T. - SHADI launches or mediates the final consumer boundary.
- SHADI delivers the secret to
Tone time. - The parent does not receive the secret as an env var, inherited fd, handle, or reusable fetch token.
sequenceDiagram
participant SHADI as SHADI runtime
participant Parent as Parent workload
participant Broker as Delivery broker
participant Child as Authorized child tool
participant Store as Secret store
SHADI->>Store: Resolve secret rule and target identity
SHADI->>Parent: Launch protected parent process
SHADI->>Broker: Create launch-scoped delivery channel
Broker-->>Parent: Endpoint reference and nonce
Parent->>Child: Start authorized tool
Child->>Broker: Request secret delivery
Broker->>Broker: Verify process identity, executable path, and optional hash
Broker->>Store: Read approved secret payload
Store-->>Broker: Secret bytes
Broker-->>Child: Release one-time secret payload
Broker-->>Broker: Close channel and revoke reuse
Why this matters¶
Two risks need different controls:
- Wrong-process disclosure: a secret reaches a parent or sibling process that was not the intended consumer.
- Prompt-injection misuse: an LLM-facing process that legitimately sees a secret is induced to exfiltrate it.
SHADI's current policy framework primarily improves the first risk by keeping secret rules bound to the exact launched executable and, for delegated Unix/macOS delivery, to a verified child executable plus a one-shot broker protocol.
Prompt injection is a separate concern from wrong-process delivery:
- Process-boundary controls decide which process can receive a secret.
- Prompt-injection controls decide whether that process should ever receive a raw secret at all.
LLM-driven executables should default away from disclose whenever a safer
action is available — deterministic or tightly scoped tools may receive
disclose when necessary, but LLM-driven agents should more commonly receive
delegate-to-child or a narrower use capability. High-value provider
credentials should prefer mediation patterns where SHADI or a trusted adapter
performs the authenticated action and returns only the result.
Current platform behavior¶
- Unix/macOS trusted delivery uses a launch-scoped broker endpoint, a companion nonce env (
*_NONCE), peer-process verification, and one-shot release semantics. - macOS temporarily opts into local Unix socket permissions only for launches that require trusted-secret delivery.
- Windows currently supports direct trusted-secret delivery through a compatibility inherited-handle path.
- Windows direct trusted-secret rules can optionally pin the launched executable hash with
exec_sha256to avoid path-only trust. - Windows ACL allowlist changes are journaled to disk before mutation and replayed on the next sandbox startup if a previous
shadictlprocess crashes before cleanup. The journal directory is locked to owner + SYSTEM with a protected DACL, and each journal entry carries an HMAC-SHA256 tag verified before replay to prevent tampering.
Compatibility mode¶
The existing env-injection and inherited-handle paths may still be kept as explicit compatibility mechanisms, but they should be documented as disclosure mode rather than the target secure design:
- env injection: explicit
discloseto the target process, - inherited Windows handles: compatibility path for direct trusted delivery,
- brokered final-consumer delivery: preferred secure path.
Policy intent¶
Treat environment injection as explicit disclosure, not as the default secure path. Prefer final-consumer delivery when the parent process only needs to launch a child tool rather than directly hold the credential.
agentbridge listeners¶
agentbridge register exposes a local coding tool as an A2A service over SLIM.
Every incoming task is forwarded to that tool's subprocess.
Registered listeners are a remote code-execution surface
Any peer that can reach agntcy/shadi/<tool>-a2a can drive the local tool
(some adapters, such as copilot, run with --allow-all-tools).
Controls:
- SLIM peers authenticate with a per-agent DID (
SHADI_SLIM_AUTH=did,SLIM_HUMAN_SEED), verified against an explicitSLIM_MEMBER_DIDSallow-list — shared secrets are not accepted for agentbridge listeners (shadi_identity::require_did_auth_from_envrejects anything else). See the Secure Agent Group Demo for the full admission model. This decides who may send a task. - Transport is mutually authenticated with SLIMRPC over TLS; keep the CA bundle private to trusted peers.
- Enforced:
register --slim-endpointrefuses to start unless it is running under a SHADI sandbox with network blocked by default (shadi_sandbox::sandbox_enforced_from_env). Seatbelt/Landlock/AppContainer sandboxes are kernel-enforced and inherited by child processes, so wrappingagentbridge registerinshadictlconfines whatever CLI tool the adapter spawns to run a task — agentbridge has no sandboxing logic of its own, it leans entirely onshadictl's existing enforcement. This decides what a task can do once it runs. See AgentBridge → Security model.
Threat model¶
Secrets and credential theft¶
- Stopped: Reading secrets without verification.
- Stopped: Exfiltration of secrets from disk when keystore access is denied.
- Stopped: Accidental logging of secrets by non-verified sessions.
- Mitigated: In-memory exposure via zeroization and limited lifetime.
Identity spoofing / provenance ambiguity¶
- Stopped: Undetected key substitution when
verify-agent-identityis used. - Mitigated: Agent ownership ambiguity via stored
human_didbinding + verification.
Filesystem abuse¶
- Stopped: Accessing paths outside allowlists (kernel enforcement).
- Stopped: Writing to disallowed paths in sandboxed processes.
- Mitigated: Destructive commands via CLI blocklist.
Network abuse¶
- Stopped: Network access when
net_blockis enabled. - Mitigated: Unapproved endpoints with best-effort
net_allowguard for Python sandbox runners.
Agent-to-agent data leakage¶
- Stopped: Unverified peers joining a session; DID-JWT authentication and (for groups) an explicit member allow-list gate admission.
- Stopped: Unauthorized peers reading messages; MLS provides confidentiality on point-to-point and group SLIM sessions.
- Stopped: Message tampering; MLS provides integrity/authentication.
Privilege escalation¶
- Mitigated: Prompt-level or path-level agent reasoning by applying sandbox policy before launch.
- Mitigated: Running blocked commands via CLI blocklist when that feature is used.
- Mitigated: Kernel-level constraints remain even if the agent tries to evade application-layer logic.
Threat-to-control mapping¶
| Threat | Control | Outcome |
|---|---|---|
| Unverified secret access | DID/VC verifier + AgentSecretAccess |
Blocked |
| Secret theft at rest | OS keystore storage | Blocked |
| Secret exfiltration in sandbox | OS sandbox + net block | Blocked |
| Unauthorized file access | Seatbelt/AppContainer allowlists | Blocked |
| Destructive commands | CLI blocklist | Mitigated |
| Peer/agent impersonation on SLIM | DID-JWT auth + member allow-list | Blocked |
| Message interception | MLS in SLIM | Blocked |
| Message tampering | MLS integrity | Blocked |
| Agent identity substitution | HKDF derivation + verify-agent-identity | Blocked (with verification) |
| Process escape | Kernel enforcement | Mitigated |
Non-goals and residual risks
- Protecting against a fully compromised host OS or kernel-level malware.
- Metadata privacy beyond message content when using SLIM/MLS, or network traffic metadata (timing, sizes, endpoints) in general.
- ACL changes on Windows could be interrupted before rollback in a crash.
- Application-level path deny rules are weaker than OS-enforced sandbox restrictions; do not rely on path matching alone for high-assurance policy.
Deployment guidance
- Prefer running agents under the sandbox with a JSON policy file.
- Use explicit disclosure only when a demo or workload truly needs the secret in the parent process, and prefer process-scoped or delegated trusted delivery where possible.
- Rotate secrets and use short-lived tokens whenever possible.
Next steps¶
- See the high-level system model in Architecture.
- Put this into practice with Sandbox and Policies.
- Review agentbridge's specific threat model in AgentBridge → Security model.