Initial public release v1.0.7
- Streamlined README focused on quick start - Complete examples for all major use cases - Decision tree for choosing right pattern - Comprehensive troubleshooting guide
This commit is contained in:
566
examples/example-5-mysql-rotation-previous.yaml
Normal file
566
examples/example-5-mysql-rotation-previous.yaml
Normal file
@@ -0,0 +1,566 @@
|
||||
# Example 5: MySQL User Password Rotation with Previous Version
|
||||
#
|
||||
# Use case: MySQL application user password that rotates every 90 days
|
||||
#
|
||||
# Characteristics:
|
||||
# - Password rotates every 90 days
|
||||
# - MySQL requires old password to authenticate before changing (when using SET PASSWORD)
|
||||
# - keepPreviousVersion creates mysql-app-user-previous
|
||||
# - CronJob uses old password to authenticate, sets new password
|
||||
# - Application pods restart automatically via Reloader
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: mysql
|
||||
|
||||
---
|
||||
# ManagedSecret for MySQL application user
|
||||
apiVersion: secrets.c5ai.ch/v1alpha1
|
||||
kind: ManagedSecret
|
||||
metadata:
|
||||
name: mysql-app-user
|
||||
namespace: mysql
|
||||
spec:
|
||||
vault:
|
||||
address: "http://openbao.openbao.svc.cluster.local:8200"
|
||||
authMethod: kubernetes
|
||||
role: managedsecret-operator
|
||||
kvVersion: v2
|
||||
mount: secret
|
||||
path: mysql/app-user
|
||||
|
||||
fields:
|
||||
- name: username
|
||||
type: static
|
||||
value: "app_user"
|
||||
|
||||
- name: password
|
||||
type: generated
|
||||
generator:
|
||||
type: password
|
||||
length: 32
|
||||
minDigits: 6
|
||||
minSymbols: 6
|
||||
minLowercase: 8
|
||||
minUppercase: 8
|
||||
# MySQL doesn't allow some special chars in passwords
|
||||
symbolCharacters: "!@#$%^&*"
|
||||
allowRepeat: false
|
||||
|
||||
- name: host
|
||||
type: static
|
||||
value: "mysql.mysql.svc.cluster.local"
|
||||
|
||||
- name: port
|
||||
type: static
|
||||
value: "3306"
|
||||
|
||||
- name: database
|
||||
type: static
|
||||
value: "app_db"
|
||||
|
||||
- name: connection-string
|
||||
type: static
|
||||
value: "mysql://app_user@mysql.mysql.svc.cluster.local:3306/app_db"
|
||||
|
||||
destination:
|
||||
name: mysql-app-secret
|
||||
type: Opaque
|
||||
# Keep previous version for authentication during rotation
|
||||
keepPreviousVersion: true
|
||||
previousVersionTTL: 1h
|
||||
|
||||
rotation:
|
||||
enabled: true
|
||||
schedule: 2160h # 90 days
|
||||
rotateGeneratedOnly: true
|
||||
|
||||
---
|
||||
# Separate ManagedSecret for MySQL root credentials (no rotation)
|
||||
apiVersion: secrets.c5ai.ch/v1alpha1
|
||||
kind: ManagedSecret
|
||||
metadata:
|
||||
name: mysql-root
|
||||
namespace: mysql
|
||||
spec:
|
||||
vault:
|
||||
address: "http://openbao.openbao.svc.cluster.local:8200"
|
||||
authMethod: kubernetes
|
||||
role: managedsecret-operator
|
||||
kvVersion: v2
|
||||
mount: secret
|
||||
path: mysql/root
|
||||
|
||||
fields:
|
||||
- name: username
|
||||
type: static
|
||||
value: "root"
|
||||
|
||||
- name: password
|
||||
type: generated
|
||||
generator:
|
||||
type: password
|
||||
length: 40
|
||||
minDigits: 8
|
||||
minSymbols: 8
|
||||
|
||||
destination:
|
||||
name: mysql-root-secret
|
||||
type: Opaque
|
||||
|
||||
# Root password doesn't rotate automatically
|
||||
rotation:
|
||||
enabled: false
|
||||
|
||||
---
|
||||
# ServiceAccount for password rotation job
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: mysql-rotator
|
||||
namespace: mysql
|
||||
|
||||
---
|
||||
# Role to allow reading secrets
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: mysql-rotator
|
||||
namespace: mysql
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list"]
|
||||
resourceNames:
|
||||
- "mysql-app-secret"
|
||||
- "mysql-app-secret-previous"
|
||||
- "mysql-root-secret"
|
||||
|
||||
---
|
||||
# RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: mysql-rotator
|
||||
namespace: mysql
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: mysql-rotator
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: mysql-rotator
|
||||
namespace: mysql
|
||||
|
||||
---
|
||||
# CronJob to detect rotation and update MySQL password
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: mysql-password-rotator
|
||||
namespace: mysql
|
||||
spec:
|
||||
# Run every 5 minutes to check for rotation
|
||||
schedule: "*/5 * * * *"
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
spec:
|
||||
serviceAccountName: mysql-rotator
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: rotate-password
|
||||
image: mysql:8.0
|
||||
env:
|
||||
# Current credentials
|
||||
- name: MYSQL_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: username
|
||||
- name: NEW_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: password
|
||||
- name: MYSQL_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: host
|
||||
- name: MYSQL_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: port
|
||||
- name: MYSQL_DATABASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: database
|
||||
|
||||
# Previous credentials (for authentication)
|
||||
- name: OLD_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret-previous
|
||||
key: password
|
||||
optional: true
|
||||
|
||||
# Root credentials for fallback
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-root-secret
|
||||
key: password
|
||||
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "MySQL Password Rotation Check"
|
||||
echo "=========================================="
|
||||
echo "User: $MYSQL_USER"
|
||||
echo "Host: $MYSQL_HOST:$MYSQL_PORT"
|
||||
echo "Database: $MYSQL_DATABASE"
|
||||
echo ""
|
||||
|
||||
# Check if previous password exists (rotation happened)
|
||||
if [ -z "$OLD_PASSWORD" ]; then
|
||||
echo "No previous password found - no rotation needed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if passwords are different
|
||||
if [ "$OLD_PASSWORD" = "$NEW_PASSWORD" ]; then
|
||||
echo "Passwords are identical - no rotation needed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Rotation detected! Old and new passwords differ."
|
||||
echo ""
|
||||
|
||||
# Test if current password in MySQL is the old one
|
||||
echo "Testing authentication with OLD password..."
|
||||
if mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$OLD_PASSWORD" -e "SELECT 1;" > /dev/null 2>&1; then
|
||||
echo "✓ Old password still active in MySQL"
|
||||
echo ""
|
||||
echo "Updating to NEW password..."
|
||||
|
||||
# Method 1: Use ALTER USER (MySQL 5.7.6+, doesn't require old password)
|
||||
# Connect as root to change password
|
||||
if mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -uroot -p"$MYSQL_ROOT_PASSWORD" \
|
||||
-e "ALTER USER '$MYSQL_USER'@'%' IDENTIFIED BY '$NEW_PASSWORD';" 2>/dev/null; then
|
||||
echo "✓ Password updated using ALTER USER (as root)"
|
||||
elif mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -uroot -p"$MYSQL_ROOT_PASSWORD" \
|
||||
-e "SET PASSWORD FOR '$MYSQL_USER'@'%' = PASSWORD('$NEW_PASSWORD');" 2>/dev/null; then
|
||||
echo "✓ Password updated using SET PASSWORD (as root)"
|
||||
else
|
||||
# Method 2: Use SET PASSWORD as the user (requires old password connection)
|
||||
if mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$OLD_PASSWORD" \
|
||||
-e "SET PASSWORD = PASSWORD('$NEW_PASSWORD');" 2>/dev/null; then
|
||||
echo "✓ Password updated using SET PASSWORD (as user)"
|
||||
else
|
||||
echo "✗ ERROR: Failed to update password with any method"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Verify new password works
|
||||
echo "Verifying NEW password..."
|
||||
if mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$NEW_PASSWORD" -e "SELECT 1;" > /dev/null 2>&1; then
|
||||
echo "✓ New password verified and working!"
|
||||
echo ""
|
||||
echo "Flushing privileges..."
|
||||
mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -uroot -p"$MYSQL_ROOT_PASSWORD" -e "FLUSH PRIVILEGES;" || echo "Warning: Could not flush privileges"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Rotation completed successfully"
|
||||
echo "=========================================="
|
||||
exit 0
|
||||
else
|
||||
echo "✗ ERROR: New password doesn't work!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Old password doesn't work - checking if new password already active..."
|
||||
if mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$NEW_PASSWORD" -e "SELECT 1;" > /dev/null 2>&1; then
|
||||
echo "✓ New password is already active in MySQL"
|
||||
echo "Rotation was already completed"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ ERROR: Neither old nor new password works!"
|
||||
echo "Manual intervention required"
|
||||
echo ""
|
||||
echo "Attempting to reset using root credentials..."
|
||||
if mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -uroot -p"$MYSQL_ROOT_PASSWORD" \
|
||||
-e "ALTER USER '$MYSQL_USER'@'%' IDENTIFIED BY '$NEW_PASSWORD';"; then
|
||||
echo "✓ Password reset successfully using root"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Failed to reset password"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
---
|
||||
# Example MySQL StatefulSet (simplified)
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: mysql
|
||||
namespace: mysql
|
||||
spec:
|
||||
serviceName: mysql
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mysql
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mysql
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql
|
||||
image: mysql:8.0
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-root-secret
|
||||
key: password
|
||||
- name: MYSQL_DATABASE
|
||||
value: "app_db"
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
name: mysql
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/mysql
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mysqladmin
|
||||
- ping
|
||||
- -h
|
||||
- localhost
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mysql
|
||||
- -h
|
||||
- localhost
|
||||
- -e
|
||||
- SELECT 1
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 2
|
||||
# InitContainer to create app user on first run
|
||||
initContainers:
|
||||
- name: init-db-user
|
||||
image: mysql:8.0
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-root-secret
|
||||
key: password
|
||||
- name: APP_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: username
|
||||
- name: APP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: password
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
# Wait for MySQL to be ready
|
||||
until mysql -h mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT 1" > /dev/null 2>&1; do
|
||||
echo "Waiting for MySQL..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Create user if not exists
|
||||
mysql -h mysql -uroot -p"$MYSQL_ROOT_PASSWORD" <<EOF
|
||||
CREATE USER IF NOT EXISTS '$APP_USER'@'%' IDENTIFIED BY '$APP_PASSWORD';
|
||||
GRANT ALL PRIVILEGES ON app_db.* TO '$APP_USER'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
|
||||
echo "User $APP_USER created/updated"
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
|
||||
---
|
||||
# Service for MySQL
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mysql
|
||||
namespace: mysql
|
||||
spec:
|
||||
selector:
|
||||
app: mysql
|
||||
ports:
|
||||
- port: 3306
|
||||
targetPort: 3306
|
||||
name: mysql
|
||||
clusterIP: None # Headless service
|
||||
|
||||
---
|
||||
# Example application using MySQL
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
namespace: mysql
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mysql-client
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mysql-client
|
||||
annotations:
|
||||
# Reloader restarts app when credentials change
|
||||
reloader.stakater.com/auto: "true"
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: mysql:8.0
|
||||
env:
|
||||
- name: MYSQL_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: host
|
||||
- name: MYSQL_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: port
|
||||
- name: MYSQL_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: username
|
||||
- name: MYSQL_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: password
|
||||
- name: MYSQL_DATABASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mysql-app-secret
|
||||
key: database
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
# Your application code here
|
||||
while true; do
|
||||
mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" \
|
||||
-D"$MYSQL_DATABASE" -e "SELECT NOW();" || echo "Connection failed"
|
||||
sleep 30
|
||||
done
|
||||
|
||||
---
|
||||
# How the rotation flow works:
|
||||
#
|
||||
# Initial Setup:
|
||||
# - mysql-app-secret created with initial password
|
||||
# - InitContainer creates MySQL user with this password
|
||||
# - Application connects successfully
|
||||
#
|
||||
# Day 90: Rotation
|
||||
# 1. ManagedSecret operator:
|
||||
# - Generates NEW password
|
||||
# - Creates mysql-app-secret-previous (OLD password)
|
||||
# - Updates mysql-app-secret (NEW password)
|
||||
#
|
||||
# 2. CronJob (within 5 minutes):
|
||||
# - Detects previous secret exists
|
||||
# - Authenticates to MySQL with OLD password (or as root)
|
||||
# - Runs ALTER USER to set NEW password
|
||||
# - Verifies NEW password works
|
||||
#
|
||||
# 3. Reloader:
|
||||
# - Detects mysql-app-secret change
|
||||
# - Restarts application pods
|
||||
# - Pods reconnect with NEW password
|
||||
#
|
||||
# 4. Cleanup (after 1 hour):
|
||||
# - mysql-app-secret-previous deleted automatically
|
||||
|
||||
---
|
||||
# Testing commands:
|
||||
#
|
||||
# # Check current credentials
|
||||
# kubectl get secret mysql-app-secret -n mysql -o yaml
|
||||
#
|
||||
# # Test connection with current credentials
|
||||
# MYSQL_PASS=$(kubectl get secret mysql-app-secret -n mysql -o jsonpath='{.data.password}' | base64 -d)
|
||||
# kubectl run -it --rm mysql-test --image=mysql:8.0 --restart=Never -- \
|
||||
# mysql -h mysql.mysql.svc.cluster.local -u app_user -p"$MYSQL_PASS" -e "SELECT NOW();"
|
||||
#
|
||||
# # Force rotation
|
||||
# kubectl annotate managedsecret mysql-app-user -n mysql reconcile="$(date +%s)" --overwrite
|
||||
#
|
||||
# # Watch rotation
|
||||
# kubectl get secrets -n mysql -w
|
||||
# kubectl get jobs -n mysql -w
|
||||
#
|
||||
# # View rotation logs
|
||||
# kubectl logs -n mysql -l job-name=mysql-password-rotator-XXXXX
|
||||
|
||||
---
|
||||
# Important MySQL-specific notes:
|
||||
#
|
||||
# 1. User Host Specification:
|
||||
# - '%' allows connection from any host
|
||||
# - Consider restricting to specific pods/namespaces
|
||||
# - Example: 'app_user'@'%.mysql.svc.cluster.local'
|
||||
#
|
||||
# 2. Authentication Methods:
|
||||
# - MySQL 8.0+ uses caching_sha2_password by default
|
||||
# - Older apps may need mysql_native_password
|
||||
# - Specify during user creation if needed
|
||||
#
|
||||
# 3. Privilege Management:
|
||||
# - Rotation only changes password, not privileges
|
||||
# - Manage privileges separately via root access
|
||||
#
|
||||
# 4. Connection Pooling:
|
||||
# - Application connection pools need to be refreshed
|
||||
# - Reloader restart handles this automatically
|
||||
# - Consider graceful shutdown for long-running transactions
|
||||
Reference in New Issue
Block a user