Use promo code BETATEST1 for full access
seekrit
Docs/Fly.io Sprites

Secrets in Fly.io Sprites

Sprites are Fly.io's persistent sandboxes: a hardware-isolated Firecracker microVM with a full Ubuntu userland and a 100 GB ext4 filesystem that survives hibernation. It sleeps when idle, wakes in 100–500 ms on the next request, and everything you installed is still there.

That persistence is the whole point of the product, and it is also what makes the secrets question different here than on every other page in this section. E2B, Modal, Daytona and Vercel Sandbox forget. A Sprite remembers — including whatever credential you left in it.

What persistence changes

Three things stop being true when the sandbox has a disk:

Ephemeral sandboxSprite
A value written to diskDies with the containerStays until you delete it, across every wake
RotationLands on the next sandbox you createNever lands — the Sprite you provisioned three weeks ago still holds the old value
SnapshotsNoneCheckpoints capture the whole filesystem, credentials included

The last one is the one that surprises people, so it gets its own section. The short version: restoring a checkpoint taken before a rotation puts the retired credential back on the machine, and it still works unless you revoked it.

The fix for all three is the same and it is the rule this page is built on: a Sprite should hold one credential — a scoped seekrit service token — and resolve everything else at process start.

Which shape to use

Inject at execResolve at service startBroker with a proxy
What the Sprite holdsThe values, for one commandOne scoped skt_ tokenOne scoped skt_ token, in a process the workload doesn't run
Where the values liveThat process's environmentThat process's environmentOnly inside the proxy, and in the outbound request
Rotation landsNext execNext cold boot or restartNext proxy restart
In a checkpointNothing, unless you snapshot mid-commandThe tokenThe token
Use whenOne-off commands from your machineA service you wrote — a dev server, a build, a training runThe Sprite runs an agent or model-generated code

Sprites' headline use case is running code you did not write, which means the third column is the common case, not the exotic one. Start there if the thing in the Sprite is a coding agent.

Inject at exec

For one-off commands driven from your machine, resolve on the host and pass the names you meant. Nothing touches the Sprite's disk:

export SEEKRIT_TOKEN=skt_…            # your token, on your machine

sprite exec --env "DATABASE_URL=$(seekrit secrets get DATABASE_URL --app storefront --env production)" \
  -- ./scripts/migrate.sh

Or the whole environment, for a command you trust, without writing a file:

seekrit run --app storefront --env production -- \
  sh -c 'sprite exec --env "DATABASE_URL=$DATABASE_URL,STRIPE_KEY=$STRIPE_SECRET_KEY" -- ./migrate.sh'

This is the standard sandbox shape: your process holds the token, the Sprite gets values and no ability to ask for more. It is also the shape that does not survive — the values are gone when the command exits, which is exactly right for a migration and useless for a server.

note

--env values land in the command's environment, readable at /proc/<pid>/environ by anything else running in the Sprite. That is fine when the Sprite is yours and busy with one job; it is not a boundary against another process in the same VM.

Resolve at service start

A Service is the only thing on a Sprite that reliably comes back: the runtime starts it at boot, restarts it when it crashes, and brings it up in dependency order after a cold wake. Which makes the service definition the natural place to put credentials — and the wrong one.

sprite-env services create … --env "STRIPE_KEY=sk_live_…" stores that value in the service definition, where it is replayed on every wake, printed by sprite-env services get, and captured by the next checkpoint. It is a .env file with extra steps.

Make the service command resolve instead:

sprite-env services create api \
  --cmd seekrit-run \
  --args "--env-file,/etc/seekrit.env,--cache,--,node,server.js" \
  --dir /home/sprite/app \
  --http-port 3000

seekrit-run authenticates with the token, fetches the environment's ciphertext, decrypts it inside the Sprite, and execs your command with the values set. Three things follow from putting it in the service command rather than in the service's --env:

  • Values never touch the disk. They exist in one process's memory, and that process is replaced by exec — no wrapper holding a copy.
  • Rotation lands on the next start. A cold boot restarts every service, so a Sprite that slept long enough to go cold picks up whatever is current when it comes back — and sprite-env services restart api forces it. You never have to find and re-provision the Sprite itself. (A warm wake resumes the process rather than restarting it, so a warm-only Sprite keeps what it resolved until something restarts the service.)
  • A checkpoint captures the token, not the values. Still a credential, but one you can scope and revoke — see the checkpoint section.

Bootstrapping the token

Write it from outside, once, with the filesystem API — never by echoing it into a command, which puts it in the exec session's argv:

seekrit token create --name sprite-api-42 --app storefront --env production
# prints:  skt_XXXXXXXX_…    (shown once)

curl -X PUT "https://api.sprites.dev/v1/sprites/api-42/fs/write?path=/etc/seekrit.env&workingDir=/" \
  -H "Authorization: Bearer $SPRITE_TOKEN" \
  --data-binary 'SEEKRIT_TOKEN=skt_XXXXXXXX_…'

sprite exec -s api-42 -- chmod 600 /etc/seekrit.env

And install the launcher into the Sprite once — it persists, so this is setup, not a boot step:

sprite exec -s api-42 -- sh -c 'curl -fsSL https://run.seekrit.dev/install.sh | sh'

A service with --http-port also takes the Sprite's URL, which is private to your Fly.io org by default and can be flipped to public with sprite config update --url-auth public. Flip it deliberately: a debug route that dumps process.env is harmless behind org auth and a published credential without it. Fly.io's own docs put it plainly — don't serve secrets, environment variables, or unrestricted filesystem access from a Sprite's HTTP service.

caution

One token, one Sprite, one environment. Mint it per Sprite so seekrit token revoke at sprite destroy is a complete cutoff, and never put an admin token on a Sprite — it is org-scoped and can mint more tokens. seekrit token list shows last-used time, so a Sprite you forgot to tear down is visible rather than silent.

Let seekrit through the egress policy

If you have tightened the Sprite's network policy, the DNS allowlist is default-deny once it exists, and a blocked lookup returns REFUSED. seekrit-run degrades gracefully rather than failing loudly, so a missing rule shows up as an app that starts without its credentials:

{
  "rules": [
    { "include": "defaults" },
    { "domain": "api.seekrit.dev", "action": "allow" }
  ]
}

The policy is read-only from inside the Sprite (/.sprite/policy/network.json), which is what makes it worth pairing with the broker shape below: an agent cannot widen its own egress.

tip

Keep --cache on for anything long-running. A Sprite wakes on a request and starts its services immediately; if that wake races a transient network problem, --cache falls back to the last encrypted response it saw instead of starting your server uncredentialed. A refused resolve (401/403) deletes the entry, so revoking the token still takes effect.

Broker for agent workloads

When the Sprite is running a coding agent, an eval harness, or anything built from model output, injection is the wrong shape for the usual reason: a process that can read os.environ can exfiltrate what it finds there. Run seekrit-proxy as its own service and give the agent placeholders.

seekrit-proxy reads its token from SEEKRIT_TOKEN, and a service's --env is the wrong place for it — a service definition is readable from inside the Sprite with sprite-env services get, which is exactly the process you are trying to keep the credential away from. Put the token-loading in a wrapper script instead:

#!/bin/bash
# /usr/local/bin/start-proxy — written once with the filesystem API, chmod +x.
set -a; . /etc/seekrit.env; set +a
exec seekrit-proxy --config /etc/seekrit-proxy.toml
# The proxy comes up first, on every cold boot.
sprite-env services create seekrit-proxy --cmd /usr/local/bin/start-proxy

# The agent starts after it, and holds no key.
sprite-env services create agent \
  --cmd python3 --args "agent.py" \
  --dir /home/sprite/agent \
  --needs seekrit-proxy \
  --env "OPENAI_BASE_URL=http://127.0.0.1:8080/openai/v1,OPENAI_API_KEY={{seekrit:OPENAI_API_KEY}}"

--needs is doing real work here: on a cold boot the runtime starts services in dependency order, so the agent never comes up against a proxy that is not listening yet.

note

--args splits on commas, so a shell one-liner cannot go there — it would be torn into fragments at every comma. A wrapper script sidesteps that; the Services REST API takes args as a real JSON array if you would rather keep it in your provisioning code.

Generate the config rather than writing it by hand — it contains secret names and no values, so it is safe to commit and safe to fs/write onto the Sprite:

seekrit proxy init --preset openai --preset anthropic

Then tighten it with methods and paths, so a legitimate credential still cannot reach an operation you did not grant.

Compose it with the egress policy

The two mechanisms answer different questions and they stack:

Sprites network policyseekrit-proxy allowlist
DecidesWhich hosts the Sprite may reach at allWhich secret may be injected toward which host, and for which methods and paths
Enforced byDNS, outside the Sprite, read-only from insideThe proxy process
Bypassable from insideNoThe agent can skip the proxy — and then reaches an allowed host with no credential

An agent that routes around the proxy gets an unauthenticated request. An agent that tries a host the policy does not allow gets REFUSED. Set the network policy to the upstreams your proxy fronts plus api.seekrit.dev, and the two together mean the only way out is through a rule you wrote.

caution

This is a weaker boundary than Cloudflare's outbound handlers, and the reason is the VM. On Cloudflare Sandbox the credential lives in the Worker, in a different process on a different machine from the container. Here the proxy is a process inside the same Sprite, and a workload with root can read another process's memory.

What it does buy, and it is most of the value: the agent's own environment, logs, crash dumps, and every filesystem checkpoint hold a placeholder rather than a key. It defeats prompt injection, a leaked transcript, and casual exfiltration. It does not defeat a determined attacker who already has root in the VM — for that, keep the credential off the Sprite entirely and put the proxy on a host you control.

Hold the Sprite up while the agent works

A service does not keep a Sprite awake; only traffic and the Tasks API do. An agent mid-turn on a Sprite that pauses loses its outbound connections. Register a task while it works and heartbeat it — the same thing Fly.io's own Managed Agents example does:

sprite-env curl -X POST /v1/tasks -d '{"name": "agent", "expire": "5m"}'
# … refresh every 60s while the agent runs, DELETE on exit

The short expiry is the safety net: an agent that crashes without cleaning up leaves a task that expires on its own, and the Sprite pauses rather than billing forever.

Checkpoints are backups of whatever you left lying around

A checkpoint captures the entire filesystem — every file, every permission, every installed package — so you can roll back a bad upgrade. It does not know which of those bytes are a credential.

That produces a failure mode with no analogue on an ephemeral sandbox:

  1. You put STRIPE_SECRET_KEY in a file on the Sprite in March.
  2. You checkpoint before an OS upgrade.
  3. You rotate the key in April, updating the file.
  4. In May something breaks and you sprite restore the March checkpoint.
  5. The March key is back on a running machine, and it works — because rotating a secret in seekrit changes what the next resolve returns. It does not reach into a filesystem snapshot, and it does not invalidate the old value at the provider.

Three rules follow:

  • Don't put values on the disk. Resolve at start, as above, and a restore brings back a launcher rather than a credential.
  • Revoke at the provider when you rotate, not just in seekrit. That is true everywhere; checkpoints just make the gap between "replaced" and "revoked" long enough to matter. See rotation.
  • Treat the bootstrap token as checkpointed. It is on the disk by design, so a restore resurrects it. Scope it to one environment, mint it per Sprite, and revoke it when the Sprite is destroyed — a resurrected token that was revoked in April fails closed.
note

Checkpoints capture in-memory state too, so /dev/shm and other tmpfs tricks are not an escape hatch. Assume anything present in the Sprite at checkpoint time is in the checkpoint.

Sprites Connectors, and why a seekrit token isn't one

Sprites has its own credential-brokering feature. Connectors store an OAuth token or API key in your Fly.io organization and route calls through api.sprites.dev/v1/gateway/<provider>/<connection_id>/<path>, identifying the calling Sprite from Fly.io's request signature. Deny-by-default, scoped by Sprite name prefix or labels, with endpoint allow- and block-lists.

Use it for what it covers. For GitHub and OpenRouter it is strictly better than putting a token in the Sprite, and every Sprite ships a sprite-api-gateway skill so an agent inside one discovers and uses connectors without being taught the URLs.

What it does not cover is everything that is not an HTTP API behind their gateway: a DATABASE_URL, object-storage credentials, a webhook signing secret, an SSH key, a license key, a provider they do not front. That is the half seekrit is for.

And there is one composition that looks appealing and does not work: wrapping seekrit as a Custom API connector so the Sprite never holds SEEKRIT_TOKEN. A seekrit service token is skt_<id>_<pkcs8> — it embeds a private key, not just a bearer credential. /v1/resolve returns ciphertext and wrapped data keys that only that key can open. Broker it and the gateway ends up holding the key material while the Sprite receives ciphertext it cannot decrypt. The token has to be where the decryption happens, which is the point of the zero-knowledge model rather than a limitation of it.

The division that does work:

Reach for
GitHub, OpenRouter, an API you wrap as a Custom API connectorSprites Connectors
Databases, storage, webhooks, SSH, anything not an HTTP APIseekrit + seekrit-run
An agent that must not hold any key, for any of the aboveseekrit-proxy, behind the network policy

Worked example: Claude Managed Agents

Fly.io's Claude Managed Agents integration runs each agent session in its own Sprite. Its worker writes the Anthropic credentials to /root/runner.env, and the service command sources the file and deletes it:

set -a; . /root/runner.env; set +a; rm -f /root/runner.env;

That is a hand-rolled seekrit-run, and it is careful for good reasons — the key stays out of process listings and off the disk after boot. Two things it cannot do: survive a cold boot (the file is gone, so a restarted service sources nothing and the runner comes up without its credential), and pick up a rotated environment key without the worker re-provisioning the Sprite.

Note which of those five variables actually needs protecting. ANTHROPIC_WORK_ID and ANTHROPIC_SESSION_ID are per-session identifiers, not credentials, and they belong in the service definition. ANTHROPIC_ENVIRONMENT_KEY is the long-lived one, and it is the one to move into seekrit:

c.put(f"/v1/sprites/{name}/fs/write",
      params={"path": "/etc/seekrit.env", "workingDir": "/"},
      content=f"SEEKRIT_TOKEN={session_token}\n".encode()).raise_for_status()

cmd = (
    "sprite-env curl -X POST /v1/tasks -d '{\"name\": \"agent-runner\", \"expire\": \"5m\"}' >/dev/null 2>&1; "
    "( while true; do sprite-env curl -X PUT /v1/tasks/agent-runner -d '{\"expire\": \"5m\"}' >/dev/null 2>&1; sleep 60; done ) & heartbeat=$!; "
    # ANTHROPIC_ENVIRONMENT_KEY (and BASE_URL) arrive from seekrit, decrypted here.
    f"seekrit-run --env-file /etc/seekrit.env --cache -- python3 {RUNNER_PATH}; "
    "kill \"$heartbeat\" 2>/dev/null || true; "
    "sprite-env curl -X DELETE /v1/tasks/agent-runner >/dev/null 2>&1 || true; "
    "sprite-env services stop agent-runner >/dev/null 2>&1 || true")

c.put(f"/v1/sprites/{name}/services/agent-runner",
      json={
          "cmd": "bash",
          "args": ["-lc", cmd],
          "needs": [],
          # Identifiers, not secrets — fine in the definition.
          "env": {
              "ANTHROPIC_SESSION_ID": session_id,
              "ANTHROPIC_ENVIRONMENT_ID": environment_id,
              "ANTHROPIC_WORK_ID": work_id,
          },
      }).raise_for_status()

The environment key now lives in one place instead of being copied into every session's Sprite, rotating it is one seekrit secrets set, and a cold-boot restart re-resolves rather than starting a runner with nothing. The Sprite is per-session and destroyed at the end, so mint session_token per session and revoke it in the same teardown that calls DELETE /v1/sprites/{name}.

Agents managing secrets from inside a Sprite

A Sprite ships with Claude Code, Codex, Cursor and Gemini preinstalled, so the agent working in one can also manage seekrit itself. Give it the metadata plane and it can create applications and environments, list secret names, and audit grants without ever holding a value:

npx plugins add seekritdev/agent-plugin

The hosted MCP server at mcp.seekrit.dev registers no tool that returns a secret value — a test enforces the split — so an agent connected to it can reorganize your secrets and cannot read one. Values stay on the local crypto plane (seekrit mcp), which is the same boundary this page draws everywhere else.

Teardown

A Sprite you forget is a credential you forget. Both halves:

sprite destroy -s api-42
seekrit token revoke skt_XXXXXXXX

Checklist

  • The Sprite holds one credential — a scoped skt_ token, never an admin token
  • Token written with the filesystem API, chmod 600, not echoed through exec
  • Service command is seekrit-run -- …, not a service --env full of values
  • --cache on, so a wake that races the network doesn't start uncredentialed
  • api.seekrit.dev allowed in the network policy, if one is set
  • Agent workload? Proxy as a service, --needs ordering, placeholders in --env
  • No checkpoint taken with plaintext values on the disk
  • URL left on --url-auth sprite unless it genuinely needs to be public
  • Token revoked in the same step that destroys the Sprite

See also