Secrets in Modal
Modal's unit of environment is a modal.Secret, and the interesting one here is
Secret.from_dict() — it is built where your code runs, not stored in
Modal's dashboard. That makes Modal a particularly clean fit: seekrit stays the
single source of truth, and Modal holds the values only for as long as the
function or sandbox that received them.
Inject into a Function at deploy time
import os
import modal
import seekrit
secrets = seekrit.Client(token=os.environ["SEEKRIT_TOKEN"]).resolve()
app = modal.App("storefront-agent")
@app.function(
secrets=[
modal.Secret.from_dict(
{
"OPENAI_API_KEY": secrets["OPENAI_API_KEY"],
"TAVILY_API_KEY": secrets["TAVILY_API_KEY"],
}
)
],
)
def run_agent(prompt: str) -> str:
import os
# The keys are ordinary environment variables in here.
...
Secret.from_dict() runs on your machine (or in CI) when you modal deploy, so
the values travel with that deployment. Named keys, not the whole secrets dict
— see Pick names, not the whole
environment.
This binds values at deploy time. A rotated secret does not reach a deployed Modal function until you deploy again. That is the honest trade for keeping Modal out of the loop; if you need rotation to land without a deploy, read the proxy section below, where the function holds no key at all.
Inject into a Sandbox
Sandboxes take the same secrets= list, and this is the more interesting case
because a sandbox is usually running something you trust less than your own
function body:
sb_app = modal.App.lookup("agent-sandboxes", create_if_missing=True)
sandbox = modal.Sandbox.create(
app=sb_app,
secrets=[modal.Secret.from_dict({"OPENAI_API_KEY": secrets["OPENAI_API_KEY"]})],
)
process = sandbox.exec("python", "-c", "import os; print(bool(os.environ['OPENAI_API_KEY']))")
print(process.stdout.read())
Why there is no Modal sync connector
Every other agent-hosting page under
third-party sync exists because the platform has
a secrets API seekrit's servers can call. Modal does not. Its control plane
at api.modal.com speaks gRPC and nothing else — secrets are managed through the
dashboard, the modal secret CLI, or the Python/JS/Go SDKs, all of which talk
that same gRPC surface. There is no REST endpoint to push to.
That turns out to be the right answer anyway. Sync means seekrit decrypting your
environment on its own servers, which seekrit's
zero-knowledge rule permits only where no client
exists at the moment values are needed. With Modal a client always exists: your
modal deploy, or your code calling Sandbox.create. Secret.from_dict() keeps
decryption on your side, which is strictly better than a connector would have
been.
Keep the credential outside the sandbox
For untrusted code, do not inject at all. Run
seekrit-proxy where the sandbox cannot reach into
it — the machine that created the sandbox — and give the sandbox a placeholder
and a URL:
# On the host, beside the code that calls Sandbox.create.
docker run --rm -e SEEKRIT_TOKEN=skt_… \
-v "$PWD/seekrit-proxy.toml:/seekrit-proxy.toml" \
-p 8080:8080 seekritdev/proxy --listen 0.0.0.0:8080
# seekrit-proxy.toml
[[route]]
prefix = "/openai"
upstream = "https://api.openai.com"
allow = ["OPENAI_API_KEY"]
methods = ["POST"]
paths = ["/v1/chat/completions", "/v1/embeddings"]
sandbox = modal.Sandbox.create(
app=sb_app,
secrets=[
modal.Secret.from_dict(
{
# A placeholder, not a key. Worthless if it leaks.
"OPENAI_API_KEY": "{{seekrit:OPENAI_API_KEY}}",
"OPENAI_BASE_URL": "https://proxy.internal.example/openai",
}
)
],
)
The agent's SDK sends Authorization: Bearer {{seekrit:OPENAI_API_KEY}}; the
proxy swaps in the real value and forwards it. The sandbox holds no seekrit
token, so there is nothing in it to steal — and the paths list means the key
cannot be spent on anything but chat completions and embeddings even by code that
finds the endpoint.
Do not run the proxy inside the same sandbox as untrusted code. The proxy
needs SEEKRIT_TOKEN, and a Modal sandbox is one process space — an agent
running as the same user can read it out of the proxy's environment, which
hands over the whole environment rather than the two keys you meant to share.
The boundary only holds when the token is somewhere the agent cannot read.
See also
- Agent sandboxes — the two shapes and when each is right
- Agent proxy — the full proxy configuration
- Notebooks —
seekrit.load(), if you are driving Modal from a notebook