# Secrets

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.

| **Field**           | **Description**                                                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Name**            | Unique 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. |
| **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 and **`*.`** wildcards, or **`*`** for 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.
- **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.

## 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 ↗](https://app.daytona.com/dashboard/secrets)
2. Click <Button>Create Secret</Button>
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 <Button>Create</Button>


```python
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,
)
```

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

- A project holds at most 500 secrets.


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

await daytona.v2.secret.set('MY_API_KEY', 'secret-value', {
  description: 'Example API key',
  allowedDomains: ['api.example.com'],
  ifNotExists: true,
})
```

| **Parameter**    | **Type**   | **Description**                                                              |
| ---------------- | ---------- | ---------------------------------------------------------------------------- |
| **`allowedDomains`** | **`string[]`** | 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.


```bash
# --stdin keeps the value out of the process list and shell history
# secret set creates or replaces; the CLI has no create-only (if_not_exists) mode
printf %s 'secret-value' | daytona secret set MY_API_KEY \
  --stdin \
  --description 'Example API key' \
  --allowed-domain api.example.com
```

| **Flag**           | **Type**  | **Description**                                     |
| ------------------ | --------- | --------------------------------------------------- |
| **`--stdin`**          | **`bool`**    | Reads the value from standard input. It strips exactly one trailing newline or CRLF. |
| **`--from-file`**      | **`string`**  | Reads the value from a file. It strips exactly one trailing newline or CRLF. |
| **`--allowed-domain`** | **`strings`** | Adds one host to the allowlist. Repeat the flag once per host. Required when creating a secret. **`'*'`** allows any verified host. |

- Pass the value with exactly one of **`--stdin`**, **`--from-file`**, or **`--value`**.


```bash
# If-None-Match: * makes the write create-only; an existing name returns 412
curl 'https://api.daytona.com/secrets/MY_API_KEY' \
  --request PUT \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'If-None-Match: *' \
  --data '{
  "value": "secret-value",
  "description": "Example API key",
  "allowed_domains": ["api.example.com"]
}'
```

| **Field**         | **Type**   | **Description**                                                 |
| ----------------- | ---------- | --------------------------------------------------------------- |
| **`allowed_domains`** | **`string[]`** | Sets the host allowlist of the secret. This field is required, and an empty list returns **`400 Bad Request`**. The list holds at most 50 entries. A longer list returns **`400 Bad Request`**. |
| **`description`**     | **`string`**   | Sets the description. The description is at most 1 KiB. A longer description returns **`400 Bad Request`**. |

- A project holds at most 500 secrets.


## 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](#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](#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 ↗](https://app.daytona.com/dashboard/sandboxes)
2. Click <Button>Create Sandbox</Button>
3. Open the **Environment** tab and click <Button>Manage secrets</Button>
4. Select the secrets to attach and return to the form
5. Click <Button>Create</Button>



```python
from daytona import Daytona

daytona = Daytona()

sandbox = daytona.v2.create(secrets=["MY_API_KEY"])
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

const sandbox = await daytona.v2.create({
  secrets: ['MY_API_KEY'],
})
```


```bash
daytona sandbox create --secret MY_API_KEY
```


```bash
curl 'https://api.daytona.com/sandboxes' \
  --request POST \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "secrets": ["MY_API_KEY"]
}'
```


## 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-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:

| **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 `/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](#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](#create-a-secret) or [updating a secret](#update-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.

| **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, or a leading or trailing dot are rejected with **`400`**. |
| **Wildcards supported** | Prefix a host with **`*.`** to allow the base domain and its subdomains.                                                                                                                  |
| **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](https://www.daytona.io/docs/en/v2/network-limits.md). The two are independent and both apply.

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


```python
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`**.


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

await daytona.v2.secret.set('GITHUB_TOKEN', 'ghp_your_real_token', {
  allowedDomains: ['api.github.com'],
})

const sandbox = await daytona.v2.create({
  secrets: ['GITHUB_TOKEN'],
})

const response = await sandbox.process.run(
  'curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/user'
)
console.log(response.output) // 200 proves substitution works
```

- GitHub answers the placeholder with **`401`**.


```bash
printf %s 'ghp_your_real_token' | daytona secret set GITHUB_TOKEN --stdin --allowed-domain api.github.com

daytona sandbox create --secret GITHUB_TOKEN

# 200 proves substitution works
daytona sandbox exec {sandboxId} -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/user'
```

- GitHub answers the placeholder with **`401`**.


## Operations

### List secrets

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 ↗](https://app.daytona.com/dashboard/secrets)
2. Use the search field to filter the list by name


```python
from daytona import Daytona

daytona = Daytona()

secrets = daytona.v2.secret.list()
for secret in secrets:
    print(secret.name)
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

const secrets = await daytona.v2.secret.list()
secrets.items.forEach((secret) => console.log(secret.name))
```


```bash
daytona secret list
```


```bash
curl 'https://api.daytona.com/secrets' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```


##### Iterate over all secrets

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.


```python
from daytona import Daytona

daytona = Daytona()

for secret in daytona.v2.secret.iter():
    print(secret.name, secret.allowed_domains)
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

for await (const secret of daytona.v2.secret.iter()) {
  console.log(secret.name, secret.allowed_domains)
}
```


### Get a secret

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


```python
from daytona import Daytona

daytona = Daytona()

secret = daytona.v2.secret.get("MY_API_KEY")
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

const secret = await daytona.v2.secret.get('MY_API_KEY')
```


```bash
daytona secret get MY_API_KEY
```


```bash
curl 'https://api.daytona.com/secrets/MY_API_KEY' \
  --header 'Authorization: Bearer YOUR_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 ↗](https://app.daytona.com/dashboard/secrets)
2. Click the <Button>⋮</Button> menu of the secret and click <Button>Edit</Button>
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 <Button>Save</Button>


```python
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.


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

await daytona.v2.secret.set('MY_API_KEY', 'new-secret-value', {
  allowedDomains: ['api.example.com', '*.example.com'],
})
```

- **`set`** does not merge with the stored secret.
- Omitting **`description`** clears the description.
- Pass **`description`** again to keep it.


```bash
# The description and allowed domains you omit keep their current values
printf %s 'new-secret-value' | daytona secret set MY_API_KEY \
  --stdin \
  --allowed-domain api.example.com \
  --allowed-domain '*.example.com'
```


```bash
curl 'https://api.daytona.com/secrets/MY_API_KEY' \
  --request PUT \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "value": "new-secret-value",
  "allowed_domains": ["api.example.com", "*.example.com"]
}'
```

- **`PUT`** replaces the whole secret.
- Omitting **`description`** clears the description.


### Delete a secret

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 ↗](https://app.daytona.com/dashboard/secrets)
2. Click the <Button>⋮</Button> menu of the secret and click <Button>Delete</Button>
3. Click <Button>Delete</Button> to confirm


```python
from daytona import Daytona

daytona = Daytona()

daytona.v2.secret.delete("MY_API_KEY")
```


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

await daytona.v2.secret.delete('MY_API_KEY')
```


```bash
daytona secret delete MY_API_KEY
```


```bash
curl 'https://api.daytona.com/secrets/MY_API_KEY' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_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.


```python
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`**.


```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

const payload = Buffer.from('username:password').toString('base64')

await daytona.v2.secret.set('BASIC_AUTH', payload, {
  allowedDomains: ['api.example.com'],
})

const sandbox = await daytona.v2.create({
  secrets: ['BASIC_AUTH'],
})

const response = await sandbox.process.run(
  'curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Basic $BASIC_AUTH" https://api.example.com/protected'
)
console.log(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`**.


```bash
# --stdin strips the trailing newline that base64 prints
printf %s 'username:password' | base64 | daytona secret set BASIC_AUTH --stdin --allowed-domain api.example.com

daytona sandbox create --secret BASIC_AUTH

daytona sandbox exec {sandboxId} -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Basic $BASIC_AUTH" https://api.example.com/protected'
```

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

## 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](https://www.daytona.io/docs/en/v2/organizations.md) or an [API key](https://www.daytona.io/docs/en/v2/api-keys.md):

| **Permission**      | **Grants**                                       |
| ------------------- | ------------------------------------------------ |
| **`read:secrets`**  | List and get secret metadata, never values       |
| **`write:secrets`** | Create and update secrets                        |
| **`delete:secrets`** | Delete 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](https://www.daytona.io/docs/en/v2/audit-logs.md) record creates, updates, and deletes by secret name, never value.