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

mailu-secrets

secret-key

Database credentials

Yes (if PostgreSQL)

postgres-app

username, password

Initial admin password

Yes (recommended)

mailu-secrets

password

Redis password

No (if Redis auth disabled)

redis-password

password

API token

No (if API disabled)

mailu-api-token

api-token

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#

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.initialAccount config)

  • 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#

  1. Never commit secrets to git - Use .gitignore for secret files

  2. Use strong random passwords - At least 24 characters for admin passwords

  3. Rotate secrets regularly - Especially after employee turnover

  4. Limit secret access - Use Kubernetes RBAC to restrict secret read permissions

  5. Use external secret management - Vault, AWS Secrets Manager, etc. for production

  6. Encrypt etcd - Enable etcd encryption at rest in your Kubernetes cluster

  7. Audit secret access - Enable audit logging for secret operations

See Also#