From b82195f2d7bd778468604671b69cbcee05dfb7d2 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 14 Jan 2026 01:30:41 -0300 Subject: [PATCH] feat: start vault consumption for outline and planka --- .../vault-csi/secrets-store-csi-driver.yaml | 2 +- services/outline/deployment.yaml | 31 ++++++--- services/outline/kustomization.yaml | 8 +++ services/outline/scripts/outline_vault_env.sh | 31 +++++++++ services/outline/secretproviderclass.yaml | 63 +++++++++++++++++++ services/outline/serviceaccount.yaml | 6 ++ services/planka/deployment.yaml | 33 ++++++---- services/planka/kustomization.yaml | 8 +++ services/planka/scripts/planka_vault_env.sh | 27 ++++++++ services/planka/secretproviderclass.yaml | 60 ++++++++++++++++++ services/planka/serviceaccount.yaml | 6 ++ services/vault/k8s-auth-config-cronjob.yaml | 47 ++++++++++++++ services/vault/kustomization.yaml | 4 ++ .../vault/scripts/vault_k8s_auth_configure.sh | 62 ++++++++++++++++++ 14 files changed, 367 insertions(+), 21 deletions(-) create mode 100644 services/outline/scripts/outline_vault_env.sh create mode 100644 services/outline/secretproviderclass.yaml create mode 100644 services/outline/serviceaccount.yaml create mode 100644 services/planka/scripts/planka_vault_env.sh create mode 100644 services/planka/secretproviderclass.yaml create mode 100644 services/planka/serviceaccount.yaml create mode 100644 services/vault/k8s-auth-config-cronjob.yaml create mode 100644 services/vault/scripts/vault_k8s_auth_configure.sh diff --git a/infrastructure/vault-csi/secrets-store-csi-driver.yaml b/infrastructure/vault-csi/secrets-store-csi-driver.yaml index 0b249fc..fec4758 100644 --- a/infrastructure/vault-csi/secrets-store-csi-driver.yaml +++ b/infrastructure/vault-csi/secrets-store-csi-driver.yaml @@ -16,5 +16,5 @@ spec: namespace: flux-system values: syncSecret: - enabled: true + enabled: false enableSecretRotation: false diff --git a/services/outline/deployment.yaml b/services/outline/deployment.yaml index 9f8160e..2cacceb 100644 --- a/services/outline/deployment.yaml +++ b/services/outline/deployment.yaml @@ -21,6 +21,7 @@ spec: labels: app: outline spec: + serviceAccountName: outline-vault nodeSelector: node-role.kubernetes.io/worker: "true" affinity: @@ -34,6 +35,11 @@ spec: containers: - name: outline image: outlinewiki/outline:1.2.0 + command: + - /bin/sh + - -c + args: + - . /vault/scripts/outline_vault_env.sh && exec node build/server/index.js ports: - name: http containerPort: 3000 @@ -66,18 +72,15 @@ spec: value: "false" - name: SMTP_PORT value: "25" - envFrom: - - secretRef: - name: outline-db - - secretRef: - name: outline-secrets - - secretRef: - name: outline-oidc - - secretRef: - name: outline-smtp volumeMounts: - name: user-data mountPath: /var/lib/outline/data + - name: vault-secrets + mountPath: /vault/secrets + readOnly: true + - name: vault-scripts + mountPath: /vault/scripts + readOnly: true readinessProbe: httpGet: path: /_health @@ -105,3 +108,13 @@ spec: - name: user-data persistentVolumeClaim: claimName: outline-user-data + - name: vault-secrets + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: outline-vault + - name: vault-scripts + configMap: + name: outline-vault-env + defaultMode: 0555 diff --git a/services/outline/kustomization.yaml b/services/outline/kustomization.yaml index 33640f6..011c6e6 100644 --- a/services/outline/kustomization.yaml +++ b/services/outline/kustomization.yaml @@ -4,9 +4,17 @@ kind: Kustomization namespace: outline resources: - namespace.yaml + - serviceaccount.yaml + - secretproviderclass.yaml - user-pvc.yaml - redis-deployment.yaml - redis-service.yaml - deployment.yaml - service.yaml - ingress.yaml +generatorOptions: + disableNameSuffixHash: true +configMapGenerator: + - name: outline-vault-env + files: + - outline_vault_env.sh=scripts/outline_vault_env.sh diff --git a/services/outline/scripts/outline_vault_env.sh b/services/outline/scripts/outline_vault_env.sh new file mode 100644 index 0000000..d9f8469 --- /dev/null +++ b/services/outline/scripts/outline_vault_env.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env sh +set -eu + +vault_dir="/vault/secrets" + +read_secret() { + cat "${vault_dir}/$1" +} + +export DATABASE_URL="$(read_secret DATABASE_URL)" +export SECRET_KEY="$(read_secret SECRET_KEY)" +export UTILS_SECRET="$(read_secret UTILS_SECRET)" + +export OIDC_AUTH_URI="$(read_secret OIDC_AUTH_URI)" +export OIDC_CLIENT_ID="$(read_secret OIDC_CLIENT_ID)" +export OIDC_CLIENT_SECRET="$(read_secret OIDC_CLIENT_SECRET)" +export OIDC_LOGOUT_URI="$(read_secret OIDC_LOGOUT_URI)" +export OIDC_TOKEN_URI="$(read_secret OIDC_TOKEN_URI)" +export OIDC_USERINFO_URI="$(read_secret OIDC_USERINFO_URI)" + +export SMTP_FROM_EMAIL="$(read_secret SMTP_FROM_EMAIL)" +export SMTP_HOST="$(read_secret SMTP_HOST)" +export SMTP_PASSWORD="$(read_secret SMTP_PASSWORD)" +export SMTP_USERNAME="$(read_secret SMTP_USERNAME)" + +if [ -f "${vault_dir}/AWS_ACCESS_KEY_ID" ]; then + export AWS_ACCESS_KEY_ID="$(read_secret AWS_ACCESS_KEY_ID)" + export AWS_SECRET_ACCESS_KEY="$(read_secret AWS_SECRET_ACCESS_KEY)" + export AWS_S3_UPLOAD_BUCKET_NAME="$(read_secret AWS_S3_UPLOAD_BUCKET_NAME)" + export AWS_S3_UPLOAD_BUCKET_URL="$(read_secret AWS_S3_UPLOAD_BUCKET_URL)" +fi diff --git a/services/outline/secretproviderclass.yaml b/services/outline/secretproviderclass.yaml new file mode 100644 index 0000000..2781c85 --- /dev/null +++ b/services/outline/secretproviderclass.yaml @@ -0,0 +1,63 @@ +# services/outline/secretproviderclass.yaml +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: outline-vault + namespace: outline +spec: + provider: vault + parameters: + vaultAddress: "http://vault.vault.svc.cluster.local:8200" + roleName: "outline" + objects: | + - objectName: "DATABASE_URL" + secretPath: "kv/data/atlas/outline/outline-db" + secretKey: "DATABASE_URL" + - objectName: "SECRET_KEY" + secretPath: "kv/data/atlas/outline/outline-secrets" + secretKey: "SECRET_KEY" + - objectName: "UTILS_SECRET" + secretPath: "kv/data/atlas/outline/outline-secrets" + secretKey: "UTILS_SECRET" + - objectName: "OIDC_AUTH_URI" + secretPath: "kv/data/atlas/outline/outline-oidc" + secretKey: "OIDC_AUTH_URI" + - objectName: "OIDC_CLIENT_ID" + secretPath: "kv/data/atlas/outline/outline-oidc" + secretKey: "OIDC_CLIENT_ID" + - objectName: "OIDC_CLIENT_SECRET" + secretPath: "kv/data/atlas/outline/outline-oidc" + secretKey: "OIDC_CLIENT_SECRET" + - objectName: "OIDC_LOGOUT_URI" + secretPath: "kv/data/atlas/outline/outline-oidc" + secretKey: "OIDC_LOGOUT_URI" + - objectName: "OIDC_TOKEN_URI" + secretPath: "kv/data/atlas/outline/outline-oidc" + secretKey: "OIDC_TOKEN_URI" + - objectName: "OIDC_USERINFO_URI" + secretPath: "kv/data/atlas/outline/outline-oidc" + secretKey: "OIDC_USERINFO_URI" + - objectName: "SMTP_FROM_EMAIL" + secretPath: "kv/data/atlas/outline/outline-smtp" + secretKey: "SMTP_FROM_EMAIL" + - objectName: "SMTP_HOST" + secretPath: "kv/data/atlas/outline/outline-smtp" + secretKey: "SMTP_HOST" + - objectName: "SMTP_PASSWORD" + secretPath: "kv/data/atlas/outline/outline-smtp" + secretKey: "SMTP_PASSWORD" + - objectName: "SMTP_USERNAME" + secretPath: "kv/data/atlas/outline/outline-smtp" + secretKey: "SMTP_USERNAME" + - objectName: "AWS_ACCESS_KEY_ID" + secretPath: "kv/data/atlas/outline/outline-s3" + secretKey: "AWS_ACCESS_KEY_ID" + - objectName: "AWS_SECRET_ACCESS_KEY" + secretPath: "kv/data/atlas/outline/outline-s3" + secretKey: "AWS_SECRET_ACCESS_KEY" + - objectName: "AWS_S3_UPLOAD_BUCKET_NAME" + secretPath: "kv/data/atlas/outline/outline-s3" + secretKey: "AWS_S3_UPLOAD_BUCKET_NAME" + - objectName: "AWS_S3_UPLOAD_BUCKET_URL" + secretPath: "kv/data/atlas/outline/outline-s3" + secretKey: "AWS_S3_UPLOAD_BUCKET_URL" diff --git a/services/outline/serviceaccount.yaml b/services/outline/serviceaccount.yaml new file mode 100644 index 0000000..8f15c78 --- /dev/null +++ b/services/outline/serviceaccount.yaml @@ -0,0 +1,6 @@ +# services/outline/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: outline-vault + namespace: outline diff --git a/services/planka/deployment.yaml b/services/planka/deployment.yaml index 9524245..d2aa431 100644 --- a/services/planka/deployment.yaml +++ b/services/planka/deployment.yaml @@ -21,6 +21,7 @@ spec: labels: app: planka spec: + serviceAccountName: planka-vault nodeSelector: node-role.kubernetes.io/worker: "true" affinity: @@ -58,6 +59,11 @@ spec: containers: - name: planka image: ghcr.io/plankanban/planka:2.0.0-rc.4 + command: + - /bin/sh + - -c + args: + - . /vault/scripts/planka_vault_env.sh && exec node app.js --prod ports: - name: http containerPort: 1337 @@ -66,23 +72,12 @@ spec: value: https://tasks.bstein.dev - name: TRUST_PROXY value: "true" - - name: OIDC_IGNORE_ROLES - value: "false" - name: OIDC_ADMIN_ROLES value: admin - name: OIDC_PROJECT_OWNER_ROLES value: planka-users - name: OIDC_ROLES_ATTRIBUTE value: groups - envFrom: - - secretRef: - name: planka-db - - secretRef: - name: planka-secrets - - secretRef: - name: planka-oidc - - secretRef: - name: planka-smtp volumeMounts: - name: user-data mountPath: /app/public/user-avatars @@ -95,6 +90,12 @@ spec: subPath: private/attachments - name: app-data mountPath: /app/.tmp + - name: vault-secrets + mountPath: /vault/secrets + readOnly: true + - name: vault-scripts + mountPath: /vault/scripts + readOnly: true readinessProbe: httpGet: path: / @@ -125,3 +126,13 @@ spec: - name: app-data persistentVolumeClaim: claimName: planka-app-data + - name: vault-secrets + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: planka-vault + - name: vault-scripts + configMap: + name: planka-vault-env + defaultMode: 0555 diff --git a/services/planka/kustomization.yaml b/services/planka/kustomization.yaml index ab42954..14a7cc9 100644 --- a/services/planka/kustomization.yaml +++ b/services/planka/kustomization.yaml @@ -4,8 +4,16 @@ kind: Kustomization namespace: planka resources: - namespace.yaml + - serviceaccount.yaml + - secretproviderclass.yaml - user-data-pvc.yaml - app-pvc.yaml - deployment.yaml - service.yaml - ingress.yaml +generatorOptions: + disableNameSuffixHash: true +configMapGenerator: + - name: planka-vault-env + files: + - planka_vault_env.sh=scripts/planka_vault_env.sh diff --git a/services/planka/scripts/planka_vault_env.sh b/services/planka/scripts/planka_vault_env.sh new file mode 100644 index 0000000..f5ab2ab --- /dev/null +++ b/services/planka/scripts/planka_vault_env.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +set -eu + +vault_dir="/vault/secrets" + +read_secret() { + cat "${vault_dir}/$1" +} + +export DATABASE_URL="$(read_secret DATABASE_URL)" +export SECRET_KEY="$(read_secret SECRET_KEY)" + +export OIDC_CLIENT_ID="$(read_secret OIDC_CLIENT_ID)" +export OIDC_CLIENT_SECRET="$(read_secret OIDC_CLIENT_SECRET)" +export OIDC_ENFORCED="$(read_secret OIDC_ENFORCED)" +export OIDC_IGNORE_ROLES="$(read_secret OIDC_IGNORE_ROLES)" +export OIDC_ISSUER="$(read_secret OIDC_ISSUER)" +export OIDC_SCOPES="$(read_secret OIDC_SCOPES)" +export OIDC_USE_OAUTH_CALLBACK="$(read_secret OIDC_USE_OAUTH_CALLBACK)" + +export SMTP_FROM="$(read_secret SMTP_FROM)" +export SMTP_HOST="$(read_secret SMTP_HOST)" +export SMTP_PASSWORD="$(read_secret SMTP_PASSWORD)" +export SMTP_PORT="$(read_secret SMTP_PORT)" +export SMTP_SECURE="$(read_secret SMTP_SECURE)" +export SMTP_TLS_REJECT_UNAUTHORIZED="$(read_secret SMTP_TLS_REJECT_UNAUTHORIZED)" +export SMTP_USER="$(read_secret SMTP_USER)" diff --git a/services/planka/secretproviderclass.yaml b/services/planka/secretproviderclass.yaml new file mode 100644 index 0000000..e72d98c --- /dev/null +++ b/services/planka/secretproviderclass.yaml @@ -0,0 +1,60 @@ +# services/planka/secretproviderclass.yaml +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: planka-vault + namespace: planka +spec: + provider: vault + parameters: + vaultAddress: "http://vault.vault.svc.cluster.local:8200" + roleName: "planka" + objects: | + - objectName: "DATABASE_URL" + secretPath: "kv/data/atlas/planka/planka-db" + secretKey: "DATABASE_URL" + - objectName: "SECRET_KEY" + secretPath: "kv/data/atlas/planka/planka-secrets" + secretKey: "SECRET_KEY" + - objectName: "OIDC_CLIENT_ID" + secretPath: "kv/data/atlas/planka/planka-oidc" + secretKey: "OIDC_CLIENT_ID" + - objectName: "OIDC_CLIENT_SECRET" + secretPath: "kv/data/atlas/planka/planka-oidc" + secretKey: "OIDC_CLIENT_SECRET" + - objectName: "OIDC_ENFORCED" + secretPath: "kv/data/atlas/planka/planka-oidc" + secretKey: "OIDC_ENFORCED" + - objectName: "OIDC_IGNORE_ROLES" + secretPath: "kv/data/atlas/planka/planka-oidc" + secretKey: "OIDC_IGNORE_ROLES" + - objectName: "OIDC_ISSUER" + secretPath: "kv/data/atlas/planka/planka-oidc" + secretKey: "OIDC_ISSUER" + - objectName: "OIDC_SCOPES" + secretPath: "kv/data/atlas/planka/planka-oidc" + secretKey: "OIDC_SCOPES" + - objectName: "OIDC_USE_OAUTH_CALLBACK" + secretPath: "kv/data/atlas/planka/planka-oidc" + secretKey: "OIDC_USE_OAUTH_CALLBACK" + - objectName: "SMTP_FROM" + secretPath: "kv/data/atlas/planka/planka-smtp" + secretKey: "SMTP_FROM" + - objectName: "SMTP_HOST" + secretPath: "kv/data/atlas/planka/planka-smtp" + secretKey: "SMTP_HOST" + - objectName: "SMTP_PASSWORD" + secretPath: "kv/data/atlas/planka/planka-smtp" + secretKey: "SMTP_PASSWORD" + - objectName: "SMTP_PORT" + secretPath: "kv/data/atlas/planka/planka-smtp" + secretKey: "SMTP_PORT" + - objectName: "SMTP_SECURE" + secretPath: "kv/data/atlas/planka/planka-smtp" + secretKey: "SMTP_SECURE" + - objectName: "SMTP_TLS_REJECT_UNAUTHORIZED" + secretPath: "kv/data/atlas/planka/planka-smtp" + secretKey: "SMTP_TLS_REJECT_UNAUTHORIZED" + - objectName: "SMTP_USER" + secretPath: "kv/data/atlas/planka/planka-smtp" + secretKey: "SMTP_USER" diff --git a/services/planka/serviceaccount.yaml b/services/planka/serviceaccount.yaml new file mode 100644 index 0000000..ca4f437 --- /dev/null +++ b/services/planka/serviceaccount.yaml @@ -0,0 +1,6 @@ +# services/planka/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: planka-vault + namespace: planka diff --git a/services/vault/k8s-auth-config-cronjob.yaml b/services/vault/k8s-auth-config-cronjob.yaml new file mode 100644 index 0000000..d974f6b --- /dev/null +++ b/services/vault/k8s-auth-config-cronjob.yaml @@ -0,0 +1,47 @@ +# services/vault/k8s-auth-config-cronjob.yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: vault-k8s-auth-config + namespace: vault +spec: + schedule: "*/15 * * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + template: + spec: + serviceAccountName: vault + restartPolicy: Never + nodeSelector: + kubernetes.io/arch: arm64 + node-role.kubernetes.io/worker: "true" + containers: + - name: configure-k8s-auth + image: hashicorp/vault:1.17.6 + imagePullPolicy: IfNotPresent + command: + - bash + - /scripts/vault_k8s_auth_configure.sh + env: + - name: VAULT_ADDR + value: http://vault.vault.svc.cluster.local:8200 + - name: VAULT_TOKEN + valueFrom: + secretKeyRef: + name: vault-oidc-admin-token + key: token + - name: VAULT_K8S_ROLE_TTL + value: 1h + volumeMounts: + - name: k8s-auth-config-script + mountPath: /scripts + readOnly: true + volumes: + - name: k8s-auth-config-script + configMap: + name: vault-k8s-auth-config-script + defaultMode: 0555 diff --git a/services/vault/kustomization.yaml b/services/vault/kustomization.yaml index 1ab70bc..9643894 100644 --- a/services/vault/kustomization.yaml +++ b/services/vault/kustomization.yaml @@ -8,6 +8,7 @@ resources: - rbac.yaml - configmap.yaml - statefulset.yaml + - k8s-auth-config-cronjob.yaml - oidc-config-cronjob.yaml - service.yaml - ingress.yaml @@ -19,3 +20,6 @@ configMapGenerator: - name: vault-oidc-config-script files: - vault_oidc_configure.sh=scripts/vault_oidc_configure.sh + - name: vault-k8s-auth-config-script + files: + - vault_k8s_auth_configure.sh=scripts/vault_k8s_auth_configure.sh diff --git a/services/vault/scripts/vault_k8s_auth_configure.sh b/services/vault/scripts/vault_k8s_auth_configure.sh new file mode 100644 index 0000000..15973e6 --- /dev/null +++ b/services/vault/scripts/vault_k8s_auth_configure.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +log() { echo "[vault-k8s-auth] $*"; } + +status_json="$(vault status -format=json || true)" +if [[ -z "${status_json}" ]]; then + log "vault status failed; check VAULT_ADDR and VAULT_TOKEN" + exit 1 +fi + +if ! grep -q '"initialized":true' <<<"${status_json}"; then + log "vault not initialized; skipping" + exit 0 +fi + +if grep -q '"sealed":true' <<<"${status_json}"; then + log "vault sealed; skipping" + exit 0 +fi + +k8s_host="https://${KUBERNETES_SERVICE_HOST}:443" +k8s_ca="$(cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt)" +k8s_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" +role_ttl="${VAULT_K8S_ROLE_TTL:-1h}" + +if ! vault auth list -format=json | grep -q '"kubernetes/"'; then + log "enabling kubernetes auth" + vault auth enable kubernetes +fi + +log "configuring kubernetes auth" +vault write auth/kubernetes/config \ + token_reviewer_jwt="${k8s_token}" \ + kubernetes_host="${k8s_host}" \ + kubernetes_ca_cert="${k8s_ca}" + +declare -A roles +roles[outline]=outline-vault +roles[planka]=planka-vault + +for namespace in "${!roles[@]}"; do + policy_name="${namespace}" + service_account="${roles[$namespace]}" + + log "writing policy ${policy_name}" + vault policy write "${policy_name}" - <