1Password for AI agents, on TEE.
Jingui ensures that secrets (API keys, OAuth tokens, credentials) are delivered to TEE workloads without the application—or any of its dependencies—ever having direct access to plaintext values outside the encrypted channel.
Why I built Jingui — the problem with AI agents and API keys, and how TEEs help.
┌─────────────┐ challenge/response ┌─────────────────┐
│ jingui │◄────────── ECIES (X25519) ────────►│ jingui-server │
│ (TEE client)│ │ (management) │
└──────┬───────┘ └─────────────────┘
│
│ env vars + stdout/stderr masking
▼
┌─────────────┐
│ your app │ ← secrets injected, ptrace blocked
└─────────────┘
- Server stores secrets in vaults and manages TEE instance access.
- Client runs inside the TEE, proves possession of its private key via a challenge-response protocol, receives secrets encrypted to its public key, decrypts them, and injects them as environment variables into the target process.
- Lockdown — on Linux/amd64, the child process is hardened with seccomp filters that block
ptraceandprocess_vm_readv, plusPR_SET_DUMPABLE=0. - Output masking — all secret values are redacted from stdout/stderr using Aho-Corasick multi-pattern matching.
Deploy on dstack (e.g. via Phala Cloud) with the provided docker-compose.yml. Configure JINGUI_MASTER_KEY and JINGUI_ADMIN_TOKEN as encrypted environment variables.
| Variable | Required | Default | Description |
|---|---|---|---|
JINGUI_MASTER_KEY |
Yes | — | Master encryption key for secrets at rest |
JINGUI_ADMIN_TOKEN |
Yes | — | Bearer token for admin APIs (min 16 chars) |
JINGUI_DB_PATH |
No | jingui.db |
SQLite database path |
JINGUI_LISTEN_ADDR |
No | :8080 |
Listen address |
JINGUI_CORS_ORIGINS |
No | — | Comma-separated allowed CORS origins |
JINGUI_RATLS_STRICT |
No | true |
Require client/server attestation exchange in challenge/fetch flow |
JINGUI_LOG_LEVEL |
No | info |
Log level (debug,info,warn,error) |
Create a .env file with secret references:
GMAIL_CLIENT_ID=jingui://my-gmail/alice@example.com/client_id
GMAIL_CLIENT_SECRET=jingui://my-gmail/alice@example.com/client_secret
GMAIL_REFRESH_TOKEN=jingui://my-gmail/alice@example.com/refresh_token
DATABASE_URL=postgres://localhost/mydbRun your application:
jingui run --server https://jingui.example.com -- python app.pyCheck local instance status and registration:
jingui status --server https://jingui.example.comRead one secret (metadata is hidden by default):
jingui read --server https://jingui.example.com 'jingui://my-gmail/alice@example.com/client_id'
# use --show-meta to print FID/Public Key to stderr for debuggingLines with jingui:// (or op://) URIs are fetched and decrypted; plain values pass through unchanged.
| Flag | Default | Description |
|---|---|---|
--server |
JINGUI_SERVER_URL env |
Server URL (required) |
--appkeys |
/dstack/.host-shared/.appkeys.json |
Path to X25519 private key file |
--env-file |
.env |
Environment file with secret refs |
--insecure |
false |
Allow plaintext HTTP |
--no-lockdown |
false |
Disable seccomp hardening |
--verbose |
false |
Enable verbose debug logs (same as --log-level debug) |
--log-level |
JINGUI_LOG_LEVEL / info |
Log level (debug,info,warn,error) |
jingui read also supports --show-meta to print FID/Public Key to stderr when debugging.
RA-TLS strict client knobs:
JINGUI_RATLS_STRICT(defaulttrue)JINGUI_RATLS_EXPECT_SERVER_APP_ID(optional pin; when set, server attestation app_id must match)JINGUI_LOG_LEVEL=debug(or--verbose) to print RA verification measurements (MR/RTMR/TCB status)
jingui://<vault>/<item>/<field_name>
jingui://<vault>/<item>/<section>/<field_name>
op:// is also accepted as an alias (1Password CLI compatible):
op://<vault>/<item>/<field_name>
op://<vault>/<item>/<section>/<field_name>
<vault>— app/service namespace (e.g.my-gmail)<item>— item within the vault (e.g.alice@example.com)<section>— optional subsection (e.g.oauth)<field_name>— field within the secret object (e.g.client_id)
Examples:
jingui://my-gmail/alice@example.com/client_idop://my-gmail/alice@example.com/client_secretjingui://my-gmail/alice@example.com/refresh_tokenjingui://my-gmail/alice@example.com/oauth/access_token(4-segment)
- In transit — ECIES (X25519 + AES-256-GCM). Secrets are encrypted to the TEE instance's public key.
- At rest — AES-256-GCM with the server master key.
- Proof of possession — before returning secrets, the server issues a nonce encrypted to the TEE's public key. Only the holder of the matching private key can decrypt and respond.
- Process isolation — seccomp BPF blocks ptrace/process_vm_readv;
PR_SET_DUMPABLE=0prevents core dumps. - Output redaction — Aho-Corasick streaming replacement masks leaked values in stdout/stderr.
RA-TLS attestation verification requires the dcap-qvl static library (built from Rust). CI builds it automatically; for local development see scripts/build-dcap-qvl.sh or the Dockerfile.
make build # current platform (requires dcap-qvl)
make build-all # cross-compile all 8 binaries (2 × 4 platforms)
make ci # lint + test + bddDocker:
docker build --target server -t jingui-server .
docker build --target client -t jingui .
docker build -t jingui-web web/- OpenAPI JSON:
/openapi.json(also committed asdocs/openapi.json) - Database schema:
docs/schema.md
Admin endpoints (require Authorization: Bearer <ADMIN_TOKEN>):
| Method | Path | Description |
|---|---|---|
| POST | /v1/vaults |
Create a vault |
| GET | /v1/vaults |
List vaults |
| GET | /v1/vaults/:id |
Get vault |
| PUT | /v1/vaults/:id |
Update vault name |
| DELETE | /v1/vaults/:id |
Delete vault (?cascade=true to delete items + access grants) |
| Method | Path | Description |
|---|---|---|
| GET | /v1/vaults/:id/items |
List sections in a vault |
| GET | /v1/vaults/:id/items/:section |
Get field keys for a section |
| PUT | /v1/vaults/:id/items/:section |
Upsert/delete fields ({fields: {k:v}, delete: [k]}) |
| DELETE | /v1/vaults/:id/items/:section |
Delete all fields in a section |
| Method | Path | Description |
|---|---|---|
| GET | /v1/vaults/:id/instances |
List instances with access to this vault |
| POST | /v1/vaults/:id/instances/:fid |
Grant instance access to vault |
| DELETE | /v1/vaults/:id/instances/:fid |
Revoke instance access to vault |
FID (Fingerprint ID) = hex(SHA1(public_key)) — a 40-char hex identifier derived from the instance's X25519 public key.
| Method | Path | Description |
|---|---|---|
| POST | /v1/instances |
Register a TEE instance (public key + dstack_app_id) |
| GET | /v1/instances |
List all instances |
| GET | /v1/instances/:fid |
Get instance details |
| PUT | /v1/instances/:fid |
Update dstack_app_id and label |
| DELETE | /v1/instances/:fid |
Delete an instance |
Per vault+instance pair control over jingui read.
| Method | Path | Description |
|---|---|---|
| GET | /v1/debug-policy/:vault/:fid |
Get debug-read policy (defaults to allow) |
| PUT | /v1/debug-policy/:vault/:fid |
Set allow_read for vault+instance |
Client endpoints (no admin auth):
| Method | Path | Description |
|---|---|---|
| POST | /v1/secrets/challenge |
Request proof-of-possession challenge |
| POST | /v1/secrets/fetch |
Fetch encrypted secrets (after challenge) |
- App keys path:
/dstack/.host-shared/.appkeys.jsonis the default location for the X25519 private key file, determined by the dstack runtime environment. - Key format: X25519 (Curve25519) key pairs; ECIES encryption uses X25519 + AES-256-GCM.
dstack_app_id: Application identity from the dstack attestation chain, used for RA-TLS verification during the challenge/fetch flow.
A basic web UI (web/) is included for trial use. It runs as a separate container (ghcr.io/leechael/jingui-web) — see docker-compose.yml. Set JINGUI_CORS_ORIGINS on the server to allow cross-origin requests.
See LICENSE for details.