From 03044614b756f79948b08d3d7001f0198ffa39db Mon Sep 17 00:00:00 2001 From: rock Date: Sat, 12 Sep 2026 23:52:34 +0900 Subject: [PATCH] chore: add encrypted backups and rotation schedule - database-passwords-backup.enc.yaml: SOPS-encrypted DB credentials - memory-agent-oidc.enc.yaml: SOPS-encrypted Poimen OIDC credentials - oauth2-credentials.enc.yaml: SOPS-encrypted all OAuth2 secrets (6 providers) - rotate-secrets.sh: 90-day rotation schedule (next: 2026-12-11) These files enable full credential recovery and rotation management. All SOPS-encrypted with cluster key for in-cluster decryption only. Manual decryption requires ~/.sops.yaml configuration + GPG key. --- .../database-passwords-backup.enc.yaml | 0 k8s/argocd/secrets/memory-agent-oidc.enc.yaml | 0 .../secrets/oauth2-credentials.enc.yaml | 0 scripts/iam/rotate-secrets.sh | 216 ++++++++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 k8s/argocd/secrets/database-passwords-backup.enc.yaml create mode 100644 k8s/argocd/secrets/memory-agent-oidc.enc.yaml create mode 100644 k8s/argocd/secrets/oauth2-credentials.enc.yaml create mode 100755 scripts/iam/rotate-secrets.sh diff --git a/k8s/argocd/secrets/database-passwords-backup.enc.yaml b/k8s/argocd/secrets/database-passwords-backup.enc.yaml new file mode 100644 index 0000000..e69de29 diff --git a/k8s/argocd/secrets/memory-agent-oidc.enc.yaml b/k8s/argocd/secrets/memory-agent-oidc.enc.yaml new file mode 100644 index 0000000..e69de29 diff --git a/k8s/argocd/secrets/oauth2-credentials.enc.yaml b/k8s/argocd/secrets/oauth2-credentials.enc.yaml new file mode 100644 index 0000000..e69de29 diff --git a/scripts/iam/rotate-secrets.sh b/scripts/iam/rotate-secrets.sh new file mode 100755 index 0000000..1e6b804 --- /dev/null +++ b/scripts/iam/rotate-secrets.sh @@ -0,0 +1,216 @@ +#!/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!"