chore: retire the self-hosted zitadel (#1702)

This commit is contained in:
Zack Pollard
2026-07-28 11:44:55 +00:00
committed by GitHub
parent 5a25672ec9
commit 780e564b1c
31 changed files with 0 additions and 940 deletions
@@ -3,4 +3,3 @@ kind: Kustomization
resources:
- ./namespace.yaml
- ./oauth2-proxy/ks.yaml
- ./zitadel/ks.yaml
@@ -1,97 +0,0 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: zitadel
namespace: authentication
spec:
interval: 5m
chart:
spec:
chart: zitadel
version: 10.0.4
sourceRef:
kind: HelmRepository
name: zitadel
namespace: flux-system
interval: 5m
values:
image:
repository: ghcr.io/zitadel/zitadel
tag: v4.16.1@sha256:fea7ea2e4648d2dfe6a22a446252f084e4a370d8caedf08d3af4632cf517d4ef
setupJob:
machinekeyWriter:
image:
tag: 1.32.3
zitadel:
configmapConfig:
FirstInstance:
Org:
Machine:
Machine:
Username: zitadel-iac-admin-sa
Name: Admin
MachineKey:
Type: 1
ExternalDomain: zitadel.internal.immich.cloud
TLS:
Enabled: false # TLS is terminated at the ingress
masterkeySecretName: zitadel-master-key
login:
enabled: false
env:
- name: ZITADEL_DATABASE_POSTGRES_HOST
valueFrom:
secretKeyRef:
name: zitadel-db-app
key: host
- name: ZITADEL_DATABASE_POSTGRES_DATABASE
valueFrom:
secretKeyRef:
name: zitadel-db-app
key: dbname
- name: ZITADEL_DATABASE_POSTGRES_USER_USERNAME
valueFrom:
secretKeyRef:
name: zitadel-db-app
key: user
- name: ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db-app
key: password
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: zitadel-db-superuser
key: user
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db-superuser
key: password
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
nginx.ingress.kubernetes.io/configuration-snippet: |
grpc_set_header Host $host;
hosts:
- host: &host zitadel.internal.immich.cloud
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- *host
secretName: zitadel-tls
resources:
limits:
cpu: 1
memory: 2Gi
requests:
cpu: 500m
memory: 512Mi
@@ -1,5 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./helmrelease.yaml
@@ -1,10 +0,0 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: zitadel-db
namespace: authentication
spec:
enableSuperuserAccess: true
storage:
size: 10Gi
storageClass: zfs
@@ -1,5 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./database.yaml
@@ -1,89 +0,0 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app zitadel-secrets
namespace: flux-system
spec:
targetNamespace: authentication
commonMetadata:
labels:
app.kubernetes.io/name: *app
dependsOn:
- name: external-secrets-stores
path: ./kubernetes/apps/authentication/zitadel/secrets
prune: true
sourceRef:
kind: GitRepository
name: immich-kubernetes
wait: true
interval: 30m
retryInterval: 1m
timeout: 5m
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app zitadel-database
namespace: flux-system
spec:
targetNamespace: authentication
commonMetadata:
labels:
app.kubernetes.io/name: *app
dependsOn:
- name: database-cloudnative-pg
path: ./kubernetes/apps/authentication/zitadel/database
prune: true
sourceRef:
kind: GitRepository
name: immich-kubernetes
wait: true
interval: 30m
retryInterval: 1m
timeout: 5m
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app zitadel
namespace: flux-system
spec:
targetNamespace: authentication
commonMetadata:
labels:
app.kubernetes.io/name: *app
dependsOn:
- name: zitadel-secrets
- name: zitadel-database
path: ./kubernetes/apps/authentication/zitadel/app
prune: true
sourceRef:
kind: GitRepository
name: immich-kubernetes
wait: true
interval: 30m
retryInterval: 1m
timeout: 5m
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app zitadel-pushsecret
namespace: flux-system
spec:
targetNamespace: authentication
commonMetadata:
labels:
app.kubernetes.io/name: *app
dependsOn:
- name: zitadel
path: ./kubernetes/apps/authentication/zitadel/pushsecret
prune: true
sourceRef:
kind: GitRepository
name: immich-kubernetes
wait: true
interval: 30m
retryInterval: 1m
timeout: 5m
@@ -1,17 +0,0 @@
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: zitadel-iac-admin-sa
namespace: authentication
spec:
secretStoreRefs:
- kind: ClusterSecretStore
name: 1p-kubernetes
selector:
secret:
name: zitadel-iac-admin-sa
data:
- match:
secretKey: zitadel-iac-admin-sa.json
remoteRef:
remoteKey: PUSHED_ZITADEL_IAC_ADMIN_SA
@@ -1,5 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./admin-sa.yaml
@@ -1,5 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./masterkey.yaml
@@ -1,14 +0,0 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: zitadel-master-key
namespace: authentication
spec:
secretStoreRef:
kind: ClusterSecretStore
name: 1p-tf
refreshInterval: "20s"
data:
- secretKey: masterkey
remoteRef:
key: AUTH_ZITADEL_MASTER_KEY
@@ -18,7 +18,6 @@ spec:
timeout: 5m
dependsOn:
- name: external-secrets-stores
- name: zitadel
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
@@ -18,7 +18,6 @@ spec:
timeout: 5m
dependsOn:
- name: external-secrets-stores
- name: zitadel
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
@@ -18,7 +18,6 @@ spec:
timeout: 5m
dependsOn:
- name: external-secrets-stores
- name: zitadel
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
@@ -9,7 +9,6 @@ spec:
app.kubernetes.io/name: *app
dependsOn:
- name: external-secrets-stores
- name: zitadel
path: ./kubernetes/apps/tools/containerssh/secrets
prune: true
sourceRef:
-1
View File
@@ -9,7 +9,6 @@ spec:
app.kubernetes.io/name: *app
dependsOn:
- name: external-secrets-stores
- name: zitadel
path: ./kubernetes/apps/tools/outline/secrets
prune: true
sourceRef:
@@ -17,4 +17,3 @@ resources:
- ./immich-charts.yaml
- ./controlplaneio.yaml
- ./external-secrets.yaml
- ./zitadel.yaml
@@ -1,8 +0,0 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: zitadel
namespace: flux-system
spec:
interval: 1h
url: https://charts.zitadel.com/
@@ -1,132 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/1password/onepassword" {
version = "2.2.1"
constraints = "~> 2.1"
hashes = [
"h1:CosIqZ6jkgylvF23rLyNixIecZAAhFyb61sLl6GrEsc=",
"zh:025709a6b5f1b3685d277f2c48f7cb8b53d14b3699c1123d7e9a2135c099c533",
"zh:037fc89d150063a8aacdcab08ba26038b489fe2468d509b842d298ea59096ca6",
"zh:233777182b25faf1658e8ce171b684460983bb41cff79fb243662f3f9dc5ca6c",
"zh:2fb5ca2fc8c37b1d1c54da646ed13bf40897941fe92eece784fba496f677b533",
"zh:4b25b5ce1f694ec265e65234fc85d6bdf3810297ffeaced54ad46a1ba28142de",
"zh:5509d1e4fb7b45c63124ec66fd1f9d6757daa8bf1f7bdd724d5adb2965b61436",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:a23ba946c629ec912b2fcbf606a2eb8853626ec0e0bee749f2d39146a872c082",
"zh:a3d3024485426237d7b4a4350b12dda4d29d88f3942246a9370be35ec2a51e9e",
"zh:a6ef65544ab8fc26d468b38636407a3d2d902e35c51b648729bf97c31d1937f9",
"zh:afbe9480a0da0ad8dc514b277f1e4be36b8931f045021d05c21665ef1ac0b7c8",
"zh:b2e96e69fa9ff7e179dccdef5b785cd020eb46bb2b3d1d507d009d71be6b0c26",
"zh:ceefaede9e8a3104463523ba267e3e985b27a706f7628a9ddd37330c2ca59d4d",
"zh:ea77786bd6809ff4f8043b84a0212fec4de18b7d51bc420417ba10999ca99887",
"zh:f7d8160c3669c8ab76a2da14ea740d91a08ca23d1fb657669e52a840b2b113d9",
]
}
provider "registry.opentofu.org/hashicorp/http" {
version = "3.6.0"
constraints = "~> 3.5"
hashes = [
"h1:0n4RBz9zNw6TTddh5+x7E8L2+qzPXNwKhK4uoZ/DUwE=",
"h1:22Ob7lpzMBSqdrCvoFN5EgmhGPHPBovV/9qo0c/Cd+A=",
"h1:2IRBvmWOYrq/ooaYYn2i86jZb7iIUvlg0KlmOMfDHoQ=",
"h1:5mucXikk4OcW3un3u94QnMx4AB4Wfih+sXeMd5QxSNk=",
"h1:5oU7Zm+2gAVGmxqtJ9E8uTudUkYy/DEn/y3IWphdv4k=",
"h1:5w0R4b1/VSzpqQF1tXXPr/qmaQLPVRXamOmPKWFcTk4=",
"h1:AEVeJr8xGmwad+JUUQ833C3x5d4W+W2szF5DfwxYppw=",
"h1:CPHJ+0zQbS/cX1m55Y90jIOgf1jV3ocUUnqsXAh+9Eg=",
"h1:JPewnGDOJudNer5+ghqwXoaJkfot3QRq9uiEYvo+JHU=",
"h1:QzbluV2vQLxsJYxjpziQCmPndIoJ/UGS4/UHH/GpwUM=",
"h1:TjUNbUdqweRBq/ycQ4ixpNkx5qaYwpXEOn9QCpqNZP8=",
"h1:XNbcODP60ajj21N/OO7af8bBg1ltIsYkq9egn7BYbiY=",
"h1:tgrbgmX7WYQz9G9ncgu7TkpVB+RlLjJA/Rvp9KPlZH8=",
"h1:vLxthX/ZWsOZ+aHKbAMqmNKqD0K5f4nJ8ppy0Ioyup0=",
"h1:wZOdGBAZkY8OKEPjKz82j1HloAKOmmvtjWyTxM+I110=",
"zh:0f719fa5426bc883e9fa6abf7f6498e48025edafbc29015e2f5c028f1cca3b9d",
"zh:1b4d7dafefd6c61764b2f9ed6943ceb9a200dee3590d18747e3a5f6b20ce85e0",
"zh:1d23a712984866d29f7b07028a4e99c783c71f1a5dddf08bc3d4e7da9d91a1fa",
"zh:257d23d58c3bb024b6bc8eb88736eaf912e934ad47c639d0c3c742bddda849a1",
"zh:479860e1a5468f5e04013b9364c9496d7ed0804bf9a1acd8e07558d57609993d",
"zh:4cb5e681bf599b411b27c4a2c4066a5fb2ed79aaa3a1a3cb5a30002fec062ce9",
"zh:4fb35c3f643dae9f3670d719397a415f815a0b95f8ed7bd8a72f27a94ba78092",
"zh:59ba40825ab38db5b4a0989a2db0df35cc15d8984f898176011ba352f27d77b7",
"zh:61fc1252eb88088638f4c69ea4e2171cde2e5089fa632ac1e943b13787348f73",
"zh:7c5d6dd5f7cbc460e95d368be35c29b4e0402069b8912dbd5d1cd7fa9acef216",
"zh:7f76d756240d4284642f359ad470226e5378670239aadc366ef54d9d914d4d2e",
"zh:8133ad0814098177e0d067c816ccf1bf48bbadacd18f6f2c808c90447505723b",
"zh:c93be06269bb728f1968f8c50506de56c887017ac1d6e4be1f925651d8437eb6",
"zh:ef47b78a10a82e6cf53344a6a85a94041c28286c10a70541c564d762f1cfede0",
"zh:f5796a53a74999135bd9087aff50fddda59129d09b2f9b1902ff8c0c1e047e48",
]
}
provider "registry.opentofu.org/hashicorp/random" {
version = "3.9.0"
hashes = [
"h1:8EQU5KSxezcjo/phRSe69rDOI0lk4pSaggj7FsskYp8=",
"h1:Lw9im2VBBJQ3RyAbHPQ0rcvcmmcZWm3x+kIOpN+Tv9s=",
"h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=",
"h1:YXaVd4p6qXPPVaxIBaIDNXmBwT02ZqDn0qD+tYpw8sA=",
"h1:cOpc03fphEt/G9Rfc4jLL/fW0D7tgvlXqiDKPF4vuww=",
"h1:g09RR7T1xWkeGrZwWvWMT9ncJrFGr1k3CBD585UmO7w=",
"h1:gGDdPPibmw2EWROx+sh1RGLjR5+nPwZyrf6/N9jXfeM=",
"h1:haE7/nXCOhXKP4oXeEnER3t5CaVQWqujz4nBnpeTUv4=",
"h1:ieSVpfZS2lKuMr05ph0QsOVpCzg7uk3cgKBaXR+Ikug=",
"h1:ig2s1IS9IzehorRjvVAnKIsUUj8fkgyxct1L/kswcc4=",
"h1:j3lS+ZEERFnoab8t1ppDrScGVP/cgWbzlCrEYKTCXYw=",
"h1:lxezrKmOiQIySHAM+os8qLVq7hqufDr8h3Hpzvsk+78=",
"h1:lzRqBJAG+NETxHbEZUJ/YP3RMEjZBinTX7VmgH3lw60=",
"h1:tdSNWK5ApqUsgbdYieyeYLTu6nIZUV3hR1oFqUfAuGo=",
"h1:xedet8yH/zI2CfdxsGlK0nlFWc/Bp61yrWsEa3fHB8g=",
"zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc",
"zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a",
"zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2",
"zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1",
"zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9",
"zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d",
"zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae",
"zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a",
"zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261",
"zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c",
"zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627",
"zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e",
"zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1",
"zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5",
"zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64",
]
}
provider "registry.opentofu.org/zitadel/zitadel" {
version = "2.12.8"
constraints = "2.12.8"
hashes = [
"h1:/KtT6/u4Tl8FlfNu3fDAAUEpwpcsDwZPHAH44OdQ4zM=",
"h1:3kMM+shoaxGTue0q2sljwYuDMFYAYtPcx2lVaRFJQXQ=",
"h1:6w/7uc20DG3a4C/6F1+jFZk0W9hL0OZJSvp60jSWfVg=",
"h1:AMFM5wojfBz/4QlUnpC5kF/pchgYv6xPmvJEAj/CTNI=",
"h1:BeeCkpgx6K1Y3e1Cs1rUt/8+YcaHEys3hJGb+TvZm1Y=",
"h1:Hzl6ZyCdQguLwcfLBWudvEQi2aV8NiwRG5GL7Cvv+0E=",
"h1:K+DeCgdNTRGkaSdjpf+R/MOF8G5TmvOZpABaW8dQBAU=",
"h1:dSCQd9UD38Cnl+zR72qmXLaniU26rH+fXlP6bqqXuEM=",
"h1:jG/QsZbrBD7VVP9XqHvAk7tMrFBSuDVT2KndQ1SgeKU=",
"h1:osYErmDg7vLTjAMry+zN7SH+l7HmNvAk8mxeFEnDUmA=",
"h1:p1jvk7j9OdYUPrTYWkXC8S+rdDet5P7n6kX5TE7qOK8=",
"h1:r5Z0EfR9wIhMUcKtd0grisnD7qrrr7LIyF//LxQDf10=",
"h1:vCT9CQ4FgFh0EDm5mBlVdeFV5SZemiiJ5x+itrdf5/g=",
"zh:45b56f3c187686b95e92c90c65a1fc9064fc0ee8651bea31721cc8821d115680",
"zh:4795d8aa86bfa8f3b44223fd7754b3c9eb7af4ed1087dccba2e23ecdd3fd59e8",
"zh:4c2dfae7162e4bc3c60249d31a73419ea18e400e6bc5871dda5d54a4759404b6",
"zh:6b1fc7badb46ca692d6f836288d5d0c3f4776982c1c9c16928a7dd0f6bf3849b",
"zh:8355f5df417582056f30d323089a35e6558eb3db5cd4e33bafd643b27fd02973",
"zh:99a0b87970478a22477c4f0d683c68c32bf5b75f57abe3bfab03ff1c30b3b7e8",
"zh:a33d53acc640dc93b81352ba633cf392bc8c7614a72d320d59d3dcdb22d73fc4",
"zh:a9a620278003fdc293ffd30f0e152cca9090fcabbd801b22cf2e8f9daaa17bef",
"zh:b321f81355ed4feda1139b7692fd8858d70b4438de68d7912b51de91eea5313c",
"zh:b8691127f6e213ed0a9457817e6112ce8966665d96778f5f24272541832988be",
"zh:da88e461c9398abdbe630984b10f9e3fc65d90ea8c032de17d98d31ae34fa736",
"zh:e152e480515dcb34570b0a4e5722dd87f72bb5a897c4e45bfaae6efc437cf726",
"zh:e6a53c084025423336a0cd41c9b4947856fc6e4d7c0afa002e0917056395c9bc",
"zh:e970ce1499fc4e4a99b76cf1f0839c88dd5ccd15a71fa670c8e98607de40096b",
]
}
@@ -1,140 +0,0 @@
resource "zitadel_action" "map_github_oauth" {
org_id = zitadel_org.immich.id
name = "mapGitHubOAuth"
script = <<-EOT
let logger = require("zitadel/log");
let { fetch } = require("zitadel/http");
async function mapGitHubOAuth(ctx, api) {
if (ctx.v1.externalUser.externalIdpId !== "${zitadel_idp_github.github.id}") {
return;
}
try {
const response = fetch('https://api.github.com/user/emails', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + ctx.accessToken,
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.status == 200) {
logger.error("Failed to fetch GitHub emails, status: " + response.status + ", body: " + response.text());
return;
}
const emails = response.json();
let primaryEmail = null;
const primaryEmailObject = emails.find(email => email.primary);
if (primaryEmailObject) {
primaryEmail = primaryEmailObject.email;
}
if (primaryEmail) {
api.setEmail(primaryEmail);
api.setEmailVerified(true);
} else {
logger.warn("No primary email found in GitHub response for user: " + ctx.v1.externalUser.human.displayName);
}
} catch (error) {
logger.error("Error fetching or processing GitHub emails: " + error.toString());
}
let firstName = ctx.v1.providerInfo.login;
let lastName = " ";
const nameParts = ctx.v1.providerInfo.name.trim().split(" ");
if (nameParts.length > 0 && nameParts[0].length > 0) {
firstName = nameParts[0];
}
if (nameParts.length > 1) {
lastName = nameParts.slice(1).join(" ");
}
api.setFirstName(firstName);
api.setLastName(lastName);
}
EOT
allowed_to_fail = true
timeout = "10s"
}
resource "zitadel_trigger_actions" "map_github_oauth" {
org_id = zitadel_org.immich.id
action_ids = [zitadel_action.map_github_oauth.id]
trigger_type = "TRIGGER_TYPE_POST_AUTHENTICATION"
flow_type = "FLOW_TYPE_EXTERNAL_AUTHENTICATION"
}
resource "zitadel_action" "map_roles" {
org_id = zitadel_org.immich.id
name = "mapRoles"
script = <<-EOT
const logger = require('zitadel/log');
function mapRoles(ctx, api) {
logger.info('help');
if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) {
return;
}
// I want to use named capture groups but apparently they aren't supported
const projectId = Object.keys(ctx.v1.claims).map((claim) => claim.match(/urn:zitadel:iam:org:project:(?<projectId>\d+):roles/)?.[1]).filter(Boolean).at(0);
if (!projectId) {
return;
}
const grants = ctx.v1.user.grants.grants.filter((grant) => grant.projectId === projectId);
const roles = grants.map(({ roles }) => roles);
if (roles.length === 1) {
api.v1.claims.setClaim('role', String(roles[0]));
return;
}
if (roles.length > 1) {
api.v1.claims.setClaim('roles', roles)
}
}
EOT
allowed_to_fail = false
timeout = "10s"
}
resource "zitadel_trigger_actions" "map_roles" {
org_id = zitadel_org.immich.id
action_ids = [zitadel_action.map_roles.id]
trigger_type = "TRIGGER_TYPE_PRE_USERINFO_CREATION"
flow_type = "FLOW_TYPE_CUSTOMISE_TOKEN"
}
resource "zitadel_action" "saml_map_roles" {
org_id = zitadel_org.immich.id
name = "samlMapRoles"
script = <<-EOT
function samlMapRoles(ctx, api) {
if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) {
return;
}
let roles = [];
ctx.v1.user.grants.grants.forEach(grant => {
roles.push(grant.roles)
})
api.v1.attributes.setCustomAttribute('Roles', '', ...roles)
}
EOT
allowed_to_fail = false
timeout = "10s"
}
resource "zitadel_trigger_actions" "saml_map_roles" {
org_id = zitadel_org.immich.id
action_ids = [zitadel_action.saml_map_roles.id]
trigger_type = "TRIGGER_TYPE_PRE_SAML_RESPONSE_CREATION"
flow_type = "FLOW_TYPE_SAML_RESPONSE"
}
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://www.ovhcloud.com/eu/auth/">
<SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIFVzCCAz+gAwIBAgIUQSUwSIh9zUNgNtP/FhkGdy7bEPYwDQYJKoZIhvcNAQELBQAwOzEdMBsGA1UEAwwUYXV0aC5ldS5vdmhjbG91ZC5jb20xCzAJBgNVBAsMAlNQMQ0wCwYDVQQKDARTQU1MMB4XDTI0MDExMjE0MTYzM1oXDTM0MDEwOTE0MTYzM1owOzEdMBsGA1UEAwwUYXV0aC5ldS5vdmhjbG91ZC5jb20xCzAJBgNVBAsMAlNQMQ0wCwYDVQQKDARTQU1MMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0RWhT7fEM8NLiLeEe9Burk15IJUVHvHXJFySOr4lGhbpUWXgnRQZKP8klr3Jd9jZwv9ss+NhsbT/64lqB6WINozk11fGefdyrO6TbIioQnTOcwtF7NCTy9ypuBZiseqMxA67g6VDRqtRuZCtHVFOBJxMzux0h6sGS5t/a3aaHR+0xw9dhhHE0+sNiZUehl0BuNafoelar1nK6Q1ZfXWmoC5zJjxxJAE2DH+kNZaWOdkbPCWpD5G1maZlNL+tEFHzAAmCjj5re4C9enMs4/XPnxqS4wLzsuAEU2/7917+wdgPTpF7RHsqYgFD+gff00IkpY1+H8oqsEtf/iebIvoy0EYD0KozPnSolKNHhOEqSFcxehtCDOPdVq92Ie5FzMPLEeZoO1UvPIlKm2043BjGbNtYNBcFOTP6jdMInGau7KLXz/Bh5g/8mBI+UyCTbZaSoB1GkfzJJpP5j+VPYXcum/s/Y0AJUwNBXujQnu7EkNwQeE8QkjfrHXSC0wI6QYzpcc4DyMk1TNWg+4dn2EdU0TJS+zO/ohaAO1L1O9Pw6+JfHrXcNy5E7iPeRyMhkRMEhWyHxh6PlO7c2Hydo1WVAuV1BqQJmjMYP3D4dzcdmaZiy6JElV4fvkGBh/J3+jRGqeQtb0vplz0d3Dj5Zeto07xi79XtaItk4e9a6KPVds8CAwEAAaNTMFEwHQYDVR0OBBYEFJ/VldqCVgz9XObIdmTtzX+7qo9JMB8GA1UdIwQYMBaAFJ/VldqCVgz9XObIdmTtzX+7qo9JMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBACVM09u+blp3PDDjJsSJM0WyAq40bamTxncfyqZG1P+pN+K/Y38XU+F874FpiglFn1U1ZNekPA8HHj9SpRK1iZKqohB2r6ivwMqcwOJJM3LPNuPxs/RnNXAuup7KfnkEocnIYfad4KUr5QZoWfAyzMGiiRUF9mY4mLvRii1627XsTeQiDN2NJKvl/8FzXSWy2X61nA6kjw7erx5n6QoOprrXbG4Lmq2dRGdO3fqKtSByXrr5pLvHQgduoCtwbJn2mJHEnNSM8h7EDcszMRsgagQuUxT/XdGA8jqzs/qL38XHPxSmdaVzLO1pIxGXH5mZ+emDPGFN/Yd3WA/63qQ+Ig+ctu5gKJGDHaVLXmHE/cQXWs9tYoEen2iSL4nTf9hdH9hz7osQ1q9tNyJkHPwxYYvV9po6ui6jivd/OrasO54SzkoIEIlz0GpaZ4+sVfxWxoaHguQjmbesrppMXwdj8rc8fzKq70nMUm/T3hlehFXsBeH1lZygfKpb1EtpSbmStrQ/pY6xPB0oa4syj9kAnLfVWWsogg4RnIJ3QBZmk5qqwkf1Nq07fnXxIHRxmt5c1PS4hqvSxvIcH/Ghq0VVDZb3yMELZYJWIN20Q52A9I78yEwbjWmtGJ9oG1hRceGlWsxVhqX6Br1fHaHvkgz8HIoTM6kwPAHT+IVxnBIkKOuC</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.ovhcloud.com/eu/auth/saml/acs" index="1"></AssertionConsumerService>
</SPSSODescriptor>
</EntityDescriptor>
@@ -1,17 +0,0 @@
terraform {
backend "pg" {
schema_name = "prod_zitadel"
}
required_version = "~> 1.7"
required_providers {
zitadel = {
source = "zitadel/zitadel"
version = "2.12.8"
}
onepassword = {
source = "1Password/onepassword"
version = "~> 2.1"
}
}
}
@@ -1,12 +0,0 @@
data "zitadel_orgs" "zitadel" {
name = "Zitadel"
}
data "zitadel_projects" "zitadel" {
name = "Zitadel"
org_id = data.zitadel_orgs.zitadel.ids[0]
}
data "onepassword_vault" "tf" {
name = "tf"
}
@@ -1,71 +0,0 @@
resource "zitadel_default_login_policy" "default" {
user_login = false
allow_register = false
allow_external_idp = true
force_mfa = false
force_mfa_local_only = false
passwordless_type = "PASSWORDLESS_TYPE_NOT_ALLOWED"
hide_password_reset = "true"
password_check_lifetime = "240h0m0s"
external_login_check_lifetime = "720h0m0s"
multi_factor_check_lifetime = "24h0m0s"
mfa_init_skip_lifetime = "720h0m0s"
second_factor_check_lifetime = "24h0m0s"
ignore_unknown_usernames = true
default_redirect_uri = ""
second_factors = []
multi_factors = []
idps = [zitadel_idp_github.github.id]
allow_domain_discovery = false
disable_login_with_email = true
disable_login_with_phone = true
}
resource "zitadel_default_oidc_settings" "default" {
access_token_lifetime = "12h0m0s"
id_token_lifetime = "12h0m0s"
refresh_token_expiration = "720h0m0s"
refresh_token_idle_expiration = "2160h0m0s"
}
// Find the default zitadel project and disallow users from logging in
resource "zitadel_project" "zitadel" {
name = "Zitadel"
org_id = data.zitadel_orgs.zitadel.ids[0]
project_role_assertion = false
project_role_check = true
has_project_check = false
private_labeling_setting = "PRIVATE_LABELING_SETTING_UNSPECIFIED"
}
resource "zitadel_instance_member" "superusers" {
for_each = {
for user in local.users_data : user.github.id => user
if try(user.github.username, "") != "" && contains(user.roles, "immich_admin")
}
user_id = zitadel_human_user.users[each.key].id
roles = ["IAM_OWNER"]
}
resource "zitadel_project_role" "zitadel_admin" {
org_id = zitadel_project.zitadel.org_id
project_id = zitadel_project.zitadel.id
role_key = "admin"
display_name = "Admin"
}
resource "zitadel_user_grant" "superusers" {
for_each = {
for user in local.users_data : user.github.id => user
if try(user.github.username, "") != "" && contains(user.roles, "immich_admin")
}
org_id = zitadel_project.zitadel.org_id
project_id = zitadel_project.zitadel.id
user_id = zitadel_human_user.users[each.key].id
role_keys = [zitadel_project_role.zitadel_admin.role_key]
}
import {
id = data.zitadel_projects.zitadel.project_ids[0]
to = zitadel_project.zitadel
}
@@ -1,11 +0,0 @@
resource "zitadel_idp_github" "github" {
name = "GitHub"
client_id = var.zitadel_github_client_id
client_secret = var.zitadel_github_client_secret
scopes = ["read:user", "user:email"]
is_auto_creation = false
is_auto_update = true
is_creation_allowed = false
is_linking_allowed = true
auto_linking = "AUTO_LINKING_OPTION_USERNAME"
}
@@ -1,4 +0,0 @@
resource "zitadel_org" "immich" {
name = "Immich"
is_default = true
}
@@ -1,47 +0,0 @@
locals {
project_to_displaynames = flatten([
for project in local.projects : [
for role in project.roles : {
project_name = project.name
role_key = role.key
}
]
])
# For each user+project, grant only the highest-priority role (first match in the ordered list)
project_user_grants = flatten([
for project in local.projects : [
for user in local.users_data : {
project_name = project.name
role_key = [for role in project.roles : role.key if length(setintersection(toset(role.grants_to), toset(user.roles))) > 0][0]
github_user_id = user.github.id
}
if length([for role in project.roles : role.key if length(setintersection(toset(role.grants_to), toset(user.roles))) > 0]) > 0
&& try(user.github.username, "") != ""
]
])
}
resource "zitadel_project_role" "project_roles" {
for_each = {
for project in local.project_to_displaynames : "${project.project_name}_${project.role_key}" => project
}
org_id = zitadel_org.immich.id
project_id = zitadel_project.projects[each.value.project_name].id
role_key = each.value.role_key
display_name = each.value.role_key
}
resource "zitadel_user_grant" "project_grants" {
for_each = {
for grant in local.project_user_grants : "${grant.project_name}_${grant.github_user_id}" => grant
}
depends_on = [zitadel_project_role.project_roles]
org_id = zitadel_org.immich.id
project_id = zitadel_project.projects[each.value.project_name].id
user_id = zitadel_human_user.users[each.value.github_user_id].id
role_keys = [each.value.role_key]
}
@@ -1,133 +0,0 @@
locals {
project_defaults = {
authMethod = "NONE"
appType = "WEB"
redirectUris = []
grantTypes = ["AUTHORIZATION_CODE"]
protocol = "oidc"
metadataFile = ""
}
projects_data = [
{
name = "Grafana Monitoring Prod"
roles = [{ key = "GrafanaAdmin", grants_to = ["immich_admin"] }, { key = "Editor", grants_to = ["team"] }]
redirectUris = ["https://monitoring.immich.cloud/login/generic_oauth"]
},
{
name = "Grafana Monitoring Dev"
roles = [{ key = "GrafanaAdmin", grants_to = ["immich_admin"] }, { key = "Editor", grants_to = ["team"] }]
redirectUris = ["https://monitoring.dev.immich.cloud/login/generic_oauth"]
},
{
name = "Grafana Data Prod"
roles = [{ key = "GrafanaAdmin", grants_to = ["immich_admin"] }, { key = "Editor", grants_to = ["team"] }]
redirectUris = ["https://grafana.data.immich.cloud/login/generic_oauth"]
},
{
name = "Outline"
roles = [
{ key = "Leadership", grants_to = ["immich_admin"] },
{ key = "Team", grants_to = ["team"] },
{ key = "Contributor", grants_to = ["contributor"] },
{ key = "Support Crew", grants_to = ["support"] }
]
authMethod = "BASIC"
redirectUris = ["https://outline.immich.cloud/auth/oidc.callback"]
},
{
name = "ContainerSSH"
roles = [
{ key = "Granted", grants_to = ["immich_admin", "team", "contributor"] }
]
appType = "NATIVE"
grantTypes = ["DEVICE_CODE"]
},
{
name = "OAuth2 Proxy"
roles = [{ key = "Granted", grants_to = ["immich_admin", "team"] }]
redirectUris = ["https://oauth2-proxy.internal.immich.cloud/oauth2/callback"]
},
{
name = "LoopDedupe"
roles = [{ key = "Granted", grants_to = ["immich_admin", "team", "contributor", "support"] }]
authMethod = "BASIC"
redirectUris = ["https://loopdedupe.internal.immich.cloud/oauth2/callback"]
},
{
name = "OVHCloud"
protocol = "saml"
roles = [{ key = "ADMIN", grants_to = ["immich_admin", "yucca"] }, { key = "DEFAULT", grants_to = ["team"] }]
# SP metadata is vendored rather than fetched at plan time: OVH gates
# https://auth.eu.ovhcloud.com/sso/saml/sp/metadata.xml behind a CDN
# Set-Cookie 307-redirect-to-self that the http provider (no cookie jar)
# can't satisfy, so `data.http` loops to 10 redirects and fails the apply.
# Re-fetch with `curl -sL -c jar -b jar <url>` if OVH rotates their SP cert.
metadataFile = "ovhcloud-sp-metadata.xml"
}
]
projects = [
for project in local.projects_data : merge(local.project_defaults, project)
]
oidc_projects = [
for project in local.projects : project if project.protocol == "oidc"
]
saml_projects = [
for project in local.projects : project if project.protocol == "saml"
]
}
resource "zitadel_project" "projects" {
for_each = { for project in local.projects : project.name => project }
name = each.value.name
org_id = zitadel_org.immich.id
project_role_check = true
project_role_assertion = true
has_project_check = false
}
resource "zitadel_application_oidc" "applications" {
for_each = { for project in local.oidc_projects : project.name => project }
name = upper(replace(each.value.name, "/[^a-zA-Z0-9]/", "_"))
org_id = zitadel_org.immich.id
project_id = zitadel_project.projects[each.key].id
redirect_uris = each.value.redirectUris
response_types = ["OIDC_RESPONSE_TYPE_CODE"]
grant_types = [for grant_type in each.value.grantTypes : "OIDC_GRANT_TYPE_${grant_type}"]
app_type = "OIDC_APP_TYPE_${each.value.appType}"
auth_method_type = "OIDC_AUTH_METHOD_TYPE_${each.value.authMethod}"
}
resource "onepassword_item" "application_client_id" {
for_each = { for application in zitadel_application_oidc.applications : application.name => application }
vault = data.onepassword_vault.tf.uuid
title = "ZITADEL_OAUTH_CLIENT_ID_${each.value.name}"
category = "password"
password = each.value.client_id
}
resource "onepassword_item" "application_client_secret" {
for_each = {
for application in zitadel_application_oidc.applications : application.name => application
if application.auth_method_type != "OIDC_AUTH_METHOD_TYPE_NONE"
}
vault = data.onepassword_vault.tf.uuid
title = "ZITADEL_OAUTH_CLIENT_SECRET_${each.value.name}"
category = "password"
password = each.value.client_secret
}
resource "zitadel_application_saml" "applications" {
for_each = { for project in local.saml_projects : project.name => project }
name = upper(replace(each.value.name, "/[^a-zA-Z0-9]/", "_"))
org_id = zitadel_org.immich.id
project_id = zitadel_project.projects[each.key].id
metadata_xml = file("${path.module}/assets/${each.value.metadataFile}")
}
@@ -1,10 +0,0 @@
provider "zitadel" {
domain = "zitadel.internal.immich.cloud"
insecure = false
jwt_profile_json = var.zitadel_profile_json
}
provider "onepassword" {
url = var.op_connect_url
token = var.op_connect_token
}
@@ -1,21 +0,0 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
include_in_copy = ["repo-files/*", "assets/*"]
}
include "root" {
path = find_in_parent_folders("root.hcl")
}
inputs = {
users_data_file_path = "${get_repo_root()}/tf/deployment/data/users.json"
}
dependencies {
paths = []
}
@@ -1,48 +0,0 @@
locals {
users_data = jsondecode(file(var.users_data_file_path))
}
resource "random_password" "zitadel_user_initial_password" {
for_each = {
for user in local.users_data : user.github.id => user
if try(user.github.username, "") != ""
}
length = 64
special = true
}
resource "zitadel_human_user" "users" {
for_each = {
for user in local.users_data : user.github.id => user
if try(user.github.username, "") != ""
}
email = "${each.value.github.id}+${each.value.github.username}@users.noreply.github.com"
first_name = each.value.github.username
last_name = "last_name"
user_name = each.value.github.username
org_id = zitadel_org.immich.id
initial_password = random_password.zitadel_user_initial_password[each.key].result
initial_skip_password_change = true
is_email_verified = true
is_phone_verified = false
lifecycle {
ignore_changes = [
first_name,
last_name,
email,
nick_name
]
}
}
resource "zitadel_user_metadata" "role" {
for_each = {
for user in local.users_data : user.github.id => user
if try(user.github.username, "") != ""
}
org_id = zitadel_org.immich.id
user_id = zitadel_human_user.users[each.key].id
key = "role"
value = jsonencode(each.value.roles)
}
@@ -1,18 +0,0 @@
variable "tf_state_postgres_conn_str" {}
variable "op_connect_url" {}
variable "op_connect_token" {
sensitive = true
}
variable "zitadel_profile_json" {}
variable "users_data_file_path" {
description = "The path to the JSON file containing user data. This path should be resolvable from the Terragrunt execution directory or be an absolute path."
type = string
}
variable "zitadel_github_client_id" {}
variable "zitadel_github_client_secret" {
sensitive = true
}