DaytonaDocs

Environment Variables & Secrets

Injecting configuration and credentials into sandboxes — and which mechanism to use.

Sandboxes usually need configuration: log levels, feature flags, endpoints — and credentials: API keys, tokens. Daytona provides two distinct mechanisms for getting values into a sandbox, and the difference between them is a security boundary, not a convenience. Plain env variables are visible to everything inside the sandbox; secrets are usable by code inside the sandbox but never readable by it.

Two mechanisms

envsecrets
What you passname → value mapnames of stored project secrets
What the guest seesreal valuesopaque placeholders (values substituted outside the sandbox on egress)
Best fornon-sensitive configAPI keys, tokens, credentials
Persistenceper-createstored once, reused across sandboxes

env — plain environment variables

The env parameter is a straightforward name → value map injected into the sandbox verbatim at create time. Every process in the sandbox can read the real values, exactly like environment variables anywhere else. Names must match [A-Za-z_][A-Za-z0-9_]* (invalid names fail the create with 400), and values are limited to 64 KiB each.

Use env for anything you would be comfortable printing in a log: configuration, endpoints, flags, non-sensitive identifiers.

Note: If the sandbox runs untrusted or AI-generated code, do not put credentials in env — that code can trivially read and exfiltrate them. Use secrets instead.

Python
from rlp import Daytona, CreateSandboxFromImageParams
 
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromImageParams(
    image="python:3.12-slim",
    env_vars={"LOG_LEVEL": "debug", "STAGE": "ci"},
))
 
print(sandbox.process.exec("echo $LOG_LEVEL").result)  # "debug"
TypeScript
import { Daytona } from 'rlpsdk'
 
const daytona = new Daytona()
const sandbox = await daytona.create({
  image: 'python:3.12-slim',
  envVars: { LOG_LEVEL: 'debug', STAGE: 'ci' },
})
 
const r = await sandbox.process.executeCommand('echo $LOG_LEVEL')
console.log(r.result) // "debug"

secrets — protected credentials

Secrets solve the problem env cannot: giving sandboxed code the ability to use a credential without the ability to read it. A secret is a named value stored encrypted in your project. You attach secrets to a sandbox by listing their names in the secrets array at create time; each name must already exist in the project or the create fails with 400.

Inside the sandbox, each attached secret appears as an environment variable — but its value is an opaque placeholder, not the real credential. When code in the sandbox makes an outbound request carrying the placeholder (say, an Authorization header for an external API), the platform substitutes the real value on the way out — and only for hosts matching the secret's allowed_domains. The real value never enters the sandbox, so even fully untrusted code cannot exfiltrate it: dumping the environment, reading memory, or sending the placeholder to an attacker's server yields nothing usable.

Create secrets first — in the dashboard or via PUT /secrets/:name — then attach them by name:

Python
from rlp import Daytona, CreateSandboxFromImageParams
 
daytona = Daytona()
 
# Store once per project; scope substitution to the real API's domain.
daytona.secret.set(
    "OPENAI_API_KEY",
    "sk-...",
    allowed_domains=["api.openai.com"],
)
 
sandbox = daytona.create(CreateSandboxFromImageParams(
    image="python:3.12-slim",
    secrets=["OPENAI_API_KEY"],
))
 
# Inside the sandbox, $OPENAI_API_KEY is a placeholder — but requests to
# api.openai.com carry the real key.
sandbox.process.exec(
    'curl -s https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"'
)
TypeScript
import { Daytona } from 'rlpsdk'
 
const daytona = new Daytona()
 
await daytona.secret.set('OPENAI_API_KEY', 'sk-...', {
  allowedDomains: ['api.openai.com'],
})
 
const sandbox = await daytona.create({
  image: 'python:3.12-slim',
  secrets: ['OPENAI_API_KEY'],
})
cURL
# Store the secret once (write-only — the API never returns values)
curl -X PUT https://api.rl.trydaytona.com/secrets/OPENAI_API_KEY \
  -H "Authorization: Bearer $RLP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value": "sk-...", "allowed_domains": ["api.openai.com"]}'
 
# Attach it by name at create time
curl -X POST https://api.rl.trydaytona.com/vms \
  -H "Authorization: Bearer $RLP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"image": "python:3.12-slim", "secrets": ["OPENAI_API_KEY"]}'

Restricting outbound network (egress)

Secrets stop credentials from leaking out of a sandbox; egress whitelisting stops the sandbox from talking to anywhere it shouldn't in the first place. The egress_policy parameter (available in the SDKs since 0.2.0) turns a sandbox into default-deny for outbound traffic: when enabled, the sandbox can reach only the destinations you list, plus DNS (which is always permitted so allowed domains can resolve). This is a strong containment control for running untrusted or AI-generated code — even if that code is malicious, it cannot reach an attacker's server or an unapproved API.

Each allow entry is one of:

  • an IPv4 address — "1.2.3.4"
  • an IPv4 CIDR — "140.82.0.0/16"
  • a domain — "api.openai.com" — or a leading-wildcard domain — "*.github.com" (matches subdomains, not the bare apex)

Allowed domains are enforced at the network layer: once a domain resolves, every port and protocol to the resolved IPs is permitted. An enabled policy with an empty allow list blocks everything except DNS. The policy is immutable after create (v1), and when it is absent or enabled: false the sandbox has open egress, exactly as before.

Python
from rlp import Daytona, CreateSandboxFromImageParams, EgressPolicy
 
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromImageParams(
    image="python:3.12-slim",
    egress_policy=EgressPolicy(
        enabled=True,
        allow=["api.openai.com", "*.github.com", "140.82.0.0/16"],
    ),
))
# Requests to api.openai.com succeed; everything else is blocked.
TypeScript
import { Daytona } from 'rlpsdk'
 
const daytona = new Daytona()
const sandbox = await daytona.create({
  image: 'python:3.12-slim',
  egressPolicy: {
    enabled: true,
    allow: ['api.openai.com', '*.github.com', '140.82.0.0/16'],
  },
})
cURL
curl -X POST https://api.rl.trydaytona.com/vms \
  -H "Authorization: Bearer $RLP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "python:3.12-slim",
    "egress_policy": {
      "enabled": true,
      "allow": ["api.openai.com", "*.github.com", "140.82.0.0/16"]
    }
  }'

Egress whitelisting pairs naturally with secrets: scope a secret's allowed_domains to the API it authenticates, and add that same domain to the egress allow list, so the sandbox can reach exactly one credentialed endpoint and nothing else.

Choosing between them

Use env for configuration and secrets for anything whose leakage would matter. A simple test: if the sandbox will ever run code you did not write and fully trust, every credential belongs in secrets. The two compose freely — most real creates carry an env map for configuration alongside a secrets list for credentials.

For sandboxes running untrusted code, combine both with an egress policy: secrets prevents credential theft, and egress whitelisting prevents the code from reaching anywhere you didn't approve.

See also

  • Secrets management — creating, updating, and deleting secrets; allowed_domains scoping; rotation.
  • Parameter reference — validation rules for env and secrets on the create request.

On this page