ce7cb702218693d08ea6d83464d2457c9250027c

This commit is contained in:
ms2k 2026-11-12 20:46:28 +09:00
commit 34c5f8963c
8 changed files with 145336 additions and 0 deletions

118
vendor/platform/zitadel/bootstrap.md vendored Normal file
View file

@ -0,0 +1,118 @@
# Zitadel Bootstrap
## 1. Terraform — Vault KV + database engine
```bash
vault kv put secret/cloudflare/api-token api-token="<cloudflare-api-token>"
vault kv put secret/postgres/admin username="********" password="<postgres-admin-password>"
```
```bash
cd terraform/internal/vault
../../scripts/tofu-versitygw-state apply \
-var="internal_domain=quorate.internal" \
-var='users=["<admin-user>"]' \
-var="admin_user=<admin-user>"
```
## 2. PostgreSQL — roles and database
```bash
cd terraform/internal/postgres
../../scripts/tofu-versitygw-state apply
```
## 3. Network policies
```bash
kubectl --context default apply -f vendor/platform/postgres/network-policies.yaml
kubectl --context default apply -f vendor/platform/vault/network-policies.yaml
```
```bash
vault read database/config/postgres
```
## 4. cert-manager — Cloudflare token + Let's Encrypt issuer
```bash
kubectl --context default apply -f vendor/platform/cert-manager/config/vso.yaml
kubectl --context default apply -f vendor/platform/cert-manager/config/issuers.yaml
```
```bash
kubectl --context default get secret cloudflare-api-token -n cert-manager
```
## 5. Gateway R
```bash
flux reconcile kustomization platform-cilium-config --context default -n flux-system --with-source
```
The `gateway-relay-tls` Certificate triggers a DNS-01 ACME challenge. Monitor:
```bash
kubectl --context default describe certificate gateway-relay-tls -n kube-system
```
Cloudflare declares `auth.quorate.dev` from the OCI relay state in `terraform/external/cloudflare`.
## 6. Zitadel namespace + network policies + VSO
```bash
kubectl --context default create namespace zitadel
# Pre-create the service account so VSO can authenticate before Helm runs
kubectl --context default create serviceaccount zitadel -n zitadel
kubectl --context default apply -f vendor/platform/zitadel/network-policies.yaml
kubectl --context default apply -f vendor/platform/zitadel/vso.yaml
```
Wait for both secrets:
```bash
kubectl --context default get secret zitadel-masterkey -n zitadel
kubectl --context default get secret zitadel-db -n zitadel
```
## 7. Helm install
```bash
helm repo add zitadel https://charts.zitadel.com
helm upgrade --install zitadel zitadel/zitadel \
--namespace zitadel \
-f vendor/platform/zitadel/values.yaml
```
## 8. HTTPRoute
```bash
kubectl --context default apply -f vendor/platform/zitadel/route.yaml
```
## 9. Post-deploy — organization and OIDC clients
Once Zitadel is up and the IAM admin machine key secret exists, mirror it to Vault for Terraform:
```bash
kubectl --context default get secret iam-admin -n zitadel -o jsonpath='{.data.iam-admin\.json}' | base64 -d > iam-admin.json
vault kv put secret/zitadel/iam-admin iam-admin.json=@iam-admin.json
```
```bash
cd terraform/internal/zitadel
../../scripts/tofu-versitygw-state apply -var="zitadel_domain=auth.quorate.dev"
```
This creates the primary service organization, the shared service project, and the Forgejo OIDC application.
The Forgejo OIDC client material is mirrored into Vault for later VSO sync. Use the non-secret outputs when configuring or verifying Forgejo's OAuth2 authentication source:
```bash
tofu output forgejo_oidc_authentication_name
tofu output forgejo_oidc_discovery_url
tofu output forgejo_oidc_redirect_uri
tofu output forgejo_oidc_vault_secret_path
```
The Forgejo authentication source name must stay aligned with the redirect URI: `https://forge.quorate.internal/user/oauth2/Zitadel/callback`.

View file

@ -0,0 +1,132 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: zitadel
namespace: zitadel
spec:
interval: 15m
releaseName: zitadel
targetNamespace: zitadel
chart:
spec:
chart: zitadel
sourceRef:
kind: HelmRepository
name: zitadel
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
image:
tag: v4.20.0@sha256:9ea0e9ecfdc43115da871242e40a0320f2ece864c7cd8b5d41a863a7f4902499
replicaCount: 2
service:
labels:
health.quorate.dev/name: Auth
serviceAccount:
create: false
name: zitadel
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
privileged: false
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
tools:
kubectl:
image:
tag: 1.37.1@sha256:712dc5385d1c1a3f83b747fa9e40f64d433a97f72ff42f9755ff7dd1f62846e4
wait4x:
image:
tag: 3.7.3@sha256:aae91ea65a38117ac07c84c7e1122691ac91c3b60529edcb8fe62292d105d8ff
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 64Mi
zitadel:
masterkeySecretName: zitadel-masterkey
configmapConfig:
ExternalDomain: auth.quorate.dev
ExternalSecure: true
ExternalPort: 443
TLS:
Enabled: false
Database:
Postgres:
Host: main-rw.postgres.svc.cluster.local
Port: 5432
Database: zitadel
MaxOpenConns: 20
MaxIdleConns: 10
ConnMaxLifetime: 30m
User:
Username: "********"
SSL:
Mode: require
Admin:
Username: "********"
SSL:
Mode: require
env:
- name: ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db
key: password
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db
key: password
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: zitadel
topologyKey: kubernetes.io/hostname
pdb:
enabled: true
minAvailable: 1
login:
enabled: true
replicaCount: 2
image:
repository: ghcr.io/zitadel/zitadel-login
tag: v4.20.0@sha256:f1abfdf3c5d16eda03cad3b584226207ecc640ca5c679d7c45abbbce7d1d63f0
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
initJob:
command: zitadel
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
setupJob:
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi

View file

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- vso.yaml
- helm-release.yaml
- oidc-internal.yaml
- network-policies.yaml
- route.yaml

13
vendor/platform/zitadel/namespace.yaml vendored Normal file
View file

@ -0,0 +1,13 @@
---
apiVersion: v1
kind: Namespace
metadata:
labels:
gateway.quorate.internal/allow-public: "true"
name: zitadel
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: zitadel
namespace: zitadel

View file

@ -0,0 +1,357 @@
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: zitadel
namespace: zitadel
spec:
endpointSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- zitadel
- zitadel-login
ingress:
# zitadel-login pod needs ingress to zitadel pods for the init wait and API communication
- fromEndpoints:
- matchLabels:
app.kubernetes.io/name: zitadel-login
toPorts:
- ports:
- port: "8080"
protocol: TCP
# Some Gateway/LB backend flows are sourced from the node host path.
- fromEntities:
- host
toPorts:
- ports:
- port: "8080"
protocol: TCP
- port: "3000"
protocol: TCP
- fromEntities:
- ingress
toPorts:
- ports:
- port: "8080"
protocol: TCP
- port: "3000"
protocol: TCP
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
gateway.networking.k8s.io/gateway-name: gateway-relay
toPorts:
- ports:
- port: "8080"
protocol: TCP
- port: "3000"
protocol: TCP
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
toPorts:
- ports:
- port: "8080"
protocol: TCP
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: postgres
cnpg.io/cluster: main
toPorts:
- ports:
- port: "5432"
protocol: TCP
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
# zitadel-login needs to talk to the zitadel API
- toEndpoints:
- matchLabels:
app.kubernetes.io/name: zitadel
toPorts:
- ports:
- port: "8080"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: zitadel-stalwart-submission
namespace: zitadel
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: zitadel
app.kubernetes.io/instance: zitadel
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: stalwart
app.kubernetes.io/name: stalwart
app.kubernetes.io/instance: stalwart
toPorts:
- ports:
- port: "587"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: zitadel-oidc-internal
namespace: zitadel
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
ingress:
- fromEntities:
- host
toPorts:
- ports:
- port: "8080"
protocol: TCP
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: longhorn
app.kubernetes.io/name: oauth2-proxy
app.kubernetes.io/instance: longhorn
toPorts:
- ports:
- port: "8080"
protocol: TCP
- port: "8443"
protocol: TCP
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: frigate
app.kubernetes.io/name: oauth2-proxy
app.kubernetes.io/instance: frigate
toPorts:
- ports:
- port: "8080"
protocol: TCP
- port: "8443"
protocol: TCP
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: stalwart
app.kubernetes.io/instance: stalwart
- matchLabels:
k8s:io.kubernetes.pod.namespace: quay
app.kubernetes.io/name: quay
app.kubernetes.io/instance: quay-registry
- matchLabels:
k8s:io.kubernetes.pod.namespace: forgejo
app.kubernetes.io/name: forgejo
app.kubernetes.io/instance: forgejo
- matchLabels:
k8s:io.kubernetes.pod.namespace: forgejo-public
app.kubernetes.io/name: forgejo
app.kubernetes.io/instance: forge-git
- matchLabels:
k8s:io.kubernetes.pod.namespace: grafana
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
toPorts:
- ports:
- port: "8443"
protocol: TCP
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel
toPorts:
- ports:
- port: "8080"
protocol: TCP
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: oidc-relay-client
spec:
endpointSelector:
matchLabels:
quorate.dev/zitadel-relay: "true"
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
toPorts:
- ports:
- port: "8443"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: host-egress-zitadel-oidc-internal
spec:
nodeSelector: {}
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel-oidc-internal
toPorts:
- ports:
- port: "8080"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: gateway-relay-egress-zitadel
spec:
endpointSelector:
matchLabels:
"reserved:ingress": ""
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel
toPorts:
- ports:
- port: "8080"
protocol: TCP
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel-login
toPorts:
- ports:
- port: "3000"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: host-egress-zitadel
spec:
nodeSelector: {}
egress:
- toEndpoints:
- matchExpressions:
- key: k8s:io.kubernetes.pod.namespace
operator: In
values: ["zitadel"]
- key: app.kubernetes.io/name
operator: In
values: ["zitadel", "zitadel-login"]
toPorts:
- ports:
- port: "8080"
protocol: TCP
- port: "3000"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: gateway-client-to-zitadel
spec:
endpointSelector:
matchLabels:
gateway.quorate.internal/allow-gateway-client: "true"
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel
toPorts:
- ports:
- port: "8080"
protocol: TCP
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: zitadel
app.kubernetes.io/name: zitadel-login
toPorts:
- ports:
- port: "3000"
protocol: TCP
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: zitadel-ingress-gateway-clients
spec:
endpointSelector:
matchExpressions:
- key: k8s:io.kubernetes.pod.namespace
operator: In
values: ["zitadel"]
- key: app.kubernetes.io/name
operator: In
values: ["zitadel", "zitadel-login"]
ingress:
- fromEndpoints:
- matchLabels:
gateway.quorate.internal/allow-gateway-client: "true"
toPorts:
- ports:
- port: "8080"
protocol: TCP
- port: "3000"
protocol: TCP
---
# Same-node Gateway/LB backend selections can return to node-local ephemeral
# source ports via the host path. Keep this with Zitadel even though the policy
# kind is clusterwide, because the exception exists only for this app path.
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
name: host-ingress-zitadel-replies
spec:
nodeSelector: {}
ingress:
- fromEndpoints:
- matchExpressions:
- key: k8s:io.kubernetes.pod.namespace
operator: In
values: ["zitadel"]
- key: app.kubernetes.io/name
operator: In
values: ["zitadel", "zitadel-login"]
toPorts:
- ports:
- port: "32768"
endPort: 60999
protocol: TCP

View file

@ -0,0 +1,217 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: zitadel-oidc-internal
namespace: zitadel
labels:
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
app.kubernetes.io/component: oidc-internal
data:
nginx.conf: |
worker_processes auto;
error_log /dev/stderr notice;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
sendfile on;
keepalive_timeout 65;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
map $ssl_server_name $oidc_certificate {
default auth.quorate.dev;
}
include /etc/nginx/conf.d/*.conf;
}
default.conf: |
server {
listen 8080;
listen 8443 ssl;
server_name auth.quorate.dev;
client_max_body_size 1m;
ssl_certificate /run/tls/$oidc_certificate.crt;
ssl_certificate_key /run/tls/$oidc_certificate.key;
ssl_protocols TLSv1.2 TLSv1.3;
proxy_http_version 1.1;
proxy_set_header Host auth.quorate.dev;
proxy_set_header X-Forwarded-Host auth.quorate.dev;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
location = /healthz {
access_log off;
default_type text/plain;
return 200 'ok';
}
location = /oauth/v2/token {
limit_except POST {
deny all;
}
proxy_pass http://zitadel.zitadel.svc.cluster.local:8080;
}
location = /oauth/v2/keys {
limit_except GET HEAD {
deny all;
}
proxy_pass http://zitadel.zitadel.svc.cluster.local:8080;
}
location = /oidc/v1/userinfo {
limit_except GET HEAD {
deny all;
}
proxy_pass http://zitadel.zitadel.svc.cluster.local:8080;
}
location = /.well-known/openid-configuration {
limit_except GET HEAD {
deny all;
}
proxy_pass http://zitadel.zitadel.svc.cluster.local:8080;
}
location / {
return 404;
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: zitadel-oidc-internal
namespace: zitadel
labels:
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
app.kubernetes.io/component: oidc-internal
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
template:
metadata:
labels:
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
app.kubernetes.io/component: oidc-internal
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: cgr.dev/chainguard/nginx@sha256:0b4de0f60ae2295fdc852564d178b5473109baf9fc826fa5eb38a59f930e218e
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: https
containerPort: 8443
protocol: TCP
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: conf-d
mountPath: /etc/nginx/conf.d
readOnly: true
- name: tmp
mountPath: /tmp
- name: tls
mountPath: /run/tls
readOnly: true
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 1
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 15
volumes:
- name: nginx-conf
configMap:
name: zitadel-oidc-internal
items:
- key: nginx.conf
path: nginx.conf
- name: conf-d
configMap:
name: zitadel-oidc-internal
items:
- key: default.conf
path: default.conf
- name: tmp
emptyDir: {}
- name: tls
secret:
secretName: gateway-relay-tls
defaultMode: 0440
items:
- key: tls.crt
path: auth.quorate.dev.crt
- key: tls.key
path: auth.quorate.dev.key
---
apiVersion: v1
kind: Service
metadata:
name: zitadel-oidc-internal
namespace: zitadel
labels:
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
app.kubernetes.io/component: oidc-internal
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: zitadel-oidc-internal
app.kubernetes.io/instance: zitadel
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
- name: https
port: 443
targetPort: https
protocol: TCP

58
vendor/platform/zitadel/route.yaml vendored Normal file
View file

@ -0,0 +1,58 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: zitadel
namespace: zitadel
spec:
parentRefs:
- name: gateway-relay
namespace: kube-system
sectionName: https
hostnames:
- auth.quorate.dev
rules:
- matches:
- path:
type: Exact
value: /robots.txt
filters:
- type: RequestRedirect
requestRedirect:
hostname: quorate.dev
port: 443
statusCode: 301
path:
type: ReplaceFullPath
replaceFullPath: /robots-deny.txt
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: X-Robots-Tag
value: noindex
- matches:
- path:
type: PathPrefix
value: /ui/v2/login
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: X-Robots-Tag
value: noindex
backendRefs:
- name: zitadel-login
port: 3000
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: X-Robots-Tag
value: noindex
backendRefs:
- name: zitadel
port: 8080

46
vendor/platform/zitadel/vso.yaml vendored Normal file
View file

@ -0,0 +1,46 @@
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: default
namespace: zitadel
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: zitadel
serviceAccount: zitadel
vaultConnectionRef: vault/vault
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: zitadel-masterkey
namespace: zitadel
spec:
type: kv-v2
mount: secret
path: zitadel/master-key
destination:
name: zitadel-masterkey
create: true
refreshAfter: 1h
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: zitadel-db
namespace: zitadel
spec:
mount: database
path: static-creds/zitadel
allowStaticCreds: true
refreshAfter: 5m
destination:
name: zitadel-db
create: true
rolloutRestartTargets:
- kind: Deployment
name: zitadel
vaultAuthRef: default