Docs/ Security/ Secrets Management

Secrets Management

Store and manage sensitive configuration values with AES-256-GCM encryption, scoped access, and zero-downtime rotation.

How sh0 Encrypts Secrets

All secrets (environment variables marked as sensitive) are encrypted at rest in sh0's SQLite database using AES-256-GCM authenticated encryption, powered by the ring cryptography library.

The encryption workflow:

  1. A unique 256-bit encryption key is generated during sh0 installation and stored in the server's configuration directory.
  2. Each secret value is encrypted with a unique nonce (96-bit random), ensuring identical values produce different ciphertexts.
  3. The authentication tag (GCM) ensures secrets cannot be tampered with without detection.
  4. Secrets are only decrypted in memory when injected into containers at startup.
Note
Secret values are never logged, never included in API responses (only the key name is returned), and never visible in the dashboard after creation. You can overwrite a secret but cannot read its current value.

Adding Secrets via Dashboard

The most common way to manage secrets is through the dashboard's Environment Variables panel.

  1. Navigate to your app and open Settings → Environment Variables.
  2. Click Add Variable.
  3. Enter the variable name (e.g., DATABASE_URL).
  4. Enter the value.
  5. Toggle Secret to encrypt the value (enabled by default for common patterns like _KEY, _SECRET, _PASSWORD, _TOKEN).
  6. Click Save.
Environment variables panel with add variable form

Via the API:

Terminal
curl -X POST https://your-server:9000/api/apps/my-app/env \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "DATABASE_URL",
    "value": "postgres://user:pass@db:5432/mydb",
    "is_secret": true
  }'
Tip
Changes to environment variables require a redeploy to take effect. sh0 will prompt you to redeploy after saving changes, or you can apply them manually.

Bulk Import from .env File

If you have an existing .env file, you can import all variables at once.

  1. In the Environment Variables panel, click Bulk Import.
  2. Paste the contents of your .env file or upload it directly.
  3. sh0 parses the file and shows a preview of all variables.
  4. Review, mark sensitive values as secrets, and click Import.
Bulk import dialog showing parsed .env file contents

The parser supports standard .env syntax:

.env
# Database
DATABASE_URL=postgres://user:pass@db:5432/mydb
REDIS_URL=redis://redis:6379

# API Keys
STRIPE_SECRET_KEY=sk_live_abc123
SENDGRID_API_KEY=SG.xyz789

# App Config
NODE_ENV=production
PORT=3000
Warning
The bulk import will overwrite existing variables with the same name. Variables not present in the import file are left unchanged.

Secret Scoping

Secrets can be scoped at two levels, giving you control over which containers have access to which values.

Stack-Level Secrets

Stack-level secrets are shared across all services in a stack. This is ideal for shared configuration like database credentials that multiple services need.

Stack-level environment variables shared across all services

Set a stack-level secret from Stack Settings → Environment Variables, or via the API:

Terminal
curl -X POST https://your-server:9000/api/stacks/my-stack/env \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "DATABASE_URL",
    "value": "postgres://user:pass@db:5432/mydb",
    "is_secret": true
  }'

App-Level Secrets

App-level secrets are specific to a single service. They take precedence over stack-level secrets with the same name, allowing you to override shared values for individual services.

Note
Precedence order: App-level variables override stack-level variables. If both define DATABASE_URL, the app-level value is used for that specific service.

Secret Rotation

Rotating a secret means updating its value and redeploying the affected containers. sh0 makes this process seamless:

  1. Update the secret value in the dashboard or via the API.
  2. sh0 triggers a blue-green deployment with the new value.
  3. The old containers continue serving traffic until the new ones are healthy.
  4. Once healthy, traffic switches to the new containers with zero downtime.
Terminal
curl -X PUT https://your-server:9000/api/apps/my-app/env/STRIPE_SECRET_KEY \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "value": "sk_live_new_key_456",
    "redeploy": true
  }'
Secret rotation with blue-green deployment in progress
Tip
For database password rotation, update the password in the database first, then update the secret in sh0 and redeploy. This order prevents connection failures during the transition.

Accessing Secrets in Containers

Secrets are injected into containers as standard environment variables. Your application accesses them the same way it would access any environment variable:

Node.js
const dbUrl = process.env.DATABASE_URL;
const stripeKey = process.env.STRIPE_SECRET_KEY;
Python
import os
db_url = os.environ["DATABASE_URL"]
stripe_key = os.environ["STRIPE_SECRET_KEY"]
Rust
let db_url = std::env::var("DATABASE_URL")?;
let stripe_key = std::env::var("STRIPE_SECRET_KEY")?;

Secrets are set at container startup and remain available for the lifetime of the container. They are not written to disk inside the container.

Danger
Never log environment variables or include them in error reports. While sh0 protects secrets at rest and in transit, your application code must also handle them responsibly.