feat: start vault consumption for outline and planka

This commit is contained in:
Brad Stein 2026-01-14 01:30:41 -03:00
parent 1d894ea80f
commit b82195f2d7
14 changed files with 367 additions and 21 deletions

View File

@ -16,5 +16,5 @@ spec:
namespace: flux-system namespace: flux-system
values: values:
syncSecret: syncSecret:
enabled: true enabled: false
enableSecretRotation: false enableSecretRotation: false

View File

@ -21,6 +21,7 @@ spec:
labels: labels:
app: outline app: outline
spec: spec:
serviceAccountName: outline-vault
nodeSelector: nodeSelector:
node-role.kubernetes.io/worker: "true" node-role.kubernetes.io/worker: "true"
affinity: affinity:
@ -34,6 +35,11 @@ spec:
containers: containers:
- name: outline - name: outline
image: outlinewiki/outline:1.2.0 image: outlinewiki/outline:1.2.0
command:
- /bin/sh
- -c
args:
- . /vault/scripts/outline_vault_env.sh && exec node build/server/index.js
ports: ports:
- name: http - name: http
containerPort: 3000 containerPort: 3000
@ -66,18 +72,15 @@ spec:
value: "false" value: "false"
- name: SMTP_PORT - name: SMTP_PORT
value: "25" value: "25"
envFrom:
- secretRef:
name: outline-db
- secretRef:
name: outline-secrets
- secretRef:
name: outline-oidc
- secretRef:
name: outline-smtp
volumeMounts: volumeMounts:
- name: user-data - name: user-data
mountPath: /var/lib/outline/data mountPath: /var/lib/outline/data
- name: vault-secrets
mountPath: /vault/secrets
readOnly: true
- name: vault-scripts
mountPath: /vault/scripts
readOnly: true
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /_health path: /_health
@ -105,3 +108,13 @@ spec:
- name: user-data - name: user-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: outline-user-data 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

View File

@ -4,9 +4,17 @@ kind: Kustomization
namespace: outline namespace: outline
resources: resources:
- namespace.yaml - namespace.yaml
- serviceaccount.yaml
- secretproviderclass.yaml
- user-pvc.yaml - user-pvc.yaml
- redis-deployment.yaml - redis-deployment.yaml
- redis-service.yaml - redis-service.yaml
- deployment.yaml - deployment.yaml
- service.yaml - service.yaml
- ingress.yaml - ingress.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: outline-vault-env
files:
- outline_vault_env.sh=scripts/outline_vault_env.sh

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1,6 @@
# services/outline/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: outline-vault
namespace: outline

View File

@ -21,6 +21,7 @@ spec:
labels: labels:
app: planka app: planka
spec: spec:
serviceAccountName: planka-vault
nodeSelector: nodeSelector:
node-role.kubernetes.io/worker: "true" node-role.kubernetes.io/worker: "true"
affinity: affinity:
@ -58,6 +59,11 @@ spec:
containers: containers:
- name: planka - name: planka
image: ghcr.io/plankanban/planka:2.0.0-rc.4 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: ports:
- name: http - name: http
containerPort: 1337 containerPort: 1337
@ -66,23 +72,12 @@ spec:
value: https://tasks.bstein.dev value: https://tasks.bstein.dev
- name: TRUST_PROXY - name: TRUST_PROXY
value: "true" value: "true"
- name: OIDC_IGNORE_ROLES
value: "false"
- name: OIDC_ADMIN_ROLES - name: OIDC_ADMIN_ROLES
value: admin value: admin
- name: OIDC_PROJECT_OWNER_ROLES - name: OIDC_PROJECT_OWNER_ROLES
value: planka-users value: planka-users
- name: OIDC_ROLES_ATTRIBUTE - name: OIDC_ROLES_ATTRIBUTE
value: groups value: groups
envFrom:
- secretRef:
name: planka-db
- secretRef:
name: planka-secrets
- secretRef:
name: planka-oidc
- secretRef:
name: planka-smtp
volumeMounts: volumeMounts:
- name: user-data - name: user-data
mountPath: /app/public/user-avatars mountPath: /app/public/user-avatars
@ -95,6 +90,12 @@ spec:
subPath: private/attachments subPath: private/attachments
- name: app-data - name: app-data
mountPath: /app/.tmp mountPath: /app/.tmp
- name: vault-secrets
mountPath: /vault/secrets
readOnly: true
- name: vault-scripts
mountPath: /vault/scripts
readOnly: true
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: /
@ -125,3 +126,13 @@ spec:
- name: app-data - name: app-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: planka-app-data 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

View File

@ -4,8 +4,16 @@ kind: Kustomization
namespace: planka namespace: planka
resources: resources:
- namespace.yaml - namespace.yaml
- serviceaccount.yaml
- secretproviderclass.yaml
- user-data-pvc.yaml - user-data-pvc.yaml
- app-pvc.yaml - app-pvc.yaml
- deployment.yaml - deployment.yaml
- service.yaml - service.yaml
- ingress.yaml - ingress.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: planka-vault-env
files:
- planka_vault_env.sh=scripts/planka_vault_env.sh

View File

@ -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)"

View File

@ -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"

View File

@ -0,0 +1,6 @@
# services/planka/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: planka-vault
namespace: planka

View File

@ -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

View File

@ -8,6 +8,7 @@ resources:
- rbac.yaml - rbac.yaml
- configmap.yaml - configmap.yaml
- statefulset.yaml - statefulset.yaml
- k8s-auth-config-cronjob.yaml
- oidc-config-cronjob.yaml - oidc-config-cronjob.yaml
- service.yaml - service.yaml
- ingress.yaml - ingress.yaml
@ -19,3 +20,6 @@ configMapGenerator:
- name: vault-oidc-config-script - name: vault-oidc-config-script
files: files:
- vault_oidc_configure.sh=scripts/vault_oidc_configure.sh - 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

View File

@ -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}" - <<EOF
path "kv/data/atlas/${namespace}/*" {
capabilities = ["read"]
}
path "kv/metadata/atlas/${namespace}/*" {
capabilities = ["list"]
}
EOF
log "writing role ${namespace}"
vault write "auth/kubernetes/role/${namespace}" \
bound_service_account_names="${service_account}" \
bound_service_account_namespaces="${namespace}" \
policies="${policy_name}" \
ttl="${role_ttl}"
done