Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2485b5f165 | ||
|
|
056964075e | ||
|
|
a1bb64dd98 | ||
|
|
438fcbe9fe | ||
|
|
afec7e9867 | ||
|
|
fc63acbcc9 | ||
|
|
ac73035c9e |
@@ -13,9 +13,16 @@ metadata:
|
||||
# WebSocket support for ComfyUI's live preview
|
||||
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||
nginx.ingress.kubernetes.io/upstream-hash-by: "$remote_addr"
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
# Single sign-on. ComfyUI has no auth of its own, so every request is handed to
|
||||
# oauth2-proxy for an allow or deny decision.
|
||||
nginx.ingress.kubernetes.io/auth-url: "http://comfyui-oauth2-proxy.comfyui.svc.cluster.local:4180/oauth2/auth"
|
||||
nginx.ingress.kubernetes.io/auth-signin: "https://comfyui.riotpiao.com/oauth2/start?rd=$scheme://$host$request_uri"
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "x-auth-request-user,x-auth-request-email"
|
||||
# No configuration-snippet. The controller rejects that annotation as risky
|
||||
# ("annotation group ConfigurationSnippet contains risky annotation"), and a
|
||||
# rejected annotation drops the WHOLE ingress from nginx.conf: the host then
|
||||
# answers 404 with no entry in the access log. ingress-nginx sets the Upgrade
|
||||
# and Connection headers itself for a websocket upgrade.
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
|
||||
@@ -4,4 +4,5 @@ kind: Kustomization
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- oauth2-proxy.yaml
|
||||
- ingress.yaml
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# ComfyUI has no authentication of its own: comfyui.riotpiao.com is open to anyone
|
||||
# who can resolve the host. It also has no OIDC support, so ZITADEL cannot be a
|
||||
# login method inside it. oauth2-proxy is the front door instead: nginx asks it for
|
||||
# a decision on every request and it answers from a ZITADEL session.
|
||||
#
|
||||
# No API client calls this host today, so the whole host is gated. If a client ever
|
||||
# needs direct access, add a skip rule for the exact paths it uses rather than
|
||||
# opening the whole host.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: comfyui-oauth2-proxy
|
||||
namespace: comfyui
|
||||
labels:
|
||||
app: comfyui-oauth2-proxy
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: comfyui-oauth2-proxy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: comfyui-oauth2-proxy
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
seccompProfile: {type: RuntimeDefault}
|
||||
containers:
|
||||
- name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
|
||||
args:
|
||||
- --provider=oidc
|
||||
- --oidc-issuer-url=https://zitadel.riotpiao.com
|
||||
- --email-domain=*
|
||||
- --upstream=static://202
|
||||
- --reverse-proxy=true
|
||||
- --set-xauthrequest=true
|
||||
- --cookie-secure=true
|
||||
- --cookie-expire=12h
|
||||
- --cookie-refresh=1h
|
||||
- --skip-provider-button=true
|
||||
- --redirect-url=https://comfyui.riotpiao.com/oauth2/callback
|
||||
- --http-address=0.0.0.0:4180
|
||||
env:
|
||||
- name: OAUTH2_PROXY_CLIENT_ID
|
||||
valueFrom: {secretKeyRef: {name: comfyui-oauth2-proxy, key: client-id}}
|
||||
- name: OAUTH2_PROXY_CLIENT_SECRET
|
||||
valueFrom: {secretKeyRef: {name: comfyui-oauth2-proxy, key: client-secret}}
|
||||
- name: OAUTH2_PROXY_COOKIE_SECRET
|
||||
valueFrom: {secretKeyRef: {name: comfyui-oauth2-proxy, key: cookie-secret}}
|
||||
ports:
|
||||
- {name: http, containerPort: 4180}
|
||||
readinessProbe:
|
||||
httpGet: {path: /ping, port: http}
|
||||
initialDelaySeconds: 5
|
||||
livenessProbe:
|
||||
httpGet: {path: /ping, port: http}
|
||||
initialDelaySeconds: 15
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: {drop: ["ALL"]}
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {cpu: 200m, memory: 128Mi}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: comfyui-oauth2-proxy
|
||||
namespace: comfyui
|
||||
labels:
|
||||
app: comfyui-oauth2-proxy
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: comfyui-oauth2-proxy
|
||||
ports:
|
||||
- {name: http, port: 4180, targetPort: http}
|
||||
---
|
||||
# The login path must NOT be gated: a request to /oauth2/start that itself demands a
|
||||
# login redirects to /oauth2/start forever. ingress-nginx applies the annotations of
|
||||
# an ingress to the locations it defines, so a separate object without auth-url keeps
|
||||
# this path open. The earlier failure of this shape was not the split — it was the
|
||||
# configuration-snippet on the app ingress, which made the controller drop the whole
|
||||
# object and left only this one alive.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: comfyui-oauth2
|
||||
namespace: comfyui
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: comfyui.riotpiao.com
|
||||
http:
|
||||
paths:
|
||||
- path: /oauth2
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: comfyui-oauth2-proxy
|
||||
port:
|
||||
number: 4180
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
data:
|
||||
KMSVC_KAFKA_BROKERS: {{ .Values.env.kafkaBrokers | quote }}
|
||||
KMSVC_REDIS_ADDR: {{ .Values.env.redisAddr | quote }}
|
||||
KMSVC_AUTHENTIK_ISSUER_URL: {{ .Values.env.authentikIssuerURL | quote }}
|
||||
KMSVC_AUTHENTIK_AUDIENCE: {{ .Values.env.authentikAudience | quote }}
|
||||
KMSVC_ZITADEL_ISSUER_URL: {{ .Values.env.zitadelIssuerURL | quote }}
|
||||
KMSVC_ZITADEL_AUDIENCE: {{ .Values.env.zitadelAudience | quote }}
|
||||
KMSVC_GRPC_LISTEN_ADDR: ":{{ .Values.grpcPort }}"
|
||||
KMSVC_HTTP_LISTEN_ADDR: ":{{ .Values.httpPort }}"
|
||||
|
||||
@@ -12,8 +12,8 @@ httpPort: 8080
|
||||
env:
|
||||
kafkaBrokers: "kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092"
|
||||
redisAddr: "kmsvc-redis-master.sqs.svc.cluster.local:6379"
|
||||
authentikIssuerURL: ""
|
||||
authentikAudience: ""
|
||||
zitadelIssuerURL: ""
|
||||
zitadelAudience: ""
|
||||
|
||||
resources:
|
||||
requests:
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: secretrotations.homelab.riotpiao.com
|
||||
spec:
|
||||
group: homelab.riotpiao.com
|
||||
names:
|
||||
kind: SecretRotation
|
||||
plural: secretrotations
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
type: object
|
||||
required:
|
||||
- provider
|
||||
- rotationInterval
|
||||
properties:
|
||||
# External system: authentik | forgejo | minio | vault
|
||||
provider:
|
||||
type: string
|
||||
enum: [authentik, forgejo, minio, vault]
|
||||
|
||||
# How often to rotate (hours)
|
||||
rotationInterval:
|
||||
type: integer
|
||||
minimum: 24
|
||||
|
||||
# Application ID in external system
|
||||
appId:
|
||||
type: string
|
||||
|
||||
# k8s Secret to update (name, namespace, key)
|
||||
secretRef:
|
||||
type: object
|
||||
required: [name, namespace]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
description: "Secret key to update (e.g., MINIO_IDENTITY_OPENID_CLIENT_SECRET)"
|
||||
|
||||
# Path to git file that holds the secret (for .enc.yaml files)
|
||||
gitPath:
|
||||
type: string
|
||||
description: "Path in homelab repo to .enc.yaml file"
|
||||
|
||||
# Ansible template values to substitute
|
||||
templateValues:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
lastRotationTime:
|
||||
type: string
|
||||
format: date-time
|
||||
nextRotationTime:
|
||||
type: string
|
||||
format: date-time
|
||||
lastRotationStatus:
|
||||
type: string
|
||||
enum: [Success, Failed, Pending]
|
||||
lastRotationError:
|
||||
type: string
|
||||
lastCommitHash:
|
||||
type: string
|
||||
|
||||
---
|
||||
# Example usage:
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: minio-oidc
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: authentik
|
||||
rotationInterval: 2160 # 90 days in hours
|
||||
appId: minio
|
||||
secretRef:
|
||||
name: minio-oidc
|
||||
namespace: storage
|
||||
key: MINIO_IDENTITY_OPENID_CLIENT_SECRET
|
||||
gitPath: k8s/argocd/secrets/minio-oidc.enc.yaml
|
||||
|
||||
---
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: portfolio-agent-oidc
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: authentik
|
||||
rotationInterval: 2160
|
||||
appId: portfolio-agent
|
||||
secretRef:
|
||||
name: portfolio-agent-oidc
|
||||
namespace: portfolio
|
||||
key: CLIENT_SECRET
|
||||
gitPath: k8s/argocd/secrets/portfolio-agent-oidc.enc.yaml
|
||||
|
||||
---
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: forgejo-registry-token
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: forgejo
|
||||
rotationInterval: 2160
|
||||
appId: rock/riotpiao.com
|
||||
secretRef:
|
||||
name: forgejo-registry-secret
|
||||
namespace: kube-system
|
||||
key: REGISTRY_TOKEN
|
||||
gitPath: k8s/argocd/secrets/forgejo-registry-secret.enc.yaml
|
||||
|
||||
---
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: minio-root-credentials
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: minio
|
||||
rotationInterval: 4320 # 180 days in hours
|
||||
appId: root
|
||||
secretRef:
|
||||
name: minio-creds
|
||||
namespace: storage
|
||||
gitPath: k8s/argocd/secrets/minio-secrets.enc.yaml
|
||||
@@ -1,92 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: secret-rotation-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: secret-rotation-controller
|
||||
spec:
|
||||
serviceAccountName: secret-rotation-controller
|
||||
containers:
|
||||
- name: controller
|
||||
image: secret-rotation-controller:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
# SOPS reads age key from this file
|
||||
- name: SOPS_AGE_KEY_FILE
|
||||
value: /etc/sops/age/private-key.txt
|
||||
|
||||
# Vault auth (token in projected volume)
|
||||
- name: VAULT_ADDR
|
||||
value: http://vault.vault.svc.cluster.local:8200
|
||||
- name: VAULT_TOKEN_FILE
|
||||
value: /var/run/secrets/vault/token
|
||||
|
||||
# Authentik
|
||||
- name: AUTHENTIK_URL
|
||||
value: http://authentik-server.iam.svc.cluster.local
|
||||
- name: AUTHENTIK_BOOTSTRAP_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-bootstrap
|
||||
key: token
|
||||
|
||||
# Git
|
||||
- name: GIT_REPO
|
||||
value: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
- name: GIT_AUTHOR_EMAIL
|
||||
value: [email protected]
|
||||
- name: GIT_AUTHOR_NAME
|
||||
value: Secret Rotation Controller
|
||||
- name: FORGEJO_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: forgejo-registry-secret
|
||||
key: REGISTRY_TOKEN
|
||||
|
||||
volumeMounts:
|
||||
# Age key from ExternalSecret (synced from Vault)
|
||||
- name: age-key
|
||||
mountPath: /etc/sops/age
|
||||
readOnly: true
|
||||
|
||||
# Vault auth token (projected)
|
||||
- name: vault-token
|
||||
mountPath: /var/run/secrets/vault
|
||||
readOnly: true
|
||||
|
||||
# Temp working dir
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
volumes:
|
||||
- name: age-key
|
||||
secret:
|
||||
secretName: sops-age-key
|
||||
defaultMode: 0400
|
||||
|
||||
- name: vault-token
|
||||
projected:
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
path: token
|
||||
audience: vault
|
||||
expirationSeconds: 3600
|
||||
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: secret-rotation
|
||||
|
||||
resources:
|
||||
- rbac.yaml
|
||||
- crd.yaml
|
||||
- external-secret.yaml
|
||||
- deployment.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: secret-rotation-controller
|
||||
app.kubernetes.io/component: automation
|
||||
managed-by: argocd
|
||||
@@ -1,53 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
namespace: secret-rotation
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
rules:
|
||||
# Read SecretRotation CRDs
|
||||
- apiGroups: ["homelab.riotpiao.com"]
|
||||
resources: ["secretrotations"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Update status
|
||||
- apiGroups: ["homelab.riotpiao.com"]
|
||||
resources: ["secretrotations/status"]
|
||||
verbs: ["get", "patch", "update"]
|
||||
|
||||
# Read k8s secrets that will be rotated
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list"]
|
||||
|
||||
# For recording events
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["create", "patch"]
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: secret-rotation-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: secret-rotation-controller
|
||||
namespace: secret-rotation
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: secret-rotation
|
||||
labels:
|
||||
kubernetes.io/metadata.name: secret-rotation
|
||||
@@ -1,32 +0,0 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: secret-rotation
|
||||
namespace: argocd
|
||||
labels:
|
||||
app.kubernetes.io/name: secret-rotation
|
||||
spec:
|
||||
project: homelab
|
||||
|
||||
sources:
|
||||
- repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
path: k8s/apps/secret-rotation-controller
|
||||
targetRevision: main
|
||||
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: secret-rotation
|
||||
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- RespectIgnoreDifferences=true
|
||||
retry:
|
||||
limit: 5
|
||||
backoff:
|
||||
duration: 5s
|
||||
factor: 2
|
||||
maxDuration: 3m
|
||||
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: comfyui-oauth2-proxy
|
||||
namespace: comfyui
|
||||
type: Opaque
|
||||
data:
|
||||
client-id: ENC[AES256_GCM,data:jFLgcrh1tiAU0pijGxy+/mFsFHzI5zSi,iv:7GYL4xtPnzyZWtEA0egPHhzvpXGevkIH3Cvr+oD9hKI=,tag:UeDTIKlfcYnOOOJiPNgN0g==,type:str]
|
||||
client-secret: ENC[AES256_GCM,data:9QFu5a2EkEMjc/B0moPIkMEChrN0Qh5BQtAzFcvW4liKxzVAPbTKgQOJeND+NQjn5ZoE89kqqRyqNJF3bT3H1pWoDFngk9J2X7BN/9r6Qyubias0Jukydw==,iv:xf2kT0fbnJat3L3xxUYm3ht0YvHptU/RIdt9Eh0D6xU=,tag:8iknaQ2kKP1OZ/1jRCa+1g==,type:str]
|
||||
cookie-secret: ENC[AES256_GCM,data:Qq+l4go/CFtfsZrgYUcTj9bB4sreu2GEqJnu15H6fxIlD7IgnmrodPoIVNpWcdm6LSYabGjm2jraN6RP,iv:EOoDHgvIydzHCC5POohjPq3Vj2YTpzhIhvdG2FzKKzc=,tag:iDXqzFU1Z79bzpG4I4xfww==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxOUtCUDduMVU5U1J4WXZX
|
||||
Q1VzaFRVdXlHaEZpeVJNOWprdDhUOEd6Z1hzCi9QZEpXN3NDSENzUE9RTGdpb3Va
|
||||
eVNHVTFLbXpMU0x5UWlFLzZnVlllWE0KLS0tIDZldVNqR1loNHlXZWovNWZ4WjFD
|
||||
dVErenJDM3haSStxZEVWZUtBbDBIZ1UK9x6Gy4drjfUz2goAaI+oeUDWdNHQFAJl
|
||||
JGI+X81r/6Krg5N8QDa/mE96E2M2G0yAO5FS1YbpCKOITUEyheacnQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-10-27T21:17:08Z"
|
||||
mac: ENC[AES256_GCM,data:tTGmWFidEWTWHgKPV7b5ABOJwwlP1yQDK5Jd60c9ko6IsmP2GBBPCth7kJgPXuPcw+NHbJ7yt7bcVto7GkPdtY9daJmjPq0Gt16ZO4Xi4hCk22JVKqi/KJcKD/KB0r/VO9w093/Who5CcYgqifextfca4CuxS5m5dcOn4kRJCfE=,iv:0fDvvQr9wp8IrD16u88Ml4HsPEeMhd8R3ljQJoIpw3E=,tag:AanM7C6vbaBBmus1u1GRoA==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
@@ -1,23 +1,28 @@
|
||||
apiVersion: ENC[AES256_GCM,data:rGU=,iv:w1yJeQvN0MPZlPx8VQZmC6zkRaHW4rtjjROea0Kx9X4=,tag:qgYgVUGMQ2BtN0RZ0nZYjQ==,type:str]
|
||||
kind: ENC[AES256_GCM,data:wp2QP1he,iv:C8c/jKh5pnBOtHqXsTEchGf9DACU/5hOJ0Mp/+Jk1kg=,tag:QUZNeguRXdi3JO6Ov6GMDg==,type:str]
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:jVsnAcFTlRiKxDoU,iv:bWEqo28kH3bxzfI3ipiY6dsqhIcl9wIxBjFu0mxVGLg=,tag:PVWTveI8xhvLxvXPldhimQ==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:ZHnVZyd3wA==,iv:3evUnFTQfRz/ZilUQIl/9dsCba3nsX6o/iQsGXMBfgM=,tag:+9kp+VRu1ksroDTbTGmGPA==,type:str]
|
||||
type: ENC[AES256_GCM,data:bHX+Bu9o,iv:8qqNddsuw4YoHAuJfUcPIqjcqNySIh5Ln8OhsfZxuk8=,tag:P7QGUXo9Io2Y19YqlWv5sQ==,type:str]
|
||||
name: grafana-oidc
|
||||
namespace: logging
|
||||
type: Opaque
|
||||
stringData:
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ENC[AES256_GCM,data:nJz9pqa6w9ZGOjanZ32D38Bolg2YrdHs1crhb2uZkKyaaKDpklVb4FOKZ46K+NS1NXjoS6XDUAlV69CSvm1PeA==,iv:g9F/iLuzNC0WogtDJ/TAb3+97N6iGkbOTlb4SzYzwx8=,tag:4F3kUheGbtW2U6jgOvRO1A==,type:str]
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ENC[AES256_GCM,data:fGqA1ofVme1lVLxFjWIiU7sbvE9ip2Af7C1ua3+HdidlcVgkwW59vVlXZhgBRdoif86C8LKQSyoCGUqiO+740g==,iv:6Kv2ybbJg1GT8DLqz9P5y+E1zr7vcIBDMUErmvxNTaw=,tag:0wK28B3G/owCVPPqb2sLmQ==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- enc: |
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrUkxLMEV3aEpILytic0Ux
|
||||
V2VEV2EvcENJVnNNRUtBbGkwT0d6eUxYeEFvCjZPSkU2MG5qME5jZ01mRVJwTUFL
|
||||
M3M0V3c3QXBoRml1Vm9NWW5JTlNRSG8KLS0tIEJnNlFmdFJXb1BPaVJzTmo1RDQv
|
||||
VXZOVHRHUEZJRUZ4eGZGM1RxSURyWXcK3ZyYCPhRUvpvT/pjPQGJoLIwaktZY6Tg
|
||||
LS0cIDUhjPFbeu7qUATOrat6vMUi0UREbXSFZ1KyAjNAwJZaUusPMA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArTXMxMWdVRHIvbWh1bVNk
|
||||
bEFFNlgzNU9zTjlzY2o3VGdnbFpnU2lkYmxrCjF4aWVGSlJaNkcxODJ6RDkrVFo2
|
||||
UkV2RUwwVHFCd0xMNG9RWXdTUHZGaXcKLS0tIHVaWjhaNHRtS2c0UkVzRk83ckhj
|
||||
OCtBVFR0d1F5amVhV2E0emRZWThvYjAKFW46i9brIL9ZoO1CEn+IewIdoUhvYK/A
|
||||
RqzZ6xV+mhlDj0zC+SpVcEm/Ki83nMYOj4+vQingZlr/Kr3Nwb0iLQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-10-26T04:03:16Z"
|
||||
mac: ENC[AES256_GCM,data:0cgVNtFMwhbEHBxu7T1XfstcQlgUTxijq9Mz36zC60E7kYXRNwGt2/0KoCaqaE0leypwAdrPn2OjeoDPeCN6x+n0vLRVX2tnli6HazBLbRsPHljDAV0ASF07iNhb0PvgtWay432Mm+sGATkel3ITdAaP0lISNLUYfEe1/TbYHkk=,iv:FZ9Bgtk7LOC3aOSOo1+50zwLGhlcUZYJaUQ9r05cAn0=,tag:tsLQ1frZ7li5l717CCtgoQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
lastmodified: "2026-10-27T19:44:32Z"
|
||||
mac: ENC[AES256_GCM,data:EJew+p/kJrurcNpLlFxJGTl69hQYxn3IkzP7EDtdSriW/WI7X1xNLC2dp/dTiLomGmG1uh6+2JI9Sc8gXmrRAWLELAbiOaYbcoU6gi89BjTCWGDAMX4lNSIuH40rsGs4YtYj5U7SGfgx5qto4ZH7LxjBmxzPWM/Xz2Uq/I5jV4k=,iv:LdhjhwCfPUIa7D0gved8Twf7ujN+dg3lN8lCnFBFs0g=,tag:bjkwUCMtR8IBL9BkbuXcnw==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
|
||||
@@ -15,6 +15,7 @@ files:
|
||||
- forgejo-registry-pull.enc.yaml
|
||||
- forgejo-runner-token.enc.yaml
|
||||
- forgejo-secrets.enc.yaml
|
||||
- comfyui-oauth2-proxy.enc.yaml
|
||||
- grafana-oidc-secrets.enc.yaml
|
||||
- grafana-secrets.enc.yaml
|
||||
- homelab-ca-secrets.enc.yaml
|
||||
|
||||
@@ -88,6 +88,20 @@ ROLES = {
|
||||
# permission string, so consumer manifests did not change.
|
||||
"llm:inference": "LLM inference access (api-gw)",
|
||||
"llm-inference": "LLM inference access (dash alias)",
|
||||
# Per-service capabilities. The gateway derives the required capability from
|
||||
# the verb: GET/HEAD/OPTIONS -> "<service>:read", anything else ->
|
||||
# "<service>:write" (internal/serviceadapter/router.go:142). The role key
|
||||
# must match that string exactly, colon included.
|
||||
"memory:read": "Poimen memory read (api-gw)",
|
||||
"memory:write": "Poimen memory write (api-gw)",
|
||||
"workflow:read": "Temporal workflow read (api-gw)",
|
||||
"workflow:execute": "Temporal workflow execute (api-gw)",
|
||||
"s3:read": "Object store read (api-gw)",
|
||||
"s3:write": "Object store write (api-gw)",
|
||||
"iam:read": "Identity read (api-gw)",
|
||||
"iam:write": "Identity write (api-gw)",
|
||||
"sqs:read": "Queue read (api-gw)",
|
||||
"sqs:write": "Queue write (api-gw)",
|
||||
}
|
||||
|
||||
_WEB = {"app_type_id": "OIDC_APP_TYPE_WEB", "auth_method_id": "OIDC_AUTH_METHOD_TYPE_BASIC"}
|
||||
@@ -108,6 +122,11 @@ OIDC_APPS = {
|
||||
"https://forgejo.riotpiao.com/user/oauth2/zitadel/callback",
|
||||
"http://localhost:3000/user/oauth2/authentik/callback",
|
||||
"https://forgejo.riotpiao.com/user/oauth2/authentik/callback"]},
|
||||
# ComfyUI has no native OIDC. oauth2-proxy is its front door and this app is
|
||||
# the proxy's client, so the callback path belongs to the proxy.
|
||||
"comfyui": {**_WEB, "redirect_uris": [
|
||||
"http://localhost:4180/oauth2/callback",
|
||||
"https://comfyui.riotpiao.com/oauth2/callback"]},
|
||||
"grafana": {**_WEB, "redirect_uris": [
|
||||
"http://localhost:3000/login/generic_oauth",
|
||||
"https://grafana.riotpiao.com/login/generic_oauth"]},
|
||||
@@ -336,13 +355,16 @@ def _ensure_roles(token: str, project_id: str, roles: dict[str, str]) -> None:
|
||||
die(f"create role {key} failed: {code} {d}")
|
||||
|
||||
|
||||
def _ensure_oidc_apps(token: str, project_id: str, apps: dict[str, dict]) -> tuple[dict, dict]:
|
||||
def _ensure_oidc_apps(token: str, project_id: str, apps: dict[str, dict],
|
||||
known_secrets: dict[str, str] | None = None) -> tuple[dict, dict]:
|
||||
code, d = _api("POST", f"/management/v1/projects/{project_id}/apps/_search", token, {})
|
||||
existing = {a.get("name"): a for a in (d.get("result") or [])}
|
||||
ids: dict[str, str] = {}
|
||||
secrets: dict[str, str] = {}
|
||||
known_secrets = known_secrets or {}
|
||||
for name, spec in apps.items():
|
||||
row = existing.get(name)
|
||||
created = False
|
||||
if row:
|
||||
ids[name] = (row.get("oidcConfig") or {}).get("clientId", "")
|
||||
app_id = row.get("id")
|
||||
@@ -365,8 +387,16 @@ def _ensure_oidc_apps(token: str, project_id: str, apps: dict[str, dict]) -> tup
|
||||
die(f"create app {name} failed: {code} {d}")
|
||||
app_id = d["appId"]
|
||||
ids[name] = d.get("clientId", "")
|
||||
created = True
|
||||
if spec.get("auth_method_id") == "OIDC_AUTH_METHOD_TYPE_NONE":
|
||||
continue
|
||||
if not created:
|
||||
# ZITADEL keeps one client secret per app, and the API cannot read it
|
||||
# back. Generating a new one here would replace the deployed copy and
|
||||
# break that app's login, so reuse the value already on disk.
|
||||
if known_secrets.get(name):
|
||||
secrets[name] = known_secrets[name]
|
||||
continue
|
||||
code, d = _api("POST",
|
||||
f"/management/v1/projects/{project_id}/apps/{app_id}"
|
||||
"/oidc_config/_generate_client_secret", token, {})
|
||||
@@ -468,7 +498,13 @@ def phase_apply(_args: argparse.Namespace) -> int:
|
||||
print(f"project : {PROJECT_NAME} ({project_id})")
|
||||
_ensure_roles(token, project_id, ROLES)
|
||||
print(f"roles : {len(ROLES)} ensured")
|
||||
ids, secrets = _ensure_oidc_apps(token, project_id, OIDC_APPS)
|
||||
known: dict[str, str] = {}
|
||||
try:
|
||||
known = (json.loads(STATE_FILE.read_bytes()).get("client_secrets", {})
|
||||
.get("value", {}) or {})
|
||||
except (OSError, ValueError):
|
||||
known = {}
|
||||
ids, secrets = _ensure_oidc_apps(token, project_id, OIDC_APPS, known)
|
||||
print(f"oidc apps : {len(ids)} ensured, {len(secrets)} secrets issued")
|
||||
|
||||
state = {
|
||||
|
||||
@@ -33,14 +33,6 @@ machine:
|
||||
- ip: ${forgejo_registry_ip}
|
||||
aliases:
|
||||
- ${forgejo_hostname}
|
||||
# kube-apiserver's static pod runs on the host network, so it resolves
|
||||
# authentik.riotpiao.com via THIS node's DNS, not cluster CoreDNS - the
|
||||
# OIDC issuer discovery call would otherwise hit the public
|
||||
# Cloudflare-fronted IP instead of nginx directly. Same reasoning as
|
||||
# the forgejo entry above.
|
||||
- ip: 192.168.1.160
|
||||
aliases:
|
||||
- authentik.riotpiao.com
|
||||
kubelet:
|
||||
image: ${kubelet_image}
|
||||
defaultRuntimeSeccompProfileEnabled: true
|
||||
@@ -128,19 +120,10 @@ cluster:
|
||||
- ${san}
|
||||
%{ endfor ~}
|
||||
image: ${kube_apiserver_img}
|
||||
# Foundation for the Authentik group -> k8s RBAC pattern in
|
||||
# k8s/infra/rbac/ - additive only, existing client-cert auth
|
||||
# (system:masters, this session's own admin@homelab-cluster kubeconfig)
|
||||
# keeps working unchanged; a bad OIDC config just means OIDC logins fail,
|
||||
# not a lockout. Prefixes avoid collision with built-in system:* users
|
||||
# and groups.
|
||||
extraArgs:
|
||||
oidc-issuer-url: https://authentik.riotpiao.com/application/o/kubernetes/
|
||||
oidc-client-id: kubernetes
|
||||
oidc-username-claim: email
|
||||
oidc-groups-claim: groups
|
||||
oidc-username-prefix: "oidc:"
|
||||
oidc-groups-prefix: "oidc:"
|
||||
# No OIDC block. The cluster authenticates with client certificates. An
|
||||
# earlier Authentik-based kube-apiserver OIDC config was removed from the
|
||||
# live nodes, and leaving it here would re-add an issuer that no longer
|
||||
# resolves on any future machine-config render.
|
||||
admissionControl:
|
||||
- name: PodSecurity
|
||||
configuration:
|
||||
|
||||
Reference in New Issue
Block a user