Major accomplishments: - ✅ SELinux policy installed and working - ✅ Core Traefik v2.10 deployment running - ✅ Production configuration ready (v3.1) - ✅ Monitoring stack configured - ✅ Comprehensive documentation created - ✅ Security hardening implemented Current status: - 🟡 Partially deployed (60% complete) - ⚠️ Docker socket access needs resolution - ❌ Monitoring stack not deployed yet - ⚠️ Production migration pending Next steps: 1. Fix Docker socket permissions 2. Deploy monitoring stack 3. Migrate to production config 4. Validate full functionality Files added: - Complete Traefik deployment documentation - Production and test configurations - Monitoring stack configurations - SELinux policy module - Security checklists and guides - Current status documentation
605 lines
19 KiB
Bash
Executable File
605 lines
19 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Complete Secrets Management Implementation
|
|
# Comprehensive Docker secrets management for HomeAudit infrastructure
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
SECRETS_DIR="$PROJECT_ROOT/secrets"
|
|
LOG_FILE="$PROJECT_ROOT/logs/secrets-management-$(date +%Y%m%d-%H%M%S).log"
|
|
|
|
# Create directories
|
|
mkdir -p "$SECRETS_DIR"/{env,files,docker,validation} "$(dirname "$LOG_FILE")"
|
|
|
|
# Logging function
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
# Generate secure random password
|
|
generate_password() {
|
|
local length="${1:-32}"
|
|
openssl rand -base64 "$length" | tr -d "=+/" | cut -c1-"$length"
|
|
}
|
|
|
|
# Create Docker secret safely
|
|
create_docker_secret() {
|
|
local secret_name="$1"
|
|
local secret_value="$2"
|
|
local overwrite="${3:-false}"
|
|
|
|
# Check if secret already exists
|
|
if docker secret inspect "$secret_name" >/dev/null 2>&1; then
|
|
if [[ "$overwrite" == "true" ]]; then
|
|
log "⚠️ Secret $secret_name exists, removing..."
|
|
docker secret rm "$secret_name" || true
|
|
sleep 1
|
|
else
|
|
log "✅ Secret $secret_name already exists, skipping"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Create the secret
|
|
echo "$secret_value" | docker secret create "$secret_name" - >/dev/null
|
|
log "✅ Created Docker secret: $secret_name"
|
|
}
|
|
|
|
# Collect existing secrets from running containers
|
|
collect_existing_secrets() {
|
|
log "Collecting existing secrets from running containers..."
|
|
|
|
local secrets_inventory="$SECRETS_DIR/existing-secrets-inventory.yaml"
|
|
cat > "$secrets_inventory" << 'EOF'
|
|
# Existing Secrets Inventory
|
|
# Collected from running containers
|
|
secrets_found:
|
|
EOF
|
|
|
|
# Scan running containers
|
|
docker ps --format "{{.Names}}" | while read -r container; do
|
|
if [[ -z "$container" ]]; then continue; fi
|
|
|
|
log "Scanning container: $container"
|
|
|
|
# Extract environment variables (sanitized)
|
|
local env_file="$SECRETS_DIR/env/${container}.env"
|
|
docker exec "$container" env 2>/dev/null | \
|
|
grep -iE "(password|secret|key|token|api)" | \
|
|
sed 's/=.*$/=REDACTED/' > "$env_file" || touch "$env_file"
|
|
|
|
# Check for mounted secret files
|
|
local mounts_file="$SECRETS_DIR/files/${container}-mounts.txt"
|
|
docker inspect "$container" 2>/dev/null | \
|
|
jq -r '.[].Mounts[]? | select(.Type=="bind") | .Source' | \
|
|
grep -iE "(secret|key|cert|password)" > "$mounts_file" 2>/dev/null || touch "$mounts_file"
|
|
|
|
# Add to inventory
|
|
if [[ -s "$env_file" || -s "$mounts_file" ]]; then
|
|
cat >> "$secrets_inventory" << EOF
|
|
$container:
|
|
env_secrets: $(wc -l < "$env_file")
|
|
mounted_secrets: $(wc -l < "$mounts_file")
|
|
env_file: "$env_file"
|
|
mounts_file: "$mounts_file"
|
|
EOF
|
|
fi
|
|
done
|
|
|
|
log "✅ Secrets inventory created: $secrets_inventory"
|
|
}
|
|
|
|
# Generate all required Docker secrets
|
|
generate_docker_secrets() {
|
|
log "Generating Docker secrets for all services..."
|
|
|
|
# Database secrets
|
|
create_docker_secret "pg_root_password" "$(generate_password 32)"
|
|
create_docker_secret "mariadb_root_password" "$(generate_password 32)"
|
|
create_docker_secret "redis_password" "$(generate_password 24)"
|
|
|
|
# Application secrets
|
|
create_docker_secret "nextcloud_db_password" "$(generate_password 32)"
|
|
create_docker_secret "nextcloud_admin_password" "$(generate_password 24)"
|
|
create_docker_secret "immich_db_password" "$(generate_password 32)"
|
|
create_docker_secret "paperless_secret_key" "$(generate_password 64)"
|
|
create_docker_secret "vaultwarden_admin_token" "$(generate_password 48)"
|
|
create_docker_secret "grafana_admin_password" "$(generate_password 24)"
|
|
|
|
# API tokens and keys
|
|
create_docker_secret "ha_api_token" "$(generate_password 64)"
|
|
create_docker_secret "jellyfin_api_key" "$(generate_password 32)"
|
|
create_docker_secret "gitea_secret_key" "$(generate_password 64)"
|
|
create_docker_secret "traefik_dashboard_password" "$(htpasswd -nbB admin $(generate_password 16) | cut -d: -f2)"
|
|
|
|
# SSL/TLS certificates (if not using Let's Encrypt)
|
|
if [[ ! -f "$SECRETS_DIR/files/tls.crt" ]]; then
|
|
log "Generating self-signed SSL certificate..."
|
|
openssl req -x509 -newkey rsa:4096 -keyout "$SECRETS_DIR/files/tls.key" -out "$SECRETS_DIR/files/tls.crt" -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" >/dev/null 2>&1
|
|
create_docker_secret "tls_certificate" "$(cat "$SECRETS_DIR/files/tls.crt")"
|
|
create_docker_secret "tls_private_key" "$(cat "$SECRETS_DIR/files/tls.key")"
|
|
fi
|
|
|
|
log "✅ All Docker secrets generated successfully"
|
|
}
|
|
|
|
# Create secrets mapping file for stack updates
|
|
create_secrets_mapping() {
|
|
log "Creating secrets mapping configuration..."
|
|
|
|
local mapping_file="$SECRETS_DIR/docker-secrets-mapping.yaml"
|
|
cat > "$mapping_file" << 'EOF'
|
|
# Docker Secrets Mapping
|
|
# Maps environment variables to Docker secrets
|
|
|
|
secrets_mapping:
|
|
postgresql:
|
|
POSTGRES_PASSWORD: pg_root_password
|
|
POSTGRES_DB_PASSWORD: pg_root_password
|
|
|
|
mariadb:
|
|
MYSQL_ROOT_PASSWORD: mariadb_root_password
|
|
MARIADB_ROOT_PASSWORD: mariadb_root_password
|
|
|
|
redis:
|
|
REDIS_PASSWORD: redis_password
|
|
|
|
nextcloud:
|
|
MYSQL_PASSWORD: nextcloud_db_password
|
|
NEXTCLOUD_ADMIN_PASSWORD: nextcloud_admin_password
|
|
|
|
immich:
|
|
DB_PASSWORD: immich_db_password
|
|
|
|
paperless:
|
|
PAPERLESS_SECRET_KEY: paperless_secret_key
|
|
|
|
vaultwarden:
|
|
ADMIN_TOKEN: vaultwarden_admin_token
|
|
|
|
homeassistant:
|
|
SUPERVISOR_TOKEN: ha_api_token
|
|
|
|
grafana:
|
|
GF_SECURITY_ADMIN_PASSWORD: grafana_admin_password
|
|
|
|
jellyfin:
|
|
JELLYFIN_API_KEY: jellyfin_api_key
|
|
|
|
gitea:
|
|
GITEA__security__SECRET_KEY: gitea_secret_key
|
|
|
|
# File secrets (certificates, keys)
|
|
file_secrets:
|
|
tls_certificate: /run/secrets/tls_certificate
|
|
tls_private_key: /run/secrets/tls_private_key
|
|
EOF
|
|
|
|
log "✅ Secrets mapping created: $mapping_file"
|
|
}
|
|
|
|
# Update stack files to use Docker secrets
|
|
update_stacks_with_secrets() {
|
|
log "Updating stack files to use Docker secrets..."
|
|
|
|
local stacks_dir="$PROJECT_ROOT/stacks"
|
|
local backup_dir="$PROJECT_ROOT/backups/stacks-pre-secrets-$(date +%Y%m%d-%H%M%S)"
|
|
|
|
# Create backup
|
|
mkdir -p "$backup_dir"
|
|
find "$stacks_dir" -name "*.yml" -exec cp {} "$backup_dir/" \;
|
|
log "✅ Stack files backed up to: $backup_dir"
|
|
|
|
# Update each stack file
|
|
find "$stacks_dir" -name "*.yml" | while read -r stack_file; do
|
|
local stack_name
|
|
stack_name=$(basename "$stack_file" .yml)
|
|
log "Updating stack file: $stack_name"
|
|
|
|
# Create updated stack with secrets
|
|
python3 << PYTHON_SCRIPT
|
|
import yaml
|
|
import re
|
|
import sys
|
|
|
|
stack_file = "$stack_file"
|
|
try:
|
|
# Load the stack file
|
|
with open(stack_file, 'r') as f:
|
|
stack_data = yaml.safe_load(f)
|
|
|
|
# Ensure secrets section exists
|
|
if 'secrets' not in stack_data:
|
|
stack_data['secrets'] = {}
|
|
|
|
# Process services
|
|
if 'services' in stack_data:
|
|
for service_name, service_config in stack_data['services'].items():
|
|
if 'environment' in service_config:
|
|
env_vars = service_config['environment']
|
|
|
|
# Convert environment list to dict if needed
|
|
if isinstance(env_vars, list):
|
|
env_dict = {}
|
|
for env in env_vars:
|
|
if '=' in env:
|
|
key, value = env.split('=', 1)
|
|
env_dict[key] = value
|
|
else:
|
|
env_dict[env] = ''
|
|
env_vars = env_dict
|
|
service_config['environment'] = env_vars
|
|
|
|
# Update password/secret environment variables
|
|
secrets_added = []
|
|
for env_key, env_value in list(env_vars.items()):
|
|
if any(keyword in env_key.lower() for keyword in ['password', 'secret', 'key', 'token']):
|
|
# Convert to _FILE pattern for Docker secrets
|
|
file_env_key = env_key + '_FILE'
|
|
secret_name = env_key.lower().replace('_', '_')
|
|
|
|
# Map common secret names
|
|
secret_mappings = {
|
|
'postgres_password': 'pg_root_password',
|
|
'mysql_password': 'nextcloud_db_password',
|
|
'mysql_root_password': 'mariadb_root_password',
|
|
'db_password': service_name + '_db_password',
|
|
'admin_password': service_name + '_admin_password',
|
|
'secret_key': service_name + '_secret_key',
|
|
'api_token': service_name + '_api_token'
|
|
}
|
|
|
|
mapped_secret = secret_mappings.get(secret_name, secret_name)
|
|
|
|
# Update environment to use secrets file
|
|
env_vars[file_env_key] = f'/run/secrets/{mapped_secret}'
|
|
if env_key in env_vars:
|
|
del env_vars[env_key]
|
|
|
|
# Add to secrets section
|
|
stack_data['secrets'][mapped_secret] = {'external': True}
|
|
secrets_added.append(mapped_secret)
|
|
|
|
# Add secrets to service if any were added
|
|
if secrets_added:
|
|
if 'secrets' not in service_config:
|
|
service_config['secrets'] = []
|
|
service_config['secrets'].extend(secrets_added)
|
|
|
|
# Write updated stack file
|
|
with open(stack_file, 'w') as f:
|
|
yaml.dump(stack_data, f, default_flow_style=False, indent=2, sort_keys=False)
|
|
|
|
print(f"✅ Updated {stack_file} with Docker secrets")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Error updating {stack_file}: {e}")
|
|
sys.exit(1)
|
|
PYTHON_SCRIPT
|
|
done
|
|
|
|
log "✅ All stack files updated to use Docker secrets"
|
|
}
|
|
|
|
# Validate secrets configuration
|
|
validate_secrets() {
|
|
log "Validating secrets configuration..."
|
|
|
|
local validation_report="$SECRETS_DIR/validation-report.yaml"
|
|
cat > "$validation_report" << EOF
|
|
secrets_validation:
|
|
timestamp: "$(date -Iseconds)"
|
|
docker_secrets:
|
|
EOF
|
|
|
|
# Check each secret
|
|
local total_secrets=0
|
|
local valid_secrets=0
|
|
|
|
docker secret ls --format "{{.Name}}" | while read -r secret_name; do
|
|
if [[ -n "$secret_name" ]]; then
|
|
((total_secrets++))
|
|
if docker secret inspect "$secret_name" >/dev/null 2>&1; then
|
|
((valid_secrets++))
|
|
echo " - name: \"$secret_name\"" >> "$validation_report"
|
|
echo " status: \"valid\"" >> "$validation_report"
|
|
echo " created: \"$(docker secret inspect "$secret_name" --format '{{.CreatedAt}}')\"" >> "$validation_report"
|
|
else
|
|
echo " - name: \"$secret_name\"" >> "$validation_report"
|
|
echo " status: \"invalid\"" >> "$validation_report"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Add summary
|
|
cat >> "$validation_report" << EOF
|
|
summary:
|
|
total_secrets: $total_secrets
|
|
valid_secrets: $valid_secrets
|
|
validation_passed: $([ $total_secrets -eq $valid_secrets ] && echo "true" || echo "false")
|
|
EOF
|
|
|
|
log "✅ Secrets validation completed: $validation_report"
|
|
|
|
if [[ $total_secrets -eq $valid_secrets ]]; then
|
|
log "🎉 All secrets validated successfully"
|
|
else
|
|
log "❌ Some secrets failed validation"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Create secrets rotation script
|
|
create_rotation_script() {
|
|
log "Creating secrets rotation automation..."
|
|
|
|
cat > "$PROJECT_ROOT/scripts/rotate-secrets.sh" << 'EOF'
|
|
#!/bin/bash
|
|
# Automated secrets rotation script
|
|
|
|
set -euo pipefail
|
|
|
|
LOG_FILE="/var/log/secrets-rotation-$(date +%Y%m%d).log"
|
|
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
generate_password() {
|
|
openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
|
|
}
|
|
|
|
rotate_secret() {
|
|
local secret_name="$1"
|
|
local new_value="$2"
|
|
|
|
log "Rotating secret: $secret_name"
|
|
|
|
# Remove old secret
|
|
if docker secret inspect "$secret_name" >/dev/null 2>&1; then
|
|
# Get services using this secret
|
|
local services
|
|
services=$(docker service ls --format "{{.Name}}" | xargs -I {} docker service inspect {} --format '{{.Spec.TaskTemplate.ContainerSpec.Secrets}}' | grep -l "$secret_name" | wc -l || echo "0")
|
|
|
|
if [[ $services -gt 0 ]]; then
|
|
log "Warning: $services services are using $secret_name"
|
|
log "Manual intervention required for rotation"
|
|
return 1
|
|
fi
|
|
|
|
docker secret rm "$secret_name"
|
|
sleep 2
|
|
fi
|
|
|
|
# Create new secret
|
|
echo "$new_value" | docker secret create "$secret_name" -
|
|
log "✅ Secret $secret_name rotated successfully"
|
|
}
|
|
|
|
# Rotate non-critical secrets (quarterly)
|
|
rotate_secret "grafana_admin_password" "$(generate_password)"
|
|
rotate_secret "traefik_dashboard_password" "$(htpasswd -nbB admin $(generate_password 16) | cut -d: -f2)"
|
|
|
|
log "✅ Secrets rotation completed"
|
|
EOF
|
|
|
|
chmod +x "$PROJECT_ROOT/scripts/rotate-secrets.sh"
|
|
|
|
# Schedule quarterly rotation (first day of quarter at 3 AM)
|
|
local rotation_cron="0 3 1 1,4,7,10 * $PROJECT_ROOT/scripts/rotate-secrets.sh"
|
|
if ! crontab -l 2>/dev/null | grep -q "rotate-secrets.sh"; then
|
|
(crontab -l 2>/dev/null; echo "$rotation_cron") | crontab -
|
|
log "✅ Quarterly secrets rotation scheduled"
|
|
fi
|
|
}
|
|
|
|
# Generate comprehensive documentation
|
|
generate_documentation() {
|
|
log "Generating secrets management documentation..."
|
|
|
|
local docs_file="$SECRETS_DIR/SECRETS_MANAGEMENT.md"
|
|
cat > "$docs_file" << 'EOF'
|
|
# Secrets Management Documentation
|
|
|
|
## Overview
|
|
This document describes the comprehensive secrets management implementation for the HomeAudit infrastructure using Docker Secrets.
|
|
|
|
## Architecture
|
|
- **Docker Secrets**: Encrypted storage and distribution of sensitive data
|
|
- **File-based secrets**: Environment variables read from files in `/run/secrets/`
|
|
- **Automated rotation**: Quarterly rotation of non-critical secrets
|
|
- **Validation**: Regular integrity checks of secrets configuration
|
|
|
|
## Secrets Inventory
|
|
|
|
### Database Secrets
|
|
- `pg_root_password`: PostgreSQL root password
|
|
- `mariadb_root_password`: MariaDB root password
|
|
- `redis_password`: Redis authentication password
|
|
|
|
### Application Secrets
|
|
- `nextcloud_db_password`: Nextcloud database password
|
|
- `nextcloud_admin_password`: Nextcloud admin user password
|
|
- `immich_db_password`: Immich database password
|
|
- `paperless_secret_key`: Paperless-NGX secret key
|
|
- `vaultwarden_admin_token`: Vaultwarden admin access token
|
|
- `grafana_admin_password`: Grafana admin password
|
|
|
|
### API Tokens
|
|
- `ha_api_token`: Home Assistant API token
|
|
- `jellyfin_api_key`: Jellyfin API key
|
|
- `gitea_secret_key`: Gitea secret key
|
|
|
|
### TLS Certificates
|
|
- `tls_certificate`: TLS certificate for HTTPS
|
|
- `tls_private_key`: TLS private key
|
|
|
|
## Usage in Stack Files
|
|
|
|
### Environment Variables
|
|
```yaml
|
|
environment:
|
|
- POSTGRES_PASSWORD_FILE=/run/secrets/pg_root_password
|
|
- MYSQL_PASSWORD_FILE=/run/secrets/nextcloud_db_password
|
|
```
|
|
|
|
### Secrets Section
|
|
```yaml
|
|
secrets:
|
|
- pg_root_password
|
|
- nextcloud_db_password
|
|
|
|
# At the bottom of the stack file
|
|
secrets:
|
|
pg_root_password:
|
|
external: true
|
|
nextcloud_db_password:
|
|
external: true
|
|
```
|
|
|
|
## Management Commands
|
|
|
|
### Create Secret
|
|
```bash
|
|
echo "my-secret-value" | docker secret create my_secret_name -
|
|
```
|
|
|
|
### List Secrets
|
|
```bash
|
|
docker secret ls
|
|
```
|
|
|
|
### Inspect Secret (metadata only)
|
|
```bash
|
|
docker secret inspect my_secret_name
|
|
```
|
|
|
|
### Remove Secret
|
|
```bash
|
|
docker secret rm my_secret_name
|
|
```
|
|
|
|
## Rotation Process
|
|
1. Identify services using the secret
|
|
2. Plan maintenance window if needed
|
|
3. Generate new secret value
|
|
4. Remove old secret
|
|
5. Create new secret with same name
|
|
6. Update services if required (usually automatic)
|
|
|
|
## Security Best Practices
|
|
1. **Never log secret values**
|
|
2. **Use Docker Secrets for all sensitive data**
|
|
3. **Rotate secrets regularly**
|
|
4. **Monitor secret access**
|
|
5. **Use strong, unique passwords**
|
|
6. **Backup secret metadata (not values)**
|
|
|
|
## Troubleshooting
|
|
|
|
### Secret Not Found
|
|
- Check if secret exists: `docker secret ls`
|
|
- Verify secret name matches stack file
|
|
- Ensure secret is marked as external
|
|
|
|
### Permission Denied
|
|
- Check if service has access to secret
|
|
- Verify secret is listed in service's secrets section
|
|
- Check Docker Swarm permissions
|
|
|
|
### Service Won't Start
|
|
- Check logs: `docker service logs <service-name>`
|
|
- Verify secret file path is correct
|
|
- Test secret access in container
|
|
|
|
## Backup and Recovery
|
|
- **Metadata backup**: Export secret names and creation dates
|
|
- **Values backup**: Store encrypted copies of secret values securely
|
|
- **Recovery**: Recreate secrets from encrypted backup values
|
|
|
|
## Monitoring and Alerts
|
|
- Monitor secret creation/deletion
|
|
- Alert on failed secret access
|
|
- Track secret rotation schedule
|
|
- Validate secret integrity regularly
|
|
EOF
|
|
|
|
log "✅ Documentation created: $docs_file"
|
|
}
|
|
|
|
# Main execution
|
|
main() {
|
|
case "${1:-complete}" in
|
|
"--collect")
|
|
collect_existing_secrets
|
|
;;
|
|
"--generate")
|
|
generate_docker_secrets
|
|
create_secrets_mapping
|
|
;;
|
|
"--update-stacks")
|
|
update_stacks_with_secrets
|
|
;;
|
|
"--validate")
|
|
validate_secrets
|
|
;;
|
|
"--rotate")
|
|
create_rotation_script
|
|
;;
|
|
"--complete"|"")
|
|
log "Starting complete secrets management implementation..."
|
|
collect_existing_secrets
|
|
generate_docker_secrets
|
|
create_secrets_mapping
|
|
update_stacks_with_secrets
|
|
validate_secrets
|
|
create_rotation_script
|
|
generate_documentation
|
|
log "🎉 Complete secrets management implementation finished!"
|
|
;;
|
|
"--help"|"-h")
|
|
cat << 'EOF'
|
|
Complete Secrets Management Implementation
|
|
|
|
USAGE:
|
|
complete-secrets-management.sh [OPTIONS]
|
|
|
|
OPTIONS:
|
|
--collect Collect existing secrets from running containers
|
|
--generate Generate all required Docker secrets
|
|
--update-stacks Update stack files to use Docker secrets
|
|
--validate Validate secrets configuration
|
|
--rotate Set up secrets rotation automation
|
|
--complete Run complete implementation (default)
|
|
--help, -h Show this help message
|
|
|
|
EXAMPLES:
|
|
# Complete implementation
|
|
./complete-secrets-management.sh
|
|
|
|
# Just generate secrets
|
|
./complete-secrets-management.sh --generate
|
|
|
|
# Validate current configuration
|
|
./complete-secrets-management.sh --validate
|
|
|
|
NOTES:
|
|
- Requires Docker Swarm mode
|
|
- Creates backups before modifying files
|
|
- All secrets are encrypted at rest
|
|
- Documentation generated automatically
|
|
EOF
|
|
;;
|
|
*)
|
|
log "❌ Unknown option: $1"
|
|
log "Use --help for usage information"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Execute main function
|
|
main "$@" |