Secrets
Inject project secrets into sandbox network traffic without exposing the plaintext to sandbox code.
Secrets store sensitive values such as API keys, tokens, and passwords for use across your Daytona sandboxes. They let sandbox code authenticate to external services without hardcoding credentials into your source, snapshots, or sandbox environment variables.
Secrets are project-scoped, encrypted credentials that Daytona injects into a sandbox's outbound HTTPS traffic without ever placing the plaintext inside the sandbox. Instead of an environment variable holding the actual API key, the sandbox holds an opaque placeholder token. An outbound proxy replaces the placeholder with the real value before the request reaches its destination, and only when the request goes to a host you have allowed.
This lets you give a sandbox access to a credential such as an LLM API key or a database password while keeping the plaintext out of the sandbox environment, file system, process arguments, and logs. Code running in the sandbox can use the credential to reach an approved host, but cannot read the value from the sandbox or send it to any other host. The proxy does not rewrite responses, so an approved host that echoes the credential returns the value into the sandbox.
| Field | Description |
|---|---|
| Name | Unique name within the project, 1 to 256 letters, digits, and underscores, not starting with a digit. Doubles as the environment variable name in the sandbox. |
| Value | The plaintext value, up to 64 KiB. Encrypted at rest and never returned by the API after creation. |
| Description | Optional human-readable description. |
| Allowed Domains | Allowlist of hosts the secret may be sent to. Supports exact hosts, wildcard subdomains, or any verified host. Required. |
| Updated At | Timestamp of the last write. |
Use cases
Secrets are used when sandbox code needs a credential for an HTTPS service but must not be able to read it.
-
Model provider keys for agents. An agent calls a model API with a key it cannot read. The proxy substitutes the key only for the provider's host, so the agent cannot send it anywhere else. Responses pass through unchanged, so a provider that echoes the key returns the plaintext into the sandbox.
-
Third-party HTTPS APIs. Tokens for source control, payment, or other APIs are substituted in each request. The plaintext stays out of files, environment variables, process arguments, and logs. The proxy replaces placeholders in the request line, every header, and the body, so the token can sit wherever the API expects.
-
Untrusted code. Code from users or agents uses a credential, and the host allowlist limits where the value can go. A request to any other host carries only the placeholder. The proxy dials the destination itself and validates its certificate, so code that spoofs DNS, SNI, or the Host header cannot redirect the value.
-
Credentials outside images and snapshots. The sandbox holds only a placeholder, so source code, images, and snapshots carry no plaintext. A snapshot restore attaches the source's secrets again, with the exceptions in use a secret in a sandbox. Forks attach the source's secrets too, under the same placeholders.
-
Shared credentials without shared values. A member with the secrets read permission attaches a secret to a sandbox but never sees its value. The API does not return a value after creation. The member also needs the sandbox write permission, which an organization role or an API key can grant.
Substitution scope
The proxy substitutes placeholders in the request line and query string, in every request header, and in the request body. Send the secret wherever the destination service expects it, for example in an Authorization or X-Api-Key header or in a JSON body field.
Before substituting into an HTTPS request, the proxy resolves and dials the destination itself and validates the upstream certificate against the system roots, so a sandbox that spoofs DNS, SNI, or the Host header cannot redirect a substituted secret. Every request on a TLS connection must address the SNI host; a request for another host on that connection returns 400.
The following cases are not substituted:
- Cleartext HTTP is never substituted. A plain
http://destination cannot be authenticated, so the placeholder is forwarded intact, even for allowlisted hosts. Use HTTPS. - Request bodies over 1 MiB stream through; placeholders past 1 MiB are not replaced.
- Transformed placeholders are not substituted. Substitution is a literal replacement, so the request must carry the placeholder unmodified. Tools that encode the placeholder before sending, such as Basic Auth helpers that Base64-encode credentials, produce a header the proxy cannot match. See HTTP Basic auth.
- HTTP/2 is not supported. The proxy speaks HTTP/1.1 and does not offer
h2during ALPN, so standard HTTP clients negotiate HTTP/1.1. A client that requires HTTP/2, such as a gRPC client, fails because the proxy negotiates HTTP/1.1. - Certificate-pinned and ECH clients cannot be intercepted. The proxy terminates TLS with its own certificate authority, so a client that pins a different certificate rejects the proxy's leaf and the handshake fails. A client that uses Encrypted Client Hello is not supported either. In both cases the request does not reach the destination.
- Non-HTTP protocols on other ports carry the placeholder as-is. Sandboxes with secrets block UDP port 443, so HTTP/3 clients fall back to TLS over TCP and are substituted normally.
Certificate trust
The proxy terminates TLS with its own certificate authority and mints a leaf certificate per destination. Daytona installs the authority before the entrypoint so clients trust the leaf:
| File | Contents |
|---|---|
/etc/rlp/ca.pem | The proxy CA certificate. |
/etc/rlp/ca-bundle.pem | The system root bundle with the proxy CA appended. |
Daytona also exports the following environment variables into every process the sandbox spawns, so common clients trust the bundle without configuration:
| Variable | Value | Read by |
|---|---|---|
SSL_CERT_FILE | /etc/rlp/ca-bundle.pem | OpenSSL-based clients |
REQUESTS_CA_BUNDLE | /etc/rlp/ca-bundle.pem | Python requests, httpx |
CURL_CA_BUNDLE | /etc/rlp/ca-bundle.pem | curl |
NODE_EXTRA_CA_CERTS | /etc/rlp/ca.pem | Node.js |
GIT_SSL_CAINFO | /etc/rlp/ca-bundle.pem | git over HTTPS |
DENO_CERT | /etc/rlp/ca-bundle.pem | Deno |
A client that ignores these variables and reads only its own trust store must be pointed at the CA bundle in the table, otherwise its TLS handshake against the proxy fails. Do not disable certificate verification to work around this. The CA install is best-effort: when the image does not allow writes to the CA location, the placeholder environment variables are still set.
The proxy does not rewrite responses. If a destination echoes the credential, the real value enters the sandbox in the response body. Do not send a secret to an echo or debugging service you would not trust with the plaintext or use one to test substitution. See verify substitution.
Host allowlist
The host allowlist is the set of hosts a secret may be sent to. Set your allowed hosts using the allowed domains array when creating a secret or updating a secret. The proxy replaces the placeholder only for requests whose destination host matches an entry in the allowlist; a request to any other host carries the unmodified placeholder.
Every secret requires an allowlist. A wildcard entry that is a single asterisk allows any verified host: the proxy replaces the placeholder for HTTPS requests to any host, including one a compromised sandbox controls. List specific hosts unless you have a specific reason not to.
| Rule | Description |
|---|---|
| Hosts only | Use hostnames of up to 253 characters made of letters, digits, dots, and hyphens; entries with a scheme, path, port, whitespace, a leading dot, or an empty label are rejected with 400. |
| Wildcards supported | Prefix a host with *. to allow its subdomains. The base domain itself does not match, and the part after *. cannot be a public suffix such as com or co.uk. |
| Case-insensitive | Host matching ignores letter case. |
| Scope tightly | List only the hosts the secret requires. |
The following examples are valid:
| Example | Value |
|---|---|
| Single host | ["api.example.com"] |
| Wildcard host | ["*.example.com"] |
| Multiple hosts | ["api.example.com", "*.example.org", "service.example.net"] |
The allowlist decides only whether a placeholder is substituted. It does not block traffic: a request to a host outside the allowlist is still forwarded, carrying the placeholder. To restrict which hosts a sandbox can reach at all, set a sandbox network policy. The two are independent and both apply.
Create a secret
Daytona provides methods to create a secret. A secret is created with a put request to its name, which also updates an existing secret of the same name. Set the if-not-exists flag to make the write create-only: the request then fails if the name is already taken, and the check runs in the same transaction as the write, so two concurrent creates cannot overwrite each other.
- Go to Daytona Dashboard ↗
- Click Create Secret
- Enter a Name and a Value. The value is write-only and is not shown again after saving
- Optionally enter a Description and a comma-separated list of Allowed Domains
- Click Create
from daytona import Daytona
daytona = Daytona()
daytona.v2.secrets.set(
"MY_API_KEY",
"secret-value",
description="Example API key",
allowed_domains=["api.example.com"],
if_not_exists=True,
)| Parameter | Type | Description |
|---|---|---|
allowed_domains | Sequence[str] | Sets the host allowlist of the secret. This parameter is required. ["*"] allows any verified host, including one a compromised sandbox controls. |
Use a secret in a sandbox
Daytona provides methods to inject a secret into a sandbox.
- When creating the sandbox, pass
secretsas a list of secret names. The secret name is the environment variable name injected into the sandbox. - Daytona sets that variable to the secret's placeholder, not the real value, before the entrypoint starts. Every process, session, PTY, and SSH exec in the sandbox inherits it.
- Your code reads the environment variable as usual and sends it in an outbound HTTPS request, for example in an
AuthorizationorX-Api-Keyheader. - The outbound proxy substitutes the real value into requests to the secret's allowed hosts and leaves the placeholder for any other host. See substitution scope.
The same stored secret cannot be injected under a different variable name. Attaching a name that does not exist in the project fails the create before any sandbox is scheduled. Duplicate names are collapsed to one.
The secrets parameter is accepted on sandbox creates. To change the secrets of an existing sandbox, see replace sandbox secrets. Secrets attached to a sandbox travel with it: a snapshot restore or fork attaches the secrets the source had when its memory was captured, under the same placeholders. Secrets deleted since the capture are not attached, and a caller without the secrets read permission gets none; the copy then reports a partial capture. A restore from a snapshot taken before Daytona recorded the attached secrets attaches none. The capture status of the copy reports which case applies.
- Go to Daytona Sandboxes ↗
- Click Create Sandbox
- Open the Environment tab and click Manage secrets
- Select the secrets to attach and return to the form
- Click Create
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create(secrets=["MY_API_KEY"])Get sandbox secrets
Get the secrets attached to a sandbox. The response lists the attached secret names, never values or placeholders, and reports how far the sandbox has applied them. A change is applied in the background, so the status stays pending until the sandbox has the new set. The response also carries a revision tag that a replace must send back.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create(secrets=["MY_API_KEY"])
info = sandbox.get_secrets()
print(info.secrets, info.status, info.etag)The response has the following fields:
| Field | Description |
|---|---|
secrets | The names of the project secrets attached to the sandbox. |
desired_revision | The revision of secrets. A sandbox that has not changed its secrets since creation is at revision 0. |
applied_revision | The revision the sandbox has applied, or None before the first change. |
status | The status is pending, applied, or failed. It is None before the first change. |
error | Explains why the change failed when status is failed. |
etag | The revision token to send back when you replace the secrets. |
http_status | The HTTP status code of the response. |
capture_status | For a restored or forked sandbox, how much of the source's secrets the copy received: complete, partial, or unresolved_legacy. Otherwise None. |
capture_unresolved_count | For a partial copy, the number of the source's secrets the copy did not receive. |
Update secrets in a sandbox
Replace the secrets attached to an existing sandbox. A replace sends the full list of names; names left out are detached, names that stay attached keep their placeholders, and new names get new ones. Every replace sends the revision tag from the last read. If the secrets changed since that read, the replace fails instead of overwriting them; get the secrets again and retry with the new tag.
The sandbox applies the change asynchronously. The replace reports the change as pending, and a later get reports it as applied once the sandbox has the new set, or as failed with an error. Processes that are already running keep their environment; processes started after the change see the new names.
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.v2.create(secrets=["MY_API_KEY"])
current = sandbox.get_secrets()
updated = sandbox.replace_secrets(
secrets=["MY_API_KEY", "GITHUB_TOKEN"],
if_match=current.etag,
)
print(updated.status) # pending until the sandbox applies the change| Parameter | Type | Description |
|---|---|---|
secrets | Sequence[str] | Sets the full list of project secret names attached to the sandbox. Names left out are detached. |
if_match | str | Sets the revision tag from the last read. This parameter is required. A stale tag raises an error. Read again and retry. |
Verify substitution
Verify substitution by calling the real service and checking the status code. For example, store a GitHub token as a secret allowed for api.github.com, then call the GitHub user endpoint from the sandbox. A successful response proves the proxy replaced the placeholder with the real token, because GitHub rejects the placeholder.
Do not use an echo service for this. Responses are not rewritten, so an echo service returns the substituted value into the sandbox and defeats the point of the secret.
from daytona import Daytona
daytona = Daytona()
daytona.v2.secrets.set(
"GITHUB_TOKEN",
"ghp_your_real_token",
allowed_domains=["api.github.com"],
)
sandbox = daytona.v2.create(
secrets=["GITHUB_TOKEN"],
)
response = sandbox.process.run(
'curl -s -o /dev/null -w "%{http_code}" '
'-H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/user'
)
print(response.output) # 200 proves substitution worksOperations
List secrets
List operations return one page of the project's secrets, ordered by name, with a cursor for the next page. A page holds 100 secrets by default and at most 200. List operations return metadata only. Secret values are never returned after creation.
- Go to Daytona Dashboard ↗
- Use the search field to filter the list by name
from daytona import Daytona
daytona = Daytona()
secrets = daytona.v2.secrets.list()
for secret in secrets.items:
print(secret.name)Iterate over all secrets
Iterate over all secrets in the project. The iterator requests the pages one after another and yields the items one at a time.
from daytona import Daytona
daytona = Daytona()
for secret in daytona.v2.secrets.iter():
print(secret.name, secret.allowed_domains)Get a secret
Get a secret by name: allowed domains, description, and update time, never the value.
from daytona import Daytona
daytona = Daytona()
secret = daytona.v2.secrets.get("MY_API_KEY")Update a secret
Update a secret. An update is the same write as a create: it always takes a new value and replaces the value, the allowed domains, and the description together.
- Go to Daytona Dashboard ↗
- Click the ⋮ menu of the secret and click Edit
- Enter a New Value. The current value cannot be shown, so a new value is required
- Adjust the Description and Allowed Domains as needed
- Click Save
from daytona import Daytona
daytona = Daytona()
daytona.v2.secrets.set(
"MY_API_KEY",
"new-secret-value",
allowed_domains=["api.example.com", "*.example.com"],
)Delete a secret
Delete a secret. To revoke a secret without deleting it, overwrite its value or narrow its allowed domains.
- Go to Daytona Dashboard ↗
- Click the ⋮ menu of the secret and click Delete
- Click Delete to confirm
from daytona import Daytona
daytona = Daytona()
daytona.v2.secrets.delete("MY_API_KEY")HTTP Basic Auth
Basic Auth helpers in HTTP clients do not work with a secret that holds a raw password. These helpers Base64-encode the credential inside the sandbox, so the outbound Authorization header carries the encoded username and placeholder. The encoded header no longer contains the placeholder, the proxy cannot substitute it, and the upstream service rejects the request.
To authenticate with Basic Auth, store the Base64-encoded username and password pair as the secret value and set the Authorization header directly:
- Encode the credential outside the sandbox:
printf '%s' 'username:password' | base64 - Store the encoded value as a secret
- In the sandbox, set the header directly:
Authorization: Basic $MY_SECRET
The placeholder then appears unmodified in the header, and the proxy substitutes the encoded credential for requests to the secret's allowed hosts. Do not pass the mounted variable through a helper that encodes it again.
import base64
from daytona import Daytona
daytona = Daytona()
payload = base64.b64encode(b"username:password").decode()
daytona.v2.secrets.set(
"BASIC_AUTH",
payload,
allowed_domains=["api.example.com"],
)
sandbox = daytona.v2.create(
secrets=["BASIC_AUTH"],
)
response = sandbox.process.run(
'curl -s -o /dev/null -w "%{http_code}" '
'-H "Authorization: Basic $BASIC_AUTH" https://api.example.com/protected'
)
print(response.output)Permissions
Secrets are scoped to a project. A secret needed by two projects must be stored in each. Access is governed by three permissions, which you can grant to an organization role or an API key.
Attaching a secret when creating a sandbox requires the secrets read permission in addition to the sandbox write permission. The dashboard enforces the same project-scoped permissions as the API: a member whose role lacks the secrets write permission does not see the create and edit actions, and one without the secrets delete permission does not see the delete action.
The audit logs record creates, updates, and deletes by secret name, never value.
| Permission | Grants |
|---|---|
read:secrets | List and get secret metadata, never values |
write:secrets | Create and update secrets |
delete:secrets | Delete secrets |