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.
?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.
- Navigate to your app and open Settings → Environment Variables.
- Click Add Variable.
- Enter the variable name (e.g.,
DATABASE_URL). - Enter the value.
- Every value is encrypted before it is stored; there is nothing to toggle.
- Click Save.
Via the API:
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"
}'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://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.
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:
- Update the value in the dashboard or through the API.
- Redeploy the app: the new container starts with the new value.
- The old container keeps serving traffic until the new one passes its health check.
- Traffic then switches to the new container.
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 '{}'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.