diff --git a/apps/app-kaneo.yaml b/apps/app-kaneo.yaml new file mode 100644 index 0000000..407252f --- /dev/null +++ b/apps/app-kaneo.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kaneo + namespace: argocd +spec: + project: default + source: + repoURL: git@forgejo.forgejo.svc.cluster.local:basicstack/stack.basicstack.de.git + targetRevision: main + path: apps/kaneo + destination: + server: https://kubernetes.default.svc + namespace: kaneo + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/apps/kaneo/README.md b/apps/kaneo/README.md new file mode 100644 index 0000000..02927cb --- /dev/null +++ b/apps/kaneo/README.md @@ -0,0 +1,53 @@ +# Kaneo + +Self-hosted [Kaneo](https://github.com/usekaneo/kaneo) — open-source project +management — deployed on the BasicStack Kubernetes cluster. + +- **URL:** https://kaneo.basicstack.de +- **Namespace:** `kaneo` +- **Distribution:** hand-rolled manifests, GitOps-managed by Argo CD + (see `apps/app-kaneo.yaml`). +- **Auth:** OIDC via Pocket ID (`https://auth.basicstack.de`) with + `CUSTOM_OAUTH_*` env vars. Client id + secret live in + `kaneo-oidc-secret` (SealedSecret). Access is restricted to + Pocket ID groups `kaneo-admins` / `kaneo-members`. +- **Database:** dedicated PostgreSQL in-namespace on + `hcloud-volumes-encrypted` (20Gi). Both the Postgres and Kaneo + pods pin to the `fsn1` datacenter via + `csi.hetzner.cloud/location=fsn1` node affinity — the Hetzner + volume is DC-local. +- **TLS:** Traefik ingress, cert issued by `letsencrypt-prod` + (DNS-01 via the Hetzner cert-manager webhook). +- **Image:** `ghcr.io/usekaneo/kaneo:2.22.0` (single container + bundling API + web on port 5173, health at `/api/health`). + +## Files + +| File | Purpose | +| --- | --- | +| `kaneo-postgres-pvc.yaml` | 20 Gi encrypted Hetzner volume for Postgres data. | +| `kaneo-postgres-deployment.yaml` | Postgres 16-alpine, pinned to `fsn1`. | +| `kaneo-postgres-service.yaml` | ClusterIP service `kaneo-postgres:5432`. | +| `kaneo-db-credentials-sealed.yaml` | Postgres user password (SealedSecret). | +| `kaneo-app-secrets-sealed.yaml` | `AUTH_SECRET` (Better Auth JWT secret, ≥32 chars). | +| `kaneo-oidc-secret-sealed.yaml` | OIDC client id + secret from Pocket ID. | +| `kaneo-deployment.yaml` | Kaneo app deployment (pinned to `fsn1`). | +| `kaneo-service.yaml` | ClusterIP service on port 80 → container 5173. | +| `kaneo-ingress.yaml` | Traefik ingress + `letsencrypt-prod` cert. | + +## Post-deploy setup + +1. Point browser at https://kaneo.basicstack.de and sign in via the + "Sign in with Pocket ID" (custom OAuth) button. +2. The first admin (`andreas.leinen@basicstack.de`, member of + `kaneo-admins`) must be promoted inside Kaneo — the current + upstream release does not map OIDC groups to roles. + +## SMTP + +SMTP env vars are intentionally not wired. Once a +`kaneo@basicstack.de` mailbox exists in Stalwart, add +`SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD/SMTP_FROM` via +`kaneo-smtp-sealed.yaml` and reference them from the deployment. Set +`DISABLE_EMAIL_OTP_SIGN_IN=true` so users continue to sign in via SSO +rather than email codes. diff --git a/apps/kaneo/kaneo-app-secrets-sealed.yaml b/apps/kaneo/kaneo-app-secrets-sealed.yaml new file mode 100644 index 0000000..7c5cb17 --- /dev/null +++ b/apps/kaneo/kaneo-app-secrets-sealed.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: kaneo-app-secrets + namespace: kaneo +spec: + encryptedData: + auth-secret: AgArCARyFt7NinaeQOfDDEpTKKWVba8IlKh7NN/vflgdOkE5bjz/P/ZzGUVhUOTyqCs+3dU+hX6sqc7IYbYckXdprwKJSXy47HLEsZ/cuJ0FRIBShRyXm+I62Yu8WWLHNJLTnlWyfirBC6Pg65kXckoKiYPwcVLSD5I+YHzo3Dt3Hl6MeW/iVocM9xgjBD/5yRt1ag+S4JYv0t1y39Cz80XxbFvo55RzYWr3OlYNI325b9W/d5uylqlDemln8NymL5iCdfnLDZoc+1r+eHAf1HDP6Uch1dG+c/xsrlvoWpzyMeSEbEY1tGeYFZ1oKm4B1pyUH9dfdxWEaNThheL5Xw2QX/klpZdTq5Jl6Lfxme5aLyqNtP9J6ovd5b1oVYW1DWc+RAqBPhFlHnkfc+8ZeP6vrmnUULq0ssSVqpXOz4zLgDHU61UzSllNdpZWXokigranj3dq5CM7WkYLEj9kfdDIGS3gDUD+EmIkIj/N96URNpOINSwFVAcnE73k3ij9mZWECLl++lzh1fqHrdYQiTI6A7UpxPudc7BoujBQFqGhUDXGdzBWsrqr11tldD+kb/kcKQYxs6sFHyw0pDgCLVZukPv7TWRNfVRYUv68h/1ZKAwmj8KkuGlNtAQ+NyvUWyct3SA+pQYWabr33pIPekWPIXMIty6/zfKu92hLFglwkauewxqLtEBZ6FFukKMyyfowV+iwDIMyY3//MvxiRzxD7sc5sLwcfvPCBzd9XGaJO1ZLX6r43gfOu6HFHiq4FqdjYJ9fqG3zUKeo5bnav7E6 + template: + metadata: + creationTimestamp: null + name: kaneo-app-secrets + namespace: kaneo diff --git a/apps/kaneo/kaneo-db-credentials-sealed.yaml b/apps/kaneo/kaneo-db-credentials-sealed.yaml new file mode 100644 index 0000000..343ed5a --- /dev/null +++ b/apps/kaneo/kaneo-db-credentials-sealed.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: kaneo-db-credentials + namespace: kaneo +spec: + encryptedData: + database-url: AgDEiqPdexD58tc4Sv63mk1erIHAeweJzGpKgoOcro6ioW4eX9pWnfVnVfmtsqQK8WXkRuEP5pAnj1LZAhuo6hfwk/lOpoYItqZKl6KGK0EQZ3u/hcmUt0yeEkTJuDPGBbgk1RuT1G2u2uDDYYJYHmAQtIIFbAoeJS2iDKnKkl6UHZdEJhO10w6IHGiNCT6Z9NovLJau5cw9tTv7xnbGO8F1QqwLh/dL0kI2oYNTYg1RxpegXRFeKGz+QSJ3rbxlH2P/E01Fevma2/ZhOBjALS1bpW9R6pgQm4n/FDu/fu72T62yw2oNvj6nIeRtIsm9ze02jQqYwF7mgct6IJOdqY7RoYnTZJQ+/Xl/iiBIGpzXan9bU/JZw6sEeyxS+tyeaolz4oiTeNfRXp8QNdX3qbXM9OE5Q+bjqx1q+cZcnFcBIvGK8vGqlWwDjwNicucuNBZuv4JWEOp2N8axoFVRUsUn/Frv7McqKT1Q2mRXf0aiGYh9m7EIdnFAx3JeiBJv392jBWcwvJpQ+J0LIsh/bJEBtdC4c16Y+38nbqfjYEUJWVIoj9SvJEo3QZPitUz99cwdtDn+cOVVYp+lv5BiunM7OgvAgBbX7F4Ri/QlL8rt3/8lO0Augwp933lSTuGGnMCrXPz2QCagI6B0l8F5q5OTnljgR7JXslyGFaemtj9jZEnCiSsw9KAiDdOUyk36CJSfHmRedJJ6o2nalb3zdFDF7iMXxz6fpTQK0QQGAccO518zUHOpb3Jb6XbOBKDgTBOWPEGlroiavrOtoRDJ+B5nR/N4c3Indpn7d0TTDU/4rz4V1uNkRrHnA26ya9Q= + postgres-password: AgBo5f/NzqZ/9jsOzXmoivPVVeLA3xk9GWocQQlLv34Rc1K5fzhSoCYdy8o7k06R83VeNsJylxfAsOI7HNIo0xWUPqC8HnbW482ajtREh93wOFCaZzjKu8ClbFIkvTb19GRCPLSmzBMFsTCJzjSa5rWGqK2Ezj7LFeTTiehsqS/PbPUYi6w4X6ZnChS3VuMXJVJAvbCF8fFS3x/YuuJVjjd+TmVsPmGOa6J+r/6RG7l4DsR+SmV3HQ7M6bLqTsUPjzg5Foz11yc8ER8xQQTgbbfrBPwgJQoTX622yHf0Q313ns7vyiKGMqge9DSQvDmGmMDmAxqtYInycVBH1CwmfKVbw3CyFGt0D5LJXiBaTE89uLErsNCQCK6NKAODqffKlOWpZ1KVF+ZIQ6nqJETtj49uqdKGowauCs3tDl/Hi82+hxycEVMWUwgu81CJ6GT7+vSZfz4+9FRbLoXlbdfR/duG14f+AkPirikeDcy3bFy5XiBgPVQXL0EbAY8TaPkYu+TS1Roo2F6Gqj3D1+i4GapVodrBntl1gMPvMnd3dcGGl/VzREGZvRsZgdRprFi+nGPmotRhIO3eGcPautlZ5m0WMqKCpA4jW0sG1sgkVwRpn2Zdq4Uw2+IKNHTkJKclIXCaw2/b6WNCmiwbe1ssFoR/QgikzshshoB5XpIYFe7Jr5Sj9mmNH6L4PpCIZ/sCe8MF5eMD8IuyWSVmflID07sfBD6GL3XB9TFLzSyviHE/VJdGY7l+ZeR64UZZ1ti4bwI= + template: + metadata: + creationTimestamp: null + name: kaneo-db-credentials + namespace: kaneo diff --git a/apps/kaneo/kaneo-deployment.yaml b/apps/kaneo/kaneo-deployment.yaml new file mode 100644 index 0000000..b8932bb --- /dev/null +++ b/apps/kaneo/kaneo-deployment.yaml @@ -0,0 +1,135 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kaneo + namespace: kaneo + labels: + app: kaneo + component: app +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: kaneo + component: app + template: + metadata: + labels: + app: kaneo + component: app + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: csi.hetzner.cloud/location + operator: In + values: + - fsn1 + initContainers: + - name: wait-for-postgres + image: postgres:16-alpine + command: + - sh + - -c + - | + until pg_isready -h kaneo-postgres -U kaneo -d kaneo; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + containers: + - name: kaneo + image: ghcr.io/usekaneo/kaneo:2.22.0 + ports: + - name: http + containerPort: 5173 + env: + # Public URL of this Kaneo instance. + - name: KANEO_CLIENT_URL + value: https://kaneo.basicstack.de + - name: CORS_ORIGINS + value: https://kaneo.basicstack.de + # Better Auth JWT secret (>=32 chars). + - name: AUTH_SECRET + valueFrom: + secretKeyRef: + name: kaneo-app-secrets + key: auth-secret + # Database credentials. DATABASE_URL is preferred; POSTGRES_* are + # accepted as a fallback and useful for the built-in migrations. + - name: POSTGRES_DB + value: kaneo + - name: POSTGRES_USER + value: kaneo + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: kaneo-db-credentials + key: postgres-password + - name: POSTGRES_HOST + value: kaneo-postgres + - name: POSTGRES_PORT + value: "5432" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: kaneo-db-credentials + key: database-url + # Self-hosted: keep public registration off, log in via OIDC only. + - name: DISABLE_REGISTRATION + value: "true" + - name: DISABLE_PASSWORD_REGISTRATION + value: "true" + - name: DISABLE_EMAIL_OTP_SIGN_IN + value: "true" + - name: KANEO_CLOUD + value: "false" + # Custom OAuth / OIDC via Pocket ID. + - name: CUSTOM_OAUTH_CLIENT_ID + valueFrom: + secretKeyRef: + name: kaneo-oidc-secret + key: client-id + - name: CUSTOM_OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: kaneo-oidc-secret + key: client-secret + - name: CUSTOM_OAUTH_DISCOVERY_URL + value: https://auth.basicstack.de/.well-known/openid-configuration + - name: CUSTOM_OAUTH_AUTHORIZATION_URL + value: https://auth.basicstack.de/authorize + - name: CUSTOM_OAUTH_TOKEN_URL + value: https://auth.basicstack.de/api/oidc/token + - name: CUSTOM_OAUTH_USER_INFO_URL + value: https://auth.basicstack.de/api/oidc/userinfo + - name: CUSTOM_OAUTH_LOGOUT_URL + value: https://auth.basicstack.de/api/oidc/end-session + - name: CUSTOM_OAUTH_SCOPES + value: openid,profile,email,groups + - name: CUSTOM_AUTH_PKCE + value: "true" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + livenessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 15 + periodSeconds: 5 + timeoutSeconds: 3 diff --git a/apps/kaneo/kaneo-ingress.yaml b/apps/kaneo/kaneo-ingress.yaml new file mode 100644 index 0000000..de218c1 --- /dev/null +++ b/apps/kaneo/kaneo-ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: kaneo + namespace: kaneo + labels: + app: kaneo + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - kaneo.basicstack.de + secretName: kaneo-tls + rules: + - host: kaneo.basicstack.de + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kaneo + port: + number: 80 diff --git a/apps/kaneo/kaneo-oidc-secret-sealed.yaml b/apps/kaneo/kaneo-oidc-secret-sealed.yaml new file mode 100644 index 0000000..1854e4a --- /dev/null +++ b/apps/kaneo/kaneo-oidc-secret-sealed.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: kaneo-oidc-secret + namespace: kaneo +spec: + encryptedData: + client-id: AgBGmlu+mGYlHcWPCRHZ3IbevGB5kGntqPasFBnny6ZydTeCmN8NWpy0bmoorecJSbvdeP38UDgfEjtmusfkipnrlfsZd7BugZLiijq5+T+9al5JTl/UChMl6u/gReaVPgLi9iketDN6oB2XC/56mV+MdMb/bO4d1u3CRFUJ/Pq1znWeQpVsXO1yOLoOge4+PYDYjgd4O6mJsbAQcmGxQ7pwcByw//+50Mvuw0gJBqI5ZSGKd0jSNnaVcECtvjY6v3xN1NCeVF1FoWKl4s7o/Nrq5mDltW/y6g0h3N1Fzan/D47UVQjGLYu6cTmM7ReqToYk65cznttGkyXfqrlVL/oEiYENtaqp/BZvwPBMKEAjJsufwGvbNgsxa3eGs+p3L+HwzdgTQbVGu2wVp+TXW8zwZvkDyprUU91Fu6QFsspV/GRYgqVmp4RxrJ0PzZInp4ZhLt/zOyWzbEkILMPV1A5fzPjrPmwpmhln/K0VTdpRN62XQRgH0P7EP6Ao0sP2/ae39eTkGlhb5fwzhSqK3LSmBYmoXNcffA4nBLFohSndmwgoxzZXM1FJ1kXl4wuevU8Eh8BD3dVIZkuYacKiIUFoNn74NbPtJpl3/MWfXef9Eb7IBR8KyurbdwSmKSAZDqjmeGTa271+KUiGfST+FYQgSDfFfQQEmh+K49WqqbOiod4Mh5RTGWJx3WNwEONsTRKVjiGgLw4A+ENNDB39PoRChoRoRRogg+ZJZE27EjxbmrKIjrw= + client-secret: AgBJQi6fIlTkOIZa1zr9jjyNOFAyZUjHWy0ZBjfcNh+iY2V3qIpG8kNPBntmvtVtemYI+UxEvvlalq0iEue+AtVmUoJeeO/lYuEDv+BrJGUWeZITbItuyoG+xGZM4x41X6FPgfRcnLCfii3dmwOeDZh1asR2Ahy2szdXrd2QaDeZMCCXNR+IeuP9U+IqJncfm+KjCqPABVbsZuGfDrT7r4XRv4mHQGnILD4IyA9N0/XyfY/cZRJryWeF5xbrOAP/ZWJduqYGIm2ahJfRZWIzk1Ujknq35iJ4bOZiw5eWb+aYhD9cxJj2UMBwWdqnXnpH+p55lrBqWlCzI0yDOfk4UQCI2RzQBJc/RvVS0z16k7El4dl9lOFTcgwBg13C/JC8Y0wo4M1jEap2ty/2ifwVZBZdflT8Uy5sk8dKBOv9wCupA/N60ACwlsavCUlgtQetVZUHIlI1U1rKCzArpGZ6PuREZokjdHw01u/j9ANEt1AaMe3G5jLcGohyvpcgL+zqI2S0rCqbSdRqWzZvr2kM8uFoeZSJLG3y4wdwZMgvRGMfoAKD1YlUGuhWeeZTda77ScM6ii8iEZEjvIgJXrfSCUf1vJ4QIAdMnNi05d3pTTDVHNTRjWBrklVb6oTpe/rFF1BBP6zhwGylWtb71xszbQyX8lzD+7v1O4iXr7ZWo86L9mGKz6FMnj0bO4LLVoqeVMy5DHo2fCuplix03P3WQCyjGKranAWdLalEV59QdnEhaQ== + template: + metadata: + creationTimestamp: null + name: kaneo-oidc-secret + namespace: kaneo diff --git a/apps/kaneo/kaneo-postgres-deployment.yaml b/apps/kaneo/kaneo-postgres-deployment.yaml new file mode 100644 index 0000000..8031a00 --- /dev/null +++ b/apps/kaneo/kaneo-postgres-deployment.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kaneo-postgres + namespace: kaneo + labels: + app: kaneo + component: database +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: kaneo + component: database + template: + metadata: + labels: + app: kaneo + component: database + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: csi.hetzner.cloud/location + operator: In + values: + - fsn1 + containers: + - name: postgres + image: postgres:16-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_DB + value: kaneo + - name: POSTGRES_USER + value: kaneo + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: kaneo-db-credentials + key: postgres-password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + livenessProbe: + exec: + command: + - pg_isready + - -U + - kaneo + - -d + - kaneo + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - pg_isready + - -U + - kaneo + - -d + - kaneo + initialDelaySeconds: 5 + periodSeconds: 5 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + persistentVolumeClaim: + claimName: kaneo-postgres-data diff --git a/apps/kaneo/kaneo-postgres-pvc.yaml b/apps/kaneo/kaneo-postgres-pvc.yaml new file mode 100644 index 0000000..1ae75e4 --- /dev/null +++ b/apps/kaneo/kaneo-postgres-pvc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: kaneo-postgres-data + namespace: kaneo + labels: + app: kaneo + component: database +spec: + accessModes: + - ReadWriteOnce + storageClassName: hcloud-volumes-encrypted + resources: + requests: + storage: 20Gi diff --git a/apps/kaneo/kaneo-postgres-service.yaml b/apps/kaneo/kaneo-postgres-service.yaml new file mode 100644 index 0000000..f92daf1 --- /dev/null +++ b/apps/kaneo/kaneo-postgres-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: kaneo-postgres + namespace: kaneo + labels: + app: kaneo + component: database +spec: + type: ClusterIP + selector: + app: kaneo + component: database + ports: + - name: postgres + port: 5432 + targetPort: 5432 diff --git a/apps/kaneo/kaneo-service.yaml b/apps/kaneo/kaneo-service.yaml new file mode 100644 index 0000000..c56b8be --- /dev/null +++ b/apps/kaneo/kaneo-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: kaneo + namespace: kaneo + labels: + app: kaneo + component: app +spec: + type: ClusterIP + selector: + app: kaneo + component: app + ports: + - name: http + port: 80 + targetPort: 5173