Guides
Use your model API key
Store your model provider's API key as a Temper secret so the agent can call the model without the real key ever entering the VM.
Your agent needs a model API key, but the VM is the one place it should not be. An agent that reads a prompt-injected web page or email could be told to print the key or send it somewhere. With Temper you store the key as a secret:
- The real value stays in the control plane (encrypted) and the credential gateway on the host. No API returns it.
- The agent gets a stand-in token (
tmpr_...) in a file and uses it like a normal key. - The gateway swaps the stand-in for the real key only on requests to the hosts you list. Anywhere else, the stand-in is useless.
This guide uses OpenRouter; the same steps work for any provider that takes the key in a request header.
Store the key
Store a developer-level secret when one key serves all your end users: every environment gets it. Give it a name in the style of
an environment variable (uppercase letters, digits and _, starting with a letter) and list the exact hosts the key may be sent to.
import { Temper } from "@temper-hq/sdk";
const temper = new Temper();
const secret = await temper.secrets.set("OPENROUTER_API_KEY", {
value: process.env.MODEL_API_KEY!,
hosts: ["openrouter.ai"], // only sent to these hosts; by default replaces the authorization header with prefix "Bearer "
});
console.log(`Stored ${secret.name} for ${secret.hosts.join(", ")}. The value is never returned.`);import os
from temper_hq import Temper
temper = Temper()
secret = temper.secrets.set(
"OPENROUTER_API_KEY",
value=os.environ["MODEL_API_KEY"],
hosts=["openrouter.ai"], # only sent to these hosts; by default replaces the authorization header with prefix "Bearer "
)
print(f"Stored {secret.name} for {', '.join(secret.hosts)}. The value is never returned.")package main
import (
"context"
"fmt"
"log"
"os"
"strings"
temper "github.com/temper-hq/sdk-go"
)
func main() {
ctx := context.Background()
client, err := temper.New()
if err != nil {
log.Fatal(err)
}
secret, err := client.Secrets.Set(ctx, "OPENROUTER_API_KEY", temper.PutSecretParams{
Value: os.Getenv("MODEL_API_KEY"),
Hosts: []string{"openrouter.ai"}, // only sent to these hosts; by default replaces the authorization header with prefix "Bearer "
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Stored %s for %s. The value is never returned.\n", secret.Name, strings.Join(secret.Hosts, ", "))
}Calling set again with the same name replaces the value and settings. The stand-in stays the same, so the agent doesn't need to
re-read anything.
Options
| Field | Default | Meaning |
|---|---|---|
value | required | The real key. Never returned by any endpoint. Must not contain a newline. |
hosts | required | Exact host names the key may be sent to. Wildcards are not accepted. |
paths | any path | Path prefixes (starting with /) the key may be sent to on those hosts. |
header | authorization | The request header that carries the key. |
prefix | "Bearer " for authorization, empty for other headers | Text before the key in that header. |
For a provider that takes the key in a custom header, set header. Anthropic, for example, uses x-api-key with no prefix:
await temper.secrets.set("ANTHROPIC_API_KEY", {
value: process.env.ANTHROPIC_API_KEY!,
hosts: ["api.anthropic.com"],
header: "x-api-key", // prefix defaults to empty for a custom header
});temper.secrets.set(
"ANTHROPIC_API_KEY",
value=os.environ["ANTHROPIC_API_KEY"],
hosts=["api.anthropic.com"],
header="x-api-key", # prefix defaults to empty for a custom header
)_, err = client.Secrets.Set(ctx, "ANTHROPIC_API_KEY", temper.PutSecretParams{
Value: os.Getenv("ANTHROPIC_API_KEY"),
Hosts: []string{"api.anthropic.com"},
Header: "x-api-key", // Prefix nil = default, which is empty for a custom header
})One key per end user
If each end user brings their own key, or you keep separate keys per customer, store an environment-level secret instead. It applies only to that environment and, when the name is the same, overrides the developer-level secret.
await temper.secrets.setForEnvironment(envId, "OPENROUTER_API_KEY", {
value: usersOwnKey,
hosts: ["openrouter.ai"],
});temper.secrets.set_for_environment(
env_id,
"OPENROUTER_API_KEY",
value=users_own_key,
hosts=["openrouter.ai"],
)_, err = client.Secrets.SetForEnvironment(ctx, envID, "OPENROUTER_API_KEY", temper.PutSecretParams{
Value: usersOwnKey,
Hosts: []string{"openrouter.ai"},
})Deleting the environment-level secret (secrets.deleteForEnvironment, Python delete_for_environment, Go DeleteForEnvironment)
puts the environment back on the developer-level secret of the same name, if there is one. See the secrets API for
listing and deleting.
Allow the requests in your policy
Requests that use a secret go through your action policy like any other credential, with the connector
secret:<NAME>. GET, HEAD and OPTIONS requests count as read; everything else, including the POST that calls the model, counts
as write. A request that no rule allows is refused, so add a rule that allows the model calls:
const current = await temper.policies.getDefault();
await temper.policies.setDefault([
...current.rules,
{ connector: "secret:OPENROUTER_API_KEY", effect: "allow" },
]);current = temper.policies.get_default()
temper.policies.set_default([
*current.rules,
{"connector": "secret:OPENROUTER_API_KEY", "effect": "allow"},
])current, err := client.Policies.GetDefault(ctx)
if err != nil {
log.Fatal(err)
}
rules := append(current.Rules, temper.PolicyRule{Connector: "secret:OPENROUTER_API_KEY", Effect: "allow"})
if _, err := client.Policies.SetDefault(ctx, rules); err != nil {
log.Fatal(err)
}setDefault replaces the whole policy, which is why the example keeps the existing rules. A broad rule such as
{ category: "write", effect: "ask" } also matches model calls, and deny and ask win over allow, so check that your other rules don't
make every model call wait for approval. An environment with its own policy uses that instead of the default; see
approvals.
Use the stand-in in the agent
Inside the VM, the stand-in is in a read-only file at /run/temper/secrets/<NAME>. Read it and send it exactly as you would send the
real key:
# Inside the VM (your agent's code)
export OPENROUTER_API_KEY=$(cat /run/temper/secrets/OPENROUTER_API_KEY)
curl -s https://openrouter.ai/api/v1/models -H "Authorization: Bearer $OPENROUTER_API_KEY"Most model SDKs read the key from an environment variable, so setting that variable from the file is usually all the agent needs. Read the file when the agent starts rather than baking the stand-in into your agent package.
When the request leaves the VM, the gateway finds the stand-in, checks that it belongs to this environment and that the host (and
path, if you set paths) is in range, and replaces the header with the real key. For that to work:
- Send the stand-in in the configured header, with the configured prefix (by default
Authorization: Bearer <stand-in>). - A header that contains a stand-in Temper can't match, or a stand-in on a host outside its list, gets the whole request refused. A stand-in is never forwarded as is.
- The real key never appears in the VM, so it can't leak from there.
Egress is allowed automatically
The VM can only reach hosts on its egress allowlist. A secret's hosts are added to the allowlist of every
environment that has the secret, so you don't need to list openrouter.ai separately. To let the agent reach other hosts, such as a
package index, set the environment's allowlist:
const egress = await temper.secrets.setEnvironmentEgress(envId, ["pypi.org", "*.pythonhosted.org"]);
console.log(egress.allow);egress = temper.secrets.set_environment_egress(env_id, ["pypi.org", "*.pythonhosted.org"])
print(egress.allow)egress, err := client.Secrets.SetEnvironmentEgress(ctx, envID, []string{"pypi.org", "*.pythonhosted.org"})
if err != nil {
log.Fatal(err)
}
fmt.Println(egress.Allow)Changes to secrets and allowlists reach running environments without replacing the VM.
Changing and removing the key
- Rotate the key by calling
setwith the new value. The stand-in doesn't change. The gateway caches the real value briefly, so the new value can take a few minutes to be used everywhere. - Delete the secret or change its hosts, and the change applies right away.
- Check usage: listing secrets shows
last_used_at, the last time the gateway swapped in the value. Every use is also recorded in the audit log by secret name, never by value.