Sign Up

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.

FieldDescription
NameUnique 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.
ValueThe plaintext value, up to 64 KiB. Encrypted at rest and never returned by the API after creation.
DescriptionOptional human-readable description.
Allowed DomainsAllowlist of hosts the secret may be sent to. Supports exact hosts, wildcard subdomains, or any verified host. Required.
Updated AtTimestamp 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 h2 during 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:

FileContents
/etc/rlp/ca.pemThe proxy CA certificate.
/etc/rlp/ca-bundle.pemThe 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:

VariableValueRead by
SSL_CERT_FILE/etc/rlp/ca-bundle.pemOpenSSL-based clients
REQUESTS_CA_BUNDLE/etc/rlp/ca-bundle.pemPython requests, httpx
CURL_CA_BUNDLE/etc/rlp/ca-bundle.pemcurl
NODE_EXTRA_CA_CERTS/etc/rlp/ca.pemNode.js
GIT_SSL_CAINFO/etc/rlp/ca-bundle.pemgit over HTTPS
DENO_CERT/etc/rlp/ca-bundle.pemDeno

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.

RuleDescription
Hosts onlyUse 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 supportedPrefix 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-insensitiveHost matching ignores letter case.
Scope tightlyList only the hosts the secret requires.

The following examples are valid:

ExampleValue
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.

  1. Go to Daytona Dashboard ↗
  2. Click Create Secret
  3. Enter a Name and a Value. The value is write-only and is not shown again after saving
  4. Optionally enter a Description and a comma-separated list of Allowed Domains
  5. 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,
)
ParameterTypeDescription
allowed_domainsSequence[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.

  1. When creating the sandbox, pass secrets as a list of secret names. The secret name is the environment variable name injected into the sandbox.
  2. 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.
  3. Your code reads the environment variable as usual and sends it in an outbound HTTPS request, for example in an Authorization or X-Api-Key header.
  4. 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.

  1. Go to Daytona Sandboxes ↗
  2. Click Create Sandbox
  3. Open the Environment tab and click Manage secrets
  4. Select the secrets to attach and return to the form
  5. 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:

FieldDescription
secretsThe names of the project secrets attached to the sandbox.
desired_revisionThe revision of secrets. A sandbox that has not changed its secrets since creation is at revision 0.
applied_revisionThe revision the sandbox has applied, or None before the first change.
statusThe status is pending, applied, or failed. It is None before the first change.
errorExplains why the change failed when status is failed.
etagThe revision token to send back when you replace the secrets.
http_statusThe HTTP status code of the response.
capture_statusFor a restored or forked sandbox, how much of the source's secrets the copy received: complete, partial, or unresolved_legacy. Otherwise None.
capture_unresolved_countFor 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
ParameterTypeDescription
secretsSequence[str]Sets the full list of project secret names attached to the sandbox. Names left out are detached.
if_matchstrSets 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 works

Operations

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.

  1. Go to Daytona Dashboard ↗
  2. 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.

  1. Go to Daytona Dashboard ↗
  2. Click the ⋮ menu of the secret and click Edit
  3. Enter a New Value. The current value cannot be shown, so a new value is required
  4. Adjust the Description and Allowed Domains as needed
  5. 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.

  1. Go to Daytona Dashboard ↗
  2. Click the ⋮ menu of the secret and click Delete
  3. 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:

  1. Encode the credential outside the sandbox: printf '%s' 'username:password' | base64
  2. Store the encoded value as a secret
  3. 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.

PermissionGrants
read:secretsList and get secret metadata, never values
write:secretsCreate and update secrets
delete:secretsDelete secrets