Manage Secrets#
How to create and manage Kubernetes secrets required by Mailu.
Problem#
Mailu requires several secrets for secure operation: application secret keys, database credentials, Redis passwords, and initial admin passwords. You need to create these secrets before deploying Mailu.
Solution#
Create Kubernetes Secret resources containing sensitive data, then reference them in the MailuChart configuration. Never commit secrets to git or include them in CDK8S code.
Understanding Secret Requirements#
Mailu requires these secrets (minimum):
Secret Purpose |
Required? |
Secret Name (configurable) |
Keys |
|---|---|---|---|
Mailu secret key |
Yes |
|
|
Database credentials |
Yes (if PostgreSQL) |
|
|
Initial admin password |
Yes (recommended) |
|
|
Redis password |
No (if Redis auth disabled) |
|
|
API token |
No (if API disabled) |
|
|
Create Mailu Secret Key#
The Mailu secret key is used for session encryption and must be a random 16-character string.
Generate Random Secret Key#
# Generate 16-character random key
SECRET_KEY=$(head -c 16 /dev/urandom | base64 | tr -d '=' | cut -c1-16)
echo $SECRET_KEY
Create Secret#
kubectl create secret generic mailu-secrets \
--namespace=mailu \
--from-literal=secret-key="$SECRET_KEY"
Verify:
kubectl get secret -n mailu mailu-secrets
kubectl describe secret -n mailu mailu-secrets
Create Initial Admin Password#
Generate a secure password for the initial admin account:
# Generate strong random password
ADMIN_PASSWORD=$(head -c 24 /dev/urandom | base64 | tr -d '=' | cut -c1-24)
echo "Admin password: $ADMIN_PASSWORD"
# Add password to existing mailu-secrets
kubectl create secret generic mailu-secrets \
--namespace=mailu \
--from-literal=secret-key="$SECRET_KEY" \
--from-literal=password="$ADMIN_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
Important: Save this password securely (password manager). You’ll need it to login to the admin UI after deployment.
Create Database Credentials Secret#
For PostgreSQL (Recommended)#
If using CloudNativePG (CNPG) or another PostgreSQL operator, the secret is usually auto-generated. Reference the existing secret:
database: {
type: 'postgresql',
postgresql: {
host: 'postgres-pooler', // CNPG pooler service
port: 5432,
database: 'mailu',
secretName: 'postgres-app', // CNPG auto-generated secret
secretKeys: {
username: 'username',
password: 'password',
},
},
}
Manual PostgreSQL: Create the secret yourself:
kubectl create secret generic postgres-credentials \
--namespace=mailu \
--from-literal=username="mailu" \
--from-literal=password="$(head -c 32 /dev/urandom | base64 | tr -d '=')"
Reference in CDK8S:
database: {
type: 'postgresql',
postgresql: {
host: 'postgres-service',
secretName: 'postgres-credentials',
secretKeys: {
username: 'username',
password: 'password',
},
},
}
For SQLite (Not Recommended for Production)#
No database secret needed (SQLite file stored in admin PVC):
database: {
type: 'sqlite',
}
Create Redis Password Secret (Optional)#
If your Redis instance requires authentication:
kubectl create secret generic redis-password \
--namespace=mailu \
--from-literal=password="$(head -c 32 /dev/urandom | base64 | tr -d '=')"
Reference in CDK8S:
redis: {
host: 'redis',
port: 6379,
secretName: 'redis-password',
secretKey: 'password',
}
If Redis has no authentication (cluster-internal only):
redis: {
host: 'redis',
port: 6379,
// No secretName needed
}
Example: Complete Secret Setup#
Script to create all required secrets at once:
#!/bin/bash
set -e
NAMESPACE="mailu"
echo "Creating Mailu secrets..."
# Generate random values
SECRET_KEY=$(head -c 16 /dev/urandom | base64 | tr -d '=' | cut -c1-16)
ADMIN_PASSWORD=$(head -c 24 /dev/urandom | base64 | tr -d '=' | cut -c1-24)
DB_PASSWORD=$(head -c 32 /dev/urandom | base64 | tr -d '=')
echo "Generated credentials:"
echo " SECRET_KEY: $SECRET_KEY"
echo " ADMIN_PASSWORD: $ADMIN_PASSWORD"
echo " (Save these securely!)"
echo ""
# Create namespace
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
# Mailu application secrets
kubectl create secret generic mailu-secrets \
--namespace=$NAMESPACE \
--from-literal=secret-key="$SECRET_KEY" \
--from-literal=password="$ADMIN_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
# Database credentials (if not using CNPG auto-generated)
kubectl create secret generic postgres-credentials \
--namespace=$NAMESPACE \
--from-literal=username="mailu" \
--from-literal=password="$DB_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Secrets created successfully!"
kubectl get secrets -n $NAMESPACE
Save as create-mailu-secrets.sh, make executable, and run:
chmod +x create-mailu-secrets.sh
./create-mailu-secrets.sh
Configure Secrets in CDK8S#
After creating secrets, reference them in your MailuChart:
import { App } from 'cdk8s';
import { MailuChart } from 'cdk8s-mailu';
const app = new App();
new MailuChart(app, 'mailu', {
namespace: 'mailu',
domain: 'example.com',
hostnames: ['mail.example.com'],
subnet: '10.42.0.0/16',
timezone: 'UTC',
// Reference secrets (not values!)
secrets: {
mailuSecretKey: 'mailu-secrets', // Secret with 'secret-key' field
initialAdminPassword: 'mailu-secrets', // Secret with 'password' field
apiToken: 'mailu-api-token', // Optional, if API enabled
},
database: {
type: 'postgresql',
postgresql: {
host: 'postgres-pooler',
port: 5432,
database: 'mailu',
secretName: 'postgres-app', // Database credentials
secretKeys: {
username: 'username',
password: 'password',
},
},
},
redis: {
host: 'redis',
port: 6379,
// secretName: 'redis-password', // Uncomment if Redis requires auth
// secretKey: 'password',
},
});
app.synth();
Rotate Secrets#
Rotate Mailu Secret Key#
Warning: Rotating the secret key invalidates all user sessions. Users must re-login.
# Generate new key
NEW_SECRET_KEY=$(head -c 16 /dev/urandom | base64 | tr -d '=' | cut -c1-16)
# Update secret
kubectl create secret generic mailu-secrets \
--namespace=mailu \
--from-literal=secret-key="$NEW_SECRET_KEY" \
--from-literal=password="<existing-admin-password>" \
--dry-run=client -o yaml | kubectl apply -f -
# Restart admin pods to pick up new secret
kubectl rollout restart deployment -n mailu mailu-admin
Rotate Database Password#
Caution: Requires updating both database and Mailu secret simultaneously.
# Step 1: Update password in PostgreSQL
kubectl exec -n postgres postgres-1 -- psql -U postgres -c "ALTER USER mailu PASSWORD 'new-password';"
# Step 2: Update Kubernetes secret
kubectl create secret generic postgres-credentials \
--namespace=mailu \
--from-literal=username="mailu" \
--from-literal=password="new-password" \
--dry-run=client -o yaml | kubectl apply -f -
# Step 3: Restart Mailu admin pod
kubectl rollout restart deployment -n mailu mailu-admin
Use External Secret Management#
For production, consider using external secret management with External Secrets Operator:
Example: Sync from HashiCorp Vault#
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: mailu
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "mailu"
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: mailu-secrets-external
namespace: mailu
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: mailu-secrets
creationPolicy: Owner
data:
- secretKey: secret-key
remoteRef:
key: mailu/secret-key
- secretKey: password
remoteRef:
key: mailu/admin-password
Verify Secret Usage#
Check if secrets are correctly mounted to pods:
# Check admin pod environment
kubectl exec -n mailu <admin-pod-name> -- env | grep SECRET_KEY
# Should show: SECRET_KEY=<your-secret-key>
# Check database connection
kubectl logs -n mailu <admin-pod-name> | grep -i database
# Should show successful database connection
Troubleshooting#
Secret not found error#
Symptom: Pod shows CreateContainerConfigError with message about missing secret.
# Check secret exists
kubectl get secret -n mailu <secret-name>
# Check secret has correct keys
kubectl get secret -n mailu <secret-name> -o jsonpath='{.data}' | jq
Solution: Create the missing secret or fix the secret name in your CDK8S configuration.
Database authentication failed#
Symptom: Admin pod logs show “authentication failed” or “password authentication failed for user”.
# Check database secret
kubectl get secret -n mailu <db-secret-name> -o jsonpath='{.data.password}' | base64 -d
# Test database connection manually
kubectl exec -n mailu <admin-pod> -- psql -h <db-host> -U <username> -d mailu
Solution: Verify database password matches between Kubernetes secret and PostgreSQL database.
Admin login fails after deployment#
Symptom: Cannot login to admin UI with generated password.
Common causes:
Initial admin account not created (check
mailu.initialAccountconfig)Password secret not mounted correctly
Wrong username (should be configured username @ domain)
Solution: Check admin pod logs for initial account creation:
kubectl logs -n mailu <admin-pod> | grep -i "initial admin"
Security Best Practices#
Never commit secrets to git - Use
.gitignorefor secret filesUse strong random passwords - At least 24 characters for admin passwords
Rotate secrets regularly - Especially after employee turnover
Limit secret access - Use Kubernetes RBAC to restrict secret read permissions
Use external secret management - Vault, AWS Secrets Manager, etc. for production
Encrypt etcd - Enable etcd encryption at rest in your Kubernetes cluster
Audit secret access - Enable audit logging for secret operations
See Also#
Quick Start Tutorial - Initial deployment with secrets
External Secrets Operator - Sync secrets from external stores
Kubernetes Secrets Documentation - Official Kubernetes docs