Skip to content

Secrets

View as Markdown

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 or send it anywhere else.

FieldDescription
NameUnique name within the project, 1 to 256 characters, matching [A-Za-z_][A-Za-z0-9_]*. 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 and *. wildcards, or * for any verified host. Required.
Updated AtTimestamp of the last write.

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.
  • 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.
  • 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.
  • HTTP Basic authentication: services that expect a Base64-encoded username and password get their credentials through a secret that holds the encoded 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 same secrets again.
  • 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.
  • Central credential management: a secret is stored once per project and attached by name to every sandbox that needs it. The audit logs record creates, updates, and deletes by name, never by value.
  • Network-restricted sandboxes: a network policy limits which hosts the sandbox reaches, and the allowlist limits which hosts receive the credential. Both apply to the same request.

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.secret.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.
  • A project holds at most 500 secrets.

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. Attaching a secret requires the secrets read permission and the sandbox write permission; see permissions.

The secrets parameter is accepted on sandbox creates. Secrets attached to a sandbox travel with it: a snapshot restore of a sandbox with secrets attaches the same secrets under the same placeholders.

  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"])

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.

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 /etc/rlp/ca-bundle.pem, otherwise its TLS handshake against the proxy fails. Do not disable certificate verification to work around this. The CA install is best-effort: if /etc is not writable in the image, 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.

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. The entry * 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, or a leading or trailing dot are rejected with 400.
Wildcards supportedPrefix a host with *. to allow the base domain and its subdomains.
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.

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.secret.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
  • GitHub answers the placeholder with 401.

List operations return the project’s secrets as a single list, ordered by name. 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.secret.list()
for secret in secrets:
print(secret.name)

Iterate over all secrets in the project. The iterator fetches the full list in one request, the same as listing, and then yields the items one at a time.

from daytona import Daytona
daytona = Daytona()
for secret in daytona.v2.secret.iter():
print(secret.name, secret.allowed_domains)

Get a secret by name: allowed domains, description, and update time, never the value.

from daytona import Daytona
daytona = Daytona()
secret = daytona.v2.secret.get("MY_API_KEY")

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.secret.set(
"MY_API_KEY",
"new-secret-value",
allowed_domains=["api.example.com", "*.example.com"],
)
  • set does not merge with the stored secret.
  • Omitting description clears the description.
  • Pass description again to keep it.

Delete a secret. Deleting a name that does not exist succeeds. Sandboxes that are already running keep the secret until they are torn down; new sandboxes can no longer attach it.

  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.secret.delete("MY_API_KEY")

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.secret.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)
  • Helpers such as curl --user and Python’s requests.auth.HTTPBasicAuth send Basic Base64(username:placeholder).
  • The upstream service rejects that header with 401.

Alternatively, store the full header value Basic Base64(username:password) as the secret and send Authorization: $MY_SECRET. Both forms keep the outbound placeholder intact.

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:

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

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.