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
Values are encrypted at rest and never written to logs. Lists show them masked; a member with the Developer role or above can reveal them in the dashboard, or through the API with ?reveal=true. So can a Read-only API key: give keys and roles only to people and tools you trust with your secrets.

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. Every value is encrypted before it is stored; there is nothing to toggle.
  6. Click Save.
Environment variables panel with add variable form

Via the API:

Terminal
curl -X POST https://panel.example.com/api/v1/apps/APP_ID/env-vars \
  -H "Authorization: Bearer $SH0_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "DATABASE_URL",
    "value": "postgres://user:pass@db:5432/mydb"
  }'
Tip
Changes to environment variables take effect at the next deployment. Redeploy the app after saving them.

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://panel.example.com/api/v1/projects/PROJECT_ID/env-vars \
  -H "Authorization: Bearer $SH0_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "DATABASE_URL",
    "value": "postgres://user:pass@db:5432/mydb"
  }'

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 app that uses it:

  1. Update the value in the dashboard or through the API.
  2. Redeploy the app: the new container starts with the new value.
  3. The old container keeps serving traffic until the new one passes its health check.
  4. Traffic then switches to the new container.
Terminal
curl -X PATCH https://panel.example.com/api/v1/apps/APP_ID/env-vars/ENV_ID \
  -H "Authorization: Bearer $SH0_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "value": "sk_live_new_key_456"
  }'

curl -X POST https://panel.example.com/api/v1/apps/APP_ID/deployments \
  -H "Authorization: Bearer $SH0_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
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.