Example: jet identity from a response-wrapped Vault token - #168
Draft
linuskendall wants to merge 4 commits into
Draft
Example: jet identity from a response-wrapped Vault token#168linuskendall wants to merge 4 commits into
linuskendall wants to merge 4 commits into
Conversation
Sample showing how jet could ingest a single-use, response-wrapped Vault token provided by its Nomad job and unwrap it itself to obtain its identity keypair in memory: - read the wrapping token from the file the Nomad template rendered - optionally verify the token's creation_path via sys/wrapping/lookup (non-consuming) to detect a substituted token - unwrap via sys/wrapping/unwrap, authenticated with the wrapping token itself; a failed unwrap means expiry or interception since the token is single use - parse the keypair field (id.json byte array or base58) into a solana_keypair::Keypair Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTR2h1D8F2iRswWoGhp3FS
Replace the hand-rolled reqwest calls with the vaultrs crate, which provides the wrapping endpoints natively: - vaultrs::sys::wrapping::lookup for the non-consuming creation_path tamper check - vaultrs::sys::wrapping::unwrap for the single-use unwrap, with the wrapping token as the client token (no other Vault credentials) - ClientError::APIError code 400 detected explicitly to surface the already-unwrapped/interception case Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTR2h1D8F2iRswWoGhp3FS
The Nomad job exports the response-wrapped token through a
template { env = true } stanza, so the token never lands on disk:
- VaultIdentityConfig::wrapped_token_file becomes wrapped_token_env,
carrying the name of the variable to read
- empty value is rejected explicitly, since an unrendered template
otherwise surfaces as a confusing 403 from Vault
- note that the token stays visible in /proc/self/environ, and that
scrubbing it needs unsafe in edition 2024 and is only sound before
any thread starts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTR2h1D8F2iRswWoGhp3FS
load_identity_from_wrapped_token now takes no arguments and reads everything from the environment: - VAULT_WRAPPED_TOKEN for the single-use wrapped token - VAULT_ADDR left to vaultrs, whose client builder already reads it and falls back to http://127.0.0.1:8200 - VAULT_EXPECTED_CREATION_PATH to opt into the tamper check Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTR2h1D8F2iRswWoGhp3FS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sample program showing how jet could obtain its identity keypair from a single-use, response-wrapped Vault token handed to it by a Nomad job, and unwrap it itself.
This is a standalone example for discussion — it changes no jet code. The only change outside
examples/is one line adding the example to the workspaceexcludelist, matching howexamples/jet-tpu-senderis treated.Why response wrapping
With Vault response wrapping, the keypair never appears in the Nomad job spec, template output, or environment. The task receives only a wrapping token — a reference to a single-use cubbyhole holding the real secret:
sys/wrapping/unwrapsucceeds exactly once. If jet's own unwrap fails, either the token expired or somebody else already unwrapped it, which is a built-in interception alarm rather than a silent compromise.wrap_ttlbounds the exposure window.sys/wrapping/lookup(which does not consume the token) exposes the token'screation_path, so jet can refuse a token that was not minted by the expected Vault endpoint before spending the single use.What's here
examples/jet-vault-identity/src/main.rs— the whole flow is one function,load_identity_from_wrapped_token, built on thevaultrscrate, which ships the wrapping endpoints natively (vaultrs::sys::wrapping::{lookup, unwrap}):$VAULT_WRAPPED_TOKEN, where a Nomadtemplate { env = true }stanza rendered it, so it never lands on disk;creation_pathviasys/wrapping/lookup;sys/wrapping/unwrap, authenticated with the wrapping token itself — jet holds no other Vault credential;keypairfield into asolana_keypair::Keypairthat only ever lives in memory.In jet proper this is what
mainwould call to produceinitial_identitybefore handing it toJetIdentitySyncGroup, in place ofconfig.identity.keypair.unwrap_or(Keypair::new())atapps/jet/src/bin/jet.rs:321.The function takes no arguments; everything comes from the environment:
VAULT_ADDRhttp://127.0.0.1:8200VAULT_WRAPPED_TOKENVAULT_EXPECTED_CREATION_PATHexamples/jet-vault-identity/README.mdcovers the producer side: the Vault KV layout, avault kv get -wrap-ttl=300sexample, and a short Nomadtemplatesketch (deliberately a sketch, not a full job spec).Testing
Verified end-to-end against a mock Vault implementing the two wrapping endpoints with single-use semantics:
VAULT_WRAPPED_TOKEN→ names the missing variableVAULT_WRAPPED_TOKEN→ points at the unrendered Nomad template (an unrendered template yields an empty string, which would otherwise surface as an opaque 403 from Vault)VAULT_ADDRunset → falls back to127.0.0.1:8200; set → honouredVAULT_EXPECTED_CREATION_PATH→ refuses before consuming the tokencargo build,cargo clippy(0 warnings) andcargo +nightly fmt --checkunder the repo'srustfmt.tomlare all clean. Note that CI does not build this example, since workspace-excluded members are outsidecargo build/clippy --all-targets— same as the existingjet-tpu-senderexample.Open questions
VAULT_EXPECTED_CREATION_PATHis optional and inert unless set. Happy to drop it if the extra knob isn't wanted.parse_keypairaccepts the solanaid.jsonbyte array (as JSON or a stringified array) and base58, because how the keypair gets into Vault varies by operator. If we standardize onvault kv put ... keypair=@id.json, only the array arm ever fires and the rest can go./proc/self/environfor the life of the process. Scrubbing it needs anunsafeblock in edition 2024 and is only sound before any thread starts. A file inNOMAD_SECRETS_DIR(tmpfs) trades that for a path that can be read once and unlinked — worth a decision either way.ConfigIdentityas a new identity source would be the natural follow-up if the approach looks right.Generated by Claude Code