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:
- A unique 256-bit encryption key is generated during sh0 installation and stored in the server's configuration directory.
- Each secret value is encrypted with a unique nonce (96-bit random), ensuring identical values produce different ciphertexts.
- The authentication tag (GCM) ensures secrets cannot be tampered with without detection.
- Secrets are only decrypted in memory when injected into containers at startup.
Adding Secrets via Dashboard
The most common way to manage secrets is through the dashboard's Environment Variables panel.
- Navigate to your app and open Settings → Environment Variables.
- Click Add Variable.
- Enter the variable name (e.g.,
DATABASE_URL). - Enter the value.
- Toggle Secret to encrypt the value (enabled by default for common patterns like
_KEY,_SECRET,_PASSWORD,_TOKEN). - Click Save.
Via the API:
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
}'Bulk Import from .env File
If you have an existing .env file, you can import all variables at once.
- In the Environment Variables panel, click Bulk Import.
- Paste the contents of your
.envfile or upload it directly. - sh0 parses the file and shows a preview of all variables.
- Review, mark sensitive values as secrets, and click Import.
The parser supports standard .env syntax:
# 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=3000Secret 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.
Set a stack-level secret from Stack Settings → Environment Variables, or via the API:
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.
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:
- Update the secret value in the dashboard or via the API.
- sh0 triggers a blue-green deployment with the new value.
- The old containers continue serving traffic until the new ones are healthy.
- Once healthy, traffic switches to the new containers with zero downtime.
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
}'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:
const dbUrl = process.env.DATABASE_URL;
const stripeKey = process.env.STRIPE_SECRET_KEY;import os
db_url = os.environ["DATABASE_URL"]
stripe_key = os.environ["STRIPE_SECRET_KEY"]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.