Secrets Management¶
Secure credential storage using Docker Secrets for production deployments.
This guide covers Docker Secrets configuration for Home Security Intelligence. For comprehensive implementation details, see Administration Guide.
Overview¶
Docker Secrets provide enhanced security for credential management by storing sensitive data separately from environment variables and injecting them into containers at runtime.
Why Use Docker Secrets?¶
| Feature | Environment Variables | Docker Secrets |
|---|---|---|
Visibility in docker inspect | Visible | Hidden |
| File permissions | N/A | 600 (restricted) |
| Credential rotation | Requires restart | Update file, restart service |
| Container access | Read-write | Read-only at /run/secrets/ |
| Git risk | Often in .env files | Separate secrets/ directory |
Supported Secrets¶
| Secret | File | Service | Purpose |
|---|---|---|---|
| PostgreSQL password | postgres_password.txt | postgres, backend | Database authentication |
| Redis password | redis_password.txt | redis, backend, redis-exporter | Cache authentication |
| Grafana admin password | grafana_admin_password.txt | grafana | Dashboard admin access |
Quick Setup¶
1. Create Secrets Directory¶
# Create directory with secure permissions
mkdir -p secrets
chmod 700 secrets
# Generate strong passwords
openssl rand -base64 32 > secrets/postgres_password.txt
openssl rand -base64 32 > secrets/redis_password.txt
openssl rand -base64 32 > secrets/grafana_admin_password.txt
# Set restrictive file permissions
chmod 600 secrets/*.txt
Alternatively, use the interactive setup script:
2. Enable Secrets in docker-compose.prod.yml¶
Uncomment the secrets section at the bottom of docker-compose.prod.yml:
secrets:
postgres_password:
file: ./secrets/postgres_password.txt
redis_password:
file: ./secrets/redis_password.txt
grafana_admin_password:
file: ./secrets/grafana_admin_password.txt
3. Update Service Configurations¶
PostgreSQL:
postgres:
secrets:
- postgres_password
environment:
- POSTGRES_USER=${POSTGRES_USER:-security}
- POSTGRES_DB=${POSTGRES_DB:-security}
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
Redis:
redis:
secrets:
- redis_password
command: >-
sh -c '
if [ -f /run/secrets/redis_password ]; then
REDIS_PASSWORD=$(cat /run/secrets/redis_password)
redis-server --appendonly yes --requirepass "$REDIS_PASSWORD"
else
redis-server --appendonly yes
fi
'
Backend:
backend:
secrets:
- postgres_password
- redis_password
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
- REDIS_PASSWORD_FILE=/run/secrets/redis_password
Grafana (monitoring profile):
grafana:
secrets:
- grafana_admin_password
environment:
- GF_SECURITY_ADMIN_PASSWORD_FILE=/run/secrets/grafana_admin_password
4. Validate and Deploy¶
# Validate configuration
docker compose -f docker-compose.prod.yml config
# Start services
docker compose -f docker-compose.prod.yml up -d
# Verify secrets are mounted
docker compose -f docker-compose.prod.yml exec postgres ls -la /run/secrets/
Credential Rotation¶
Zero-Downtime Rotation¶
- Update the secret file:
# Generate new password
openssl rand -base64 32 > secrets/postgres_password.txt
chmod 600 secrets/postgres_password.txt
- Restart the service:
- Restart dependent services:
- Verify the change:
docker compose -f docker-compose.prod.yml logs postgres | tail -20
curl http://localhost:8000/api/system/health
Rotation Schedule¶
| Credential | Recommended Rotation | Notes |
|---|---|---|
| Database password | Every 90 days | Restart postgres, backend |
| Redis password | Every 90 days | Restart redis, backend, redis-exporter |
| Grafana admin | Every 90 days | Restart grafana |
| Compromised | Immediately | Full rotation of affected credential |
Accessing Secrets in Application Code¶
Backend Application¶
The backend can read secrets from files when _FILE environment variables are set:
from pathlib import Path
import os
def get_secret(secret_name: str, env_var: str) -> str:
"""Read secret from Docker secret file or environment variable."""
# Check for _FILE variant first
file_path = Path(f'/run/secrets/{secret_name}')
if file_path.exists():
return file_path.read_text().strip()
# Fall back to environment variable
return os.getenv(env_var, '')
# Usage
postgres_password = get_secret('postgres_password', 'POSTGRES_PASSWORD')
redis_password = get_secret('redis_password', 'REDIS_PASSWORD')
PostgreSQL (Native Support)¶
PostgreSQL has native support for POSTGRES_PASSWORD_FILE:
Redis (Command Script)¶
Redis requires reading the secret in the startup command:
command: >-
sh -c '
if [ -f /run/secrets/redis_password ]; then
REDIS_PASSWORD=$(cat /run/secrets/redis_password)
redis-server --requirepass "$REDIS_PASSWORD"
else
redis-server
fi
'
Grafana (Native Support)¶
Grafana supports _FILE suffix for environment variables:
Migration from Environment Variables¶
Current Setup (Environment Variables)¶
# .env file
POSTGRES_PASSWORD=my_secret_password
REDIS_PASSWORD=redis_secret
GF_ADMIN_PASSWORD=grafana_secret
Migration Steps¶
- Create secrets files:
mkdir -p secrets && chmod 700 secrets
echo "my_secret_password" > secrets/postgres_password.txt
echo "redis_secret" > secrets/redis_password.txt
echo "grafana_secret" > secrets/grafana_admin_password.txt
chmod 600 secrets/*.txt
-
Update docker-compose.prod.yml:
-
Uncomment the
secrets:top-level section - Add
secrets:to each service -
Change environment variables to
_FILEvariants -
Remove credentials from .env:
- Restart services:
- Verify migration:
# Check secrets are accessible
docker compose exec postgres cat /run/secrets/postgres_password
# Verify application connectivity
curl http://localhost:8000/api/system/health
Security Best Practices¶
File Permissions¶
# Directory: owner read/write/execute only
chmod 700 secrets/
# Files: owner read/write only
chmod 600 secrets/*.txt
# Verify permissions
ls -la secrets/
Version Control¶
The secrets/ directory is already in .gitignore. Verify it is never committed:
# Should show nothing
git status secrets/
# Verify .gitignore includes secrets/
grep secrets .gitignore
Access Control¶
- Restrict file system access to the secrets directory
- Use SELinux/AppArmor for additional container hardening
- Only Docker daemon needs read access to secret files
Backup Considerations¶
- Back up secret files securely (encrypted storage)
- Never store unencrypted secrets in version control
- Document which services use which secrets
- Consider using a secrets manager (Vault, AWS Secrets Manager) for larger deployments
Encryption at Rest¶
Docker Secrets are not encrypted at rest in standalone Docker mode. Protect secrets by:
- Using full-disk encryption (LUKS, BitLocker)
- Restricting physical access to the host
- Using encrypted backup storage
Troubleshooting¶
Secret File Not Found¶
Solution:
mkdir -p secrets
openssl rand -base64 32 > secrets/postgres_password.txt
chmod 600 secrets/postgres_password.txt
Permission Denied¶
Solution:
Container Cannot Read Secret¶
# Verify secret is mounted
docker compose exec postgres ls -la /run/secrets/
# Check secret content
docker compose exec postgres cat /run/secrets/postgres_password
Trailing Whitespace in Secret¶
Trailing newlines can cause authentication failures:
# Create secret without trailing newline
echo -n "password_here" > secrets/postgres_password.txt
# Or trim existing file
tr -d '\n' < secrets/postgres_password.txt > secrets/temp.txt
mv secrets/temp.txt secrets/postgres_password.txt
Database Connection Fails After Migration¶
- Verify the password in the secret file matches the database:
- If password mismatch, update the database password:
docker compose exec postgres psql -U postgres -c \
"ALTER USER security PASSWORD '$(cat secrets/postgres_password.txt)'"
See Also¶
- Administration Guide - Comprehensive secrets and security guide
- Redis Setup - Redis authentication configuration
- Database Setup - PostgreSQL configuration
- Configuration Reference - All environment variables