Skip to content

Use Vault Credentials And Egress Proxy

Use this page after your agent is authenticated and you need to use a vault credential – call an external API through the egress proxy, or sign a short-lived SSH certificate. The agent never sees the raw secret; SigID injects it at the egress boundary.

This page continues where the Agent Self-Serve Quickstart ends: you already have (or can obtain) a delegated token. There is no SDK method or CLI command for vault, egress, or SSH signing yet – both @sigid/client and the Rust SDK cover authentication only. The examples below are plain HTTP, which is the authoritative wire contract implemented by the sigid-egress and sigid-server route handlers.

What You Are Building

  1. A list of vault credentials your agent is authorized to use, each identified by a credential_id and constrained to specific upstream hosts.
  2. An outbound call to a third-party API (POST /v1/proxy) where SigID injects the credential's secret and forwards – your agent supplies the request, never the secret.
  3. Optionally, a short-lived SSH certificate signed by a vault-held SSH CA, for server access without distributing long-lived keys.

Concepts

Concept Meaning
credential_id UUID identifying a vault credential row. You pass this to egress / SSH signing; you never receive the underlying secret.
credential_type oauth2, api_key, ssh_key, ssh_ca, or custom. Determines usage path.
auth_scheme How the secret is injected: bearer, basic, header:<name>, or query:<key>. Vault-authoritative – set at credential creation, validated on use, the caller cannot override it.
allowed_hosts Normalized hostnames the credential may be injected toward. The egress sends the SSRF-pinned target host; the core rejects injection toward any host not in this set.
grant Binds a credential to an agent or delegation with a capability snapshot and optional expiry. A delegated runtime use requires vault:read scope and an active grant.
non_exportable If set, the RFC 8693 export path and owner GET .../token refuse the credential; egress injection is its only usage channel.
egress injection boundary The trust boundary where the decrypted secret lives momentarily in sigid-egress sigid-memguard-backed memory, zeroized after use. The agent operates outside this boundary.

Before You Start

Value Production Local Docker Compose
IDP / identity host https://auth.sigid.org http://auth.sigid.localhost:3000
Egress host (POST /v1/proxy) egress endpoint (often same front host via HAProxy) http://auth.sigid.localhost:3000
Required token scopes vault:list (discover), vault:read (egress resolve) same
Token type delegated agent access token same
export SIGID_IDP="${SIGID_IDP:-https://auth.sigid.org}"
# Delegated token (carrying vault:list + vault:read) – for vault list and egress:
export DELEGATED_TOKEN='<delegated agent access token>'
# Direct agent token – for SSH signing (ssh-sign rejects delegated JWTs):
export AGENT_TOKEN='<direct agent access token>'

Security rules for this entire page:

  • Vault list and egress (/v1/proxy) need a delegated token carrying vault:list / vault:read and an active grant on the credential. SSH signing (ssh-sign) needs a direct agent token – it rejects delegated JWTs.
  • Never log bearer tokens, credential_id is fine to log but the injected secret and upstream response bodies may contain secret material – do not echo full proxy responses into tickets or logs.
  • Prefer egress injection over the RFC 8693 export path. If a credential is marked non_exportable, export is impossible by design – injection is the only channel.

List Vault Credentials

Discover credential_id values your agent may use. Scope: vault:list. Returns metadata only – never decrypted secrets.

curl -sS "$SIGID_IDP/api/v1/vault/credentials?limit=50" \
  -H "authorization: Bearer $DELEGATED_TOKEN"

Response (shape)

[
  {
    "id": "018f3d44-7d4d-7d5d-8d3b-9f0c4c37a111",
    "tenant_id": "018f...",
    "tenant_name": "My Workspace",
    "provider": "deepseek",
    "credential_type": "api_key",
    "label": "production-key",
    "auth_scheme": "bearer",
    "allowed_hosts": ["api.deepseek.com"],
    "non_exportable": true,
    "status": "active",
    "metadata": {},
    "created_at": "2026-07-01T12:00:00Z",
    "updated_at": "2026-07-01T12:00:00Z",
    "last_used_at": null
  }
]
Field Semantics
id The credential_id you pass to /v1/proxy or SSH signing
credential_type Usage path: api_key/custom/ssh_key → egress; ssh_ca → SSH signing; oauth2 → managed OAuth (separate /oauth/token exchange)
auth_scheme Injection method; you may echo it on /v1/proxy but cannot change it
allowed_hosts Pick a credential whose allowed_hosts covers your target URL's host, else egress rejects the call
status Must be active; revoked credentials fail at resolve time
non_exportable If true, only egress injection can use this credential

List supports cursor pagination via cursor_ts + cursor_id query parameters.

Call An API Through Egress

POST /v1/proxy forwards a caller-built request to an upstream API, injecting the decrypted credential according to the vault-authoritative auth_scheme. Scope: vault:read + active grant. Hosted at the egress service (sigid-egress).

curl -sS "$SIGID_IDP/v1/proxy" \
  -X POST \
  -H "authorization: Bearer $DELEGATED_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "credential_id": "018f3d44-7d4d-7d5d-8d3b-9f0c4c37a111",
    "method": "POST",
    "url": "https://api.deepseek.com/v1/chat/completions",
    "headers": [["content-type", "application/json"]],
    "body": "{\"model\":\"deepseek-chat\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}"
  }'

Request fields

Field Type Notes
credential_id string (UUID) Required. Must be a valid UUID; the credential must be active, granted to your agent/delegation, and its allowed_hosts must cover the target host.
method enum GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS
url string Full HTTPS URL of the upstream target. SSRF-validated; the host must be in the credential's allowed_hosts.
headers array of [name, value] pairs Caller-supplied request headers. Auth/injection headers are added by egress per auth_scheme; do not duplicate them. Sanitized before forwarding.
body string UTF-8 request body. Mutually exclusive with body_base64.
body_base64 string Base64-encoded body for binary payloads. Mutually exclusive with body.
auth_scheme string (optional) If supplied, must equal the stored scheme or the call is rejected. Omit to let the vault-authoritative scheme apply.
stream boolean (optional) Default false. When true, the upstream response body is forwarded chunk-by-chunk with no whole-response buffering. Use this for SSE / LLM completion streams. Request bodies stay JSON-buffered on this path.

Response: the upstream API's response. With stream: false (default), the body is buffered under a hard size cap. With stream: true, headers return as soon as the upstream answers and chunks are piped through; mid-stream transfer stops if a metered share exhausts its byte slice, the idle gap between chunks exceeds ~90s, or the absolute hop timeout (default 5 minutes) elapses. When the stream closes, egress records a post-stream usage envelope (bytes, chunks, duration, outcome, advisory token counts when present) as an egress.stream.closed audit event. Upstream response headers are replayed through an allowlist in both modes.

Streamed request body (POST /v1/proxy/body)

For large uploads that should not be base64-wrapped in JSON, use the raw-body path. Metadata is carried in headers; the HTTP body is the upstream payload.

curl -sS "$EGRESS/v1/proxy/body" \
  -X POST \
  -H "authorization: Bearer $AGENT_TOKEN" \
  -H "x-sigid-credential-id: $CREDENTIAL_ID" \
  -H "x-sigid-url: https://api.example.com/v1/files" \
  -H "x-sigid-method: POST" \
  -H "content-type: application/octet-stream" \
  --data-binary @large.bin
Header Notes
authorization Bearer agent/recipient token (required)
x-sigid-credential-id Vault credential UUID
x-sigid-url Full HTTPS upstream URL (SSRF + host binding)
x-sigid-method GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS
x-sigid-auth-scheme Optional; must match stored scheme if set
x-sigid-stream-response Default true. Set false for a buffered upstream reply
x-sigid-header-<name> Optional extra upstream request headers
content-type Forwarded to upstream as its Content-Type

Request bodies are capped at 32 MiB and count against a shared credential's byte budget when a lease is held. Response streaming behaviour matches stream: true on /v1/proxy.

The agent never receives the injected secret. The decrypted credential lives only in sigid-egress memory for the duration of the request and is zeroized after use.

Sign An SSH Certificate

POST /api/v1/agents/delegations/ssh-sign signs a short-lived SSH certificate using a vault-held ssh_ca credential. Requires a direct agent principal (not a delegated JWT). The agent never receives the CA private key.

curl -sS "$SIGID_IDP/api/v1/agents/delegations/ssh-sign" \
  -X POST \
  -H "authorization: Bearer $AGENT_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "credential_id": "018f3d44-aaaa-7d5d-8d3b-9f0c4c37aaaa",
    "target_host": "deploy.example.com",
    "principals": ["deploy"],
    "validity_seconds": 300
  }'

Request fields

Field Type Notes
credential_id string The ssh_ca credential id. Must be active and owned by the agent.
target_host string Host the certificate is valid for.
principals array of strings SSH principals (usernames) the certificate authorizes.
validity_seconds u64 Certificate lifetime. Default 300s (5 min), max 3600s (1 hour).

The certificate key_id is server-generated (sigid:<tenant>:<agent>); the caller does not supply it.

Response (shape)

{
  "certificate": "[email protected] AAAA...",
  "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----...",
  "public_key": "ssh-ed25519 AAAA...",
  "valid_until": "2026-08-02T12:05:00Z",
  "fingerprint": "SHA256:..."
}
Field Semantics
certificate The short-lived SSH certificate to present to the target host
private_key The ephemeral private key backing the certificate (for -i)
public_key The corresponding ephemeral public key
valid_until Certificate expiry timestamp
fingerprint SHA-256 fingerprint of the ephemeral public key

Using the certificate

Write the returned material to local files, then connect with ssh -i + CertificateFile (the response field comments above map directly to these flags):

printf '%s\n' "$CERTIFICATE" > agent-cert.pub
printf '%s\n' "$PRIVATE_KEY"  > agent-key && chmod 600 agent-key
ssh -i agent-key -o CertificateFile=agent-cert.pub [email protected]

The certificate authorizes only the principals you requested and expires at valid_until; reconnect after expiry by requesting a new signature.

This is the only SSH path exposed by the HTTP egress; it does not provide an ssh-agent socket signer. SSH key credentials (ssh_key type) are injected via /v1/proxy like any static credential; only ssh_ca credentials sign certificates here.

Token Exchange And Refresh

Vault list and egress require scopes your agent does not hold by default: vault:list (discover credentials) and vault:read (egress resolve). Both are in the agent allowlist, so there are two ways to obtain them.

Path A – request at challenge auth (direct token). Ask for the scopes when authenticating; the direct token then carries them:

sigid-cli auth --scope "vault:list vault:read"

Path B – exchange from an owner delegation (delegated token). Once an owner grants the agent a delegation that includes these scopes (one-time, via device-flow delegation or the Identity UI), the agent can mint a delegated token on demand – no human re-approval:

curl -sS "$SIGID_IDP/api/v1/agents/delegations/token" \
  -X POST \
  -H "authorization: Bearer $AGENT_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "scope": "vault:list vault:read" }'

The requested scope must be a subset of the delegation's scopes. See Delegation.

Refresh a delegated token via /oauth/token with grant_type=refresh_token (the refresh token comes from the exchange or the device-flow poll).

Keep the direct agent token (Path A; also used for SSH signing) and the delegated token (Path B) separate – do not overwrite one cache entry with the other.

Failure Modes

Symptom Cause What to do
GET /vault/credentials403 access_denied Missing vault:list scope, or no active grant for the credential Re-run device delegation requesting vault:list; have the owner grant the credential to your agent
GET /vault/credentials501 feature_disabled Vault service not configured on the tenant Contact operator to enable vault
POST /v1/proxy401 Missing Authorization header Send Authorization: Bearer <token>
POST /v1/proxy400 Bad request body: credential_id not a UUID, both body and body_base64 set, invalid method, non-HTTPS url, too many headers, or non-Bearer Authorization scheme Validate inputs against the request schema above
POST /v1/proxy429 Per-caller rate limit or in-flight concurrency cap on egress Back off; reduce request rate / concurrency
POST /v1/proxy502 Vault resolve failed: invalid/expired token, missing vault:read scope, no active grant, host not in allowed_hosts, auth_scheme mismatch, credential revoked, or internal error Verify token validity + scope + freshness, grant active, host allowed, auth_scheme correct (or omit it); check core logs
ssh-sign403 access_denied Used a delegated JWT or non-agent principal Re-authenticate as the agent directly (sigid-cli auth); SSH signing requires the direct agent token
ssh-sign400 Invalid credential_id, credential not ssh_ca, or validity_seconds > 3600 Use a ssh_ca credential id you own; keep validity ≤ 3600 (300 default for least privilege)
ssh-sign404 / vault not configured Vault service disabled Enable vault / contact operator

Security Checklist

  • Delegated token carries vault:list + vault:read; direct agent token kept separate for SSH signing
  • Each egress target host is covered by the credential's allowed_hosts
  • No bearer tokens, injected secrets, or full upstream response bodies logged
  • non_exportable credentials preferred – injection, not export
  • SSH validity_seconds kept to the minimum needed (default 300s)
  • Grants revoked when an agent no longer needs a credential
  • Egress calls respect per-caller rate limits (back off on 429)

See Also