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¶
- A list of vault credentials your agent is authorized to use, each identified
by a
credential_idand constrained to specific upstream hosts. - 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. - 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 carryingvault:list/vault:readand an active grant on the credential. SSH signing (ssh-sign) needs a direct agent token – it rejects delegated JWTs. - Never log bearer tokens,
credential_idis 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:
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/credentials → 403 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/credentials → 501 feature_disabled |
Vault service not configured on the tenant | Contact operator to enable vault |
POST /v1/proxy → 401 |
Missing Authorization header |
Send Authorization: Bearer <token> |
POST /v1/proxy → 400 |
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/proxy → 429 |
Per-caller rate limit or in-flight concurrency cap on egress | Back off; reduce request rate / concurrency |
POST /v1/proxy → 502 |
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-sign → 403 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-sign → 400 |
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-sign → 404 / 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_exportablecredentials preferred – injection, not export - SSH
validity_secondskept 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¶
- Agent Self-Serve Quickstart – authentication and delegated token that precedes this page
- Delegation – token exchange (RFC 8693) and refresh
- Agent And MCP Auth – agent integration overview