#!/bin/bash # Secret Rotation Script # Rotates all OAuth2 and service account credentials # Should be run quarterly (every 90 days) # # Usage: ./rotate-secrets.sh [--dry-run] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" DRY_RUN=${1:-} # Color output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } log "=== Secret Rotation Script ===" log "Rotation Date: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" if [[ -n "$DRY_RUN" ]]; then log "Running in DRY-RUN mode (no changes will be applied)" fi # Verify prerequisites log "Checking prerequisites..." command -v kubectl &>/dev/null || error "kubectl not found" command -v openssl &>/dev/null || error "openssl not found" command -v sops &>/dev/null || error "sops not found" command -v jq &>/dev/null || error "jq not found" # Check kubeconfig kubectl cluster-info &>/dev/null || error "Not connected to cluster" # Get bootstrap token log "Retrieving Authentik bootstrap token..." BOOTSTRAP_TOKEN=$(kubectl -n iam get secret authentik-secrets \ -o jsonpath='{.data.AUTHENTIK_BOOTSTRAP_TOKEN}' 2>/dev/null | base64 -d) || \ error "Failed to get bootstrap token" # Generate new secrets (13 OAuth2 + service accounts) log "Generating 13 new secrets (256-bit)..." generate_secret() { openssl rand -base64 32 } declare -A NEW_SECRETS for svc in api-gw minio poimen paperless grafana argocd forgejo homarr immich vault portfolio-agent memory-agent local-llm; do NEW_SECRETS[$svc]=$(generate_secret) log " $svc: ${NEW_SECRETS[$svc]:0:15}..." done log "" log "=== Updating Authentik OAuth2 Providers ===" # Authentik provider mapping declare -A PROVIDER_PKS=( [api-gw]=2 [minio]=3 [poimen]=4 [paperless]=5 [grafana]=6 [argocd]=7 [forgejo]=8 [homarr]=9 [immich]=10 [vault]=11 ) for provider in "${!PROVIDER_PKS[@]}"; do pk=${PROVIDER_PKS[$provider]} secret=${NEW_SECRETS[$provider]} log "Updating $provider (pk=$pk)..." if [[ -z "$DRY_RUN" ]]; then response=$(curl -s -X PATCH "https://authentik.riotpiao.com/api/v3/providers/oauth2/$pk/" \ -H "Authorization: Bearer $BOOTSTRAP_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"client_secret\": \"$secret\"}") if echo "$response" | jq -e '.pk' &>/dev/null; then log " ✅ $provider updated" else error "Failed to update $provider: $(echo "$response" | jq '.detail // .')" fi fi done log "" log "=== Updating k8s Secrets ===" # Update api-gw if [[ -z "$DRY_RUN" ]]; then log "Patching api/api-gateway-oauth2-creds..." kubectl -n api patch secret api-gateway-oauth2-creds \ -p "{\"data\":{\"client-secret\":\"$(echo -n "${NEW_SECRETS[api-gw]}" | base64)\"}}" --type=merge 2>/dev/null || true fi # Update minio (skip if namespace doesn't exist) if kubectl get ns minio &>/dev/null 2>&1; then if [[ -z "$DRY_RUN" ]]; then log "Patching minio/minio-oauth2-creds..." kubectl -n minio patch secret minio-oauth2-creds \ -p "{\"data\":{\"client-secret\":\"$(echo -n "${NEW_SECRETS[minio]}" | base64)\"}}" --type=merge 2>/dev/null || true fi fi # Update poimen if [[ -z "$DRY_RUN" ]]; then log "Patching poimen/poimen-oauth2-creds..." kubectl -n poimen patch secret poimen-oauth2-creds \ -p "{\"data\":{\"client-secret\":\"$(echo -n "${NEW_SECRETS[poimen]}" | base64)\"}}" --type=merge 2>/dev/null || true fi # Update paperless if [[ -z "$DRY_RUN" ]]; then log "Patching paperless/paperless-oauth2-creds..." kubectl -n paperless patch secret paperless-oauth2-creds \ -p "{\"data\":{\"client-secret\":\"$(echo -n "${NEW_SECRETS[paperless]}" | base64)\"}}" --type=merge 2>/dev/null || true fi # Update logging/grafana if [[ -z "$DRY_RUN" ]]; then log "Patching logging/grafana-oauth2-creds..." kubectl -n logging patch secret grafana-oauth2-creds \ -p "{\"data\":{\"client-secret\":\"$(echo -n "${NEW_SECRETS[grafana]}" | base64)\"}}" --type=merge 2>/dev/null || true fi # Update service accounts if [[ -z "$DRY_RUN" ]]; then log "Patching portfolio/portfolio-agent-oidc..." kubectl -n portfolio patch secret portfolio-agent-oidc \ -p "{\"data\":{\"CLIENT_SECRET\":\"$(echo -n "${NEW_SECRETS[portfolio-agent]}" | base64)\"}}" --type=merge 2>/dev/null || true log "Patching poimen/memory-agent-oidc..." kubectl -n poimen patch secret memory-agent-oidc \ -p "{\"data\":{\"CLIENT_SECRET\":\"$(echo -n "${NEW_SECRETS[memory-agent]}" | base64)\"}}" --type=merge 2>/dev/null || true log "Patching llm-serving/local-llm-jwt..." kubectl -n llm-serving patch secret local-llm-jwt \ -p "{\"data\":{\"client-secret\":\"$(echo -n "${NEW_SECRETS[local-llm]}" | base64)\"}}" --type=merge 2>/dev/null || true fi log "" log "=== Updating SOPS-encrypted manifests ===" # Create oauth2-credentials.enc.yaml cat > "$REPO_ROOT/k8s/argocd/secrets/oauth2-credentials.yaml" << 'OAUTH_EOF' apiVersion: v1 kind: Secret metadata: name: oauth2-credentials namespace: iam type: Opaque data: OAUTH_EOF for svc in api-gw minio poimen paperless grafana; do echo " ${svc}-client-secret: $(echo -n "${NEW_SECRETS[$svc]}" | base64)" >> \ "$REPO_ROOT/k8s/argocd/secrets/oauth2-credentials.yaml" done if [[ -z "$DRY_RUN" ]]; then log "Encrypting oauth2-credentials.yaml with SOPS..." sops -e "$REPO_ROOT/k8s/argocd/secrets/oauth2-credentials.yaml" > \ "$REPO_ROOT/k8s/argocd/secrets/oauth2-credentials.enc.yaml" rm "$REPO_ROOT/k8s/argocd/secrets/oauth2-credentials.yaml" log " ✅ oauth2-credentials.enc.yaml created" fi # Create memory-agent-oidc.enc.yaml cat > "$REPO_ROOT/k8s/argocd/secrets/memory-agent-oidc.yaml" << AGENT_EOF apiVersion: v1 kind: Secret metadata: name: memory-agent-oidc namespace: poimen type: Opaque data: CLIENT_ID: bWVtb3J5LWFnZW50 CLIENT_SECRET: $(echo -n "${NEW_SECRETS[memory-agent]}" | base64) ISSUER: aHR0cHM6Ly9hdXRoZW50aWsucmlvdHBpYW8uY29tL2FwcGxpY2F0aW9uL28v TOKEN_URL: aHR0cHM6Ly9hdXRoZW50aWsucmlvdHBpYW8uY29tL2FwcGxpY2F0aW9uL28vdG9rZW4v AGENT_EOF if [[ -z "$DRY_RUN" ]]; then log "Encrypting memory-agent-oidc.yaml with SOPS..." sops -e "$REPO_ROOT/k8s/argocd/secrets/memory-agent-oidc.yaml" > \ "$REPO_ROOT/k8s/argocd/secrets/memory-agent-oidc.enc.yaml" rm "$REPO_ROOT/k8s/argocd/secrets/memory-agent-oidc.yaml" log " ✅ memory-agent-oidc.enc.yaml created" fi log "" log "=== Summary ===" log "Rotated 13 credentials:" log " OAuth2 Providers: api-gw, minio, poimen, paperless, grafana, argocd, forgejo, homarr, immich, vault" log " Service Accounts: portfolio-agent, memory-agent, local-llm" log "" log "Next steps:" log " 1. Review changes: git diff k8s/argocd/secrets/" log " 2. Commit: git add k8s/argocd/secrets/oauth2-credentials.enc.yaml" log " 3. Commit message: 'chore: rotate OAuth2 secrets (quarterly)'" log " 4. Push: git push" log "" log "✅ Rotation complete!"