Skip to main content

1Password Credential Injection

In Secrets Without Exposure you stored credentials in your OS keychain. This module shows a stronger pattern: keep your secrets in 1Password and let sbx inject them at the network boundary. The real secret never enters the sandbox - the agent only ever sees a sentinel value.


Why this mattersโ€‹

  • Your API keys stay in 1Password, the vault you already trust.
  • The sandbox gets a placeholder (proxy-managed), never the real token.
  • The proxy swaps the placeholder for the real key only on recognized outbound HTTPS calls - and only for domains you allow.

Prerequisitesโ€‹

  • Docker Sandboxes (sbx) installed - see Pre-flight Checklist
  • A 1Password account with the desktop app installed and unlocked
  • An agent CLI (Claude, Codex, Gemini, etc.)

Step 1 - Install the 1Password CLIโ€‹

macOS
brew install --cask 1password-cli
Linux (Debian/Ubuntu)
# After adding the 1Password apt repo
sudo apt install 1password-cli
Windows
winget install AgileBits.1Password.CLI

Verify:

op --version

Step 2 - Authenticateโ€‹

Recommended - desktop integration:

  1. Open and unlock the 1Password desktop app.
  2. Go to Settings โ†’ Developer.
  3. Enable Integrate with 1Password CLI.
  4. (Optional) Enable biometric unlock.

Alternative - manual sign-in:

eval $(op signin)

Confirm your session:

op whoami

You should see your account URL, email, and user ID. If it says "not currently signed in," authentication failed - fix it before continuing.


Step 3 - Find your secret referenceโ€‹

References use the format op://<vault>/<item>/<field>.

# List your vaults
op vault list

# List items in a vault
op item list --vault Employee

# Inspect an item to find the field name (don't assume "token")
op item get "OpenAI API Key" --vault Employee
Field names matter

API Credential items store the secret in a field named credential, not token.

Avoid special characters in references

A reference like op://Employee/OpenAI API Key (docker work)/credential will fail:

ERROR: invalid character in secret reference: '('

Instead: rename the item to a clean handle, use its 26-character UUID, or copy the reference directly from the desktop app.


Choose an injection patternโ€‹

Pattern 1 - Persistent (reuse across future sandboxes)โ€‹

set -o pipefail
op read "op://Employee/OpenAI/credential" | sbx secret set -g openai

Update an existing secret:

sbx secret rm -g openai
op read "op://Employee/OpenAI/credential" | sbx secret set -g openai

Verify:

sbx secret ls

Pattern 2 - Ephemeral (fresh per launch)โ€‹

The key resolves fresh each run and is cleared on exit - never stored.

OPENAI_API_KEY="op://Employee/OpenAI/credential" op run -- sbx run codex
ANTHROPIC_API_KEY="op://Employee/Anthropic/credential" op run -- sbx run claude

Pattern 3 - Multiple providers via env fileโ€‹

Create .sbx-secrets.env:

ANTHROPIC_API_KEY=op://Employee/Anthropic/credential
OPENAI_API_KEY=op://Employee/OpenAI/credential

Launch with the file:

op run --env-file=.sbx-secrets.env -- sbx run claude
Add it to .gitignore

.sbx-secrets.env holds credential references. They aren't the secrets themselves, but keep the file out of git anyway.


Step 4 - Prove containmentโ€‹

The sandbox should never see your real key:

sbx run --name op-test shell -d
sbx exec op-test -- bash -lc 'echo "OPENAI_API_KEY=$OPENAI_API_KEY"'
sbx rm op-test

Inside the sandbox the value shows proxy-managed (the sentinel) - never your real key.


Step 5 - Enable injection via domain bindingโ€‹

Back up your config first:

cp ~/.config/sbx/credentials.yaml ~/.config/sbx/credentials.yaml.bak

Edit ~/.config/sbx/credentials.yaml and add under bindings::

openai:
discovery: []
allowedDomains:
- api.openai.com

Start a fresh sandbox (existing ones cache the old config) and verify the proxy injects the real key on an allowed outbound call:

sbx run --name inj-test shell -d
sbx exec inj-test -- bash -lc \
'curl -s -o /dev/null -w "%{http_code}\n" https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY"'
sbx rm inj-test

A 200 confirms the proxy swapped the sentinel for your real key at the network boundary.


Troubleshootingโ€‹

ErrorCauseFix
You are not currently signed inNo live op sessioneval $(op signin) or enable desktop integration; check op whoami
Enter secret: input cannot be emptyop read failed upstreamAdd set -o pipefail; fix the auth issue
isn't an item in the ... vaultVault doesn't exist on your accountRun op vault list, use your real vault
invalid character in secret reference: '('Hand-built reference with special charsRename item, use UUID, or copy from app
not injecting warningService has no domain bindingAdd an allowedDomains entry in credentials.yaml

How it worksโ€‹

The sandbox runs a credential proxy that intercepts outbound calls. Your secret stays in 1Password on the host and is retrieved only when needed. Inside the container it exists only as a sentinel (proxy-managed); the proxy swaps it for the real value at the network boundary - and only for recognized providers with an allowedDomains binding.


โœ… Checkpointโ€‹

Confirm:

  • op whoami shows an active 1Password session
  • sbx secret ls lists your stored credential (Pattern 1) or your env file resolves at launch (Patterns 2 & 3)
  • Inside a sandbox, $OPENAI_API_KEY reads proxy-managed, not your real key
  • An allowed outbound call returns 200

Next: controlling what the agent can reach on the network in Network Policy.