From d0faf8e0060100030e5d033e877cf81a06275bb2 Mon Sep 17 00:00:00 2001 From: bulbashenko Date: Sun, 11 Oct 2026 21:53:14 +0200 Subject: [PATCH] Add a Kubernetes base next to the compose stack The same ory/ configs become ConfigMaps; Kratos, Hydra and Keto run their migrations as init containers; Oathkeeper, the UI and the hydra-clients Job follow the compose services one to one, with the same service names so internal URLs stay valid. Traefik IngressRoutes carry over the routes and rate limits from the compose labels: auth.bulbashenko.com on the web entrypoint (Cloudflare Tunnel), users.int on websecure behind the mesh and Oathkeeper. A new workflow builds ghcr.io/bulbashenko-com/auth-ui: on main, so the deploying repo can pin configs and image to one commit. The compose deployment on Coolify is untouched. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/image.yml | 36 +++++ README.md | 6 + deploy/k8s/ory.yaml | 277 ++++++++++++++++++++++++++++++++++++ deploy/k8s/routes.yaml | 96 +++++++++++++ deploy/k8s/ui.yaml | 93 ++++++++++++ kustomization.yaml | 46 ++++++ ui/Dockerfile | 2 +- 7 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/image.yml create mode 100644 deploy/k8s/ory.yaml create mode 100644 deploy/k8s/routes.yaml create mode 100644 deploy/k8s/ui.yaml create mode 100644 kustomization.yaml diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml new file mode 100644 index 0000000..79fe09d --- /dev/null +++ b/.github/workflows/image.yml @@ -0,0 +1,36 @@ +# Builds the UI image (also used for the hydra-clients Job) for the Kubernetes deployment. +# Tag = full commit SHA: bulbashenko-com/infra pins the same SHA for the configs and the image. +name: image + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + ui: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v6 + with: + context: ui + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/auth-ui:${{ github.sha }} + ghcr.io/${{ github.repository_owner }}/auth-ui:main + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index c34e3d4..bde4197 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ Groups decide access. Each OIDC client names one group in `metadata.required_gro | `mail-users` | Mail clients and the Stalwart web UI | | `vpn-users` | Joining the VPN mesh (Headscale) | +## Deploy on Kubernetes + +`kustomization.yaml` at the root is a Kustomize base built from the same `ory/` files that `compose.yml` mounts: Deployments for Kratos, Hydra and Keto (each runs its migration as an init container), Oathkeeper and the UI, the `hydra-clients` Job, and Traefik routes (`auth-public` on the `web` entrypoint, `auth-admin` for `users.int` behind the mesh and Oathkeeper). Service names match the compose stack, so the configs' internal URLs work unchanged. + +The deploying side provides the namespace, the image tag and a Secret `auth` with `KRATOS_DSN`, `HYDRA_DSN`, `KETO_DSN`, `KRATOS_SECRETS_{COOKIE,DEFAULT,CIPHER,PAGINATION}`, `HYDRA_SECRETS_{SYSTEM,COOKIE,PAGINATION}`, `SMTP_CONNECTION_URI`, `SMTP_SENDER` and `OIDC_{SITE,COOLIFY,HEADSCALE}_CLIENT_SECRET`. bulbashenko.com deploys it from [bulbashenko-com/infra](https://github.com/bulbashenko-com/infra), pinned to one commit of this repo; `.github/workflows/image.yml` builds `ghcr.io/bulbashenko-com/auth-ui:` for that commit. + ## Deploy on Coolify 1. **Databases.** In the shared Postgres, create three databases, each with its own role: diff --git a/deploy/k8s/ory.yaml b/deploy/k8s/ory.yaml new file mode 100644 index 0000000..efd8c3e --- /dev/null +++ b/deploy/k8s/ory.yaml @@ -0,0 +1,277 @@ +# Ory services. Configs come from ConfigMaps generated from ory/ (see the root kustomization.yaml); +# DSNs and secrets from the Secret `auth`, which the deploying repo (bulbashenko-com/infra) provides. +# Service names match the compose stack, so the configs' internal URLs (kratos:4433, hydra:4445, keto:4466) still work. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kratos +spec: + replicas: 1 + selector: + matchLabels: { app: kratos } + template: + metadata: + labels: { app: kratos } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + initContainers: + - name: migrate + image: oryd/kratos:v26.2.0 + args: ["migrate", "sql", "-e", "--yes", "-c", "/etc/config/kratos/kratos.yml"] + env: + - { name: SQA_OPT_OUT, value: "true" } + - { name: DSN, valueFrom: { secretKeyRef: { name: auth, key: KRATOS_DSN } } } + volumeMounts: + - { name: config, mountPath: /etc/config/kratos } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + containers: + - name: kratos + image: oryd/kratos:v26.2.0 + args: ["serve", "-c", "/etc/config/kratos/kratos.yml", "--watch-courier"] + env: + - { name: SQA_OPT_OUT, value: "true" } + - { name: LOG_LEVEL, value: info } + - { name: DSN, valueFrom: { secretKeyRef: { name: auth, key: KRATOS_DSN } } } + - { name: SECRETS_COOKIE, valueFrom: { secretKeyRef: { name: auth, key: KRATOS_SECRETS_COOKIE } } } + - { name: SECRETS_DEFAULT, valueFrom: { secretKeyRef: { name: auth, key: KRATOS_SECRETS_DEFAULT } } } + - { name: SECRETS_CIPHER, valueFrom: { secretKeyRef: { name: auth, key: KRATOS_SECRETS_CIPHER } } } + - { name: SECRETS_PAGINATION, valueFrom: { secretKeyRef: { name: auth, key: KRATOS_SECRETS_PAGINATION } } } + - { name: COURIER_SMTP_CONNECTION_URI, valueFrom: { secretKeyRef: { name: auth, key: SMTP_CONNECTION_URI } } } + - { name: COURIER_SMTP_FROM_ADDRESS, valueFrom: { secretKeyRef: { name: auth, key: SMTP_SENDER } } } + ports: + - { name: public, containerPort: 4433 } + - { name: admin, containerPort: 4434 } + readinessProbe: + httpGet: { path: /admin/health/ready, port: admin } + livenessProbe: + httpGet: { path: /admin/health/alive, port: admin } + resources: + requests: { cpu: 10m, memory: 48Mi } + limits: { memory: 256Mi } + volumeMounts: + - { name: config, mountPath: /etc/config/kratos } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + volumes: + - name: config + projected: + sources: + - configMap: { name: kratos-config } + - configMap: + name: kratos-courier-templates + items: + - { key: recovery_code.invalid.email.body.gotmpl, path: courier-templates/recovery_code/invalid/email.body.gotmpl } + - { key: recovery_code.invalid.email.body.plaintext.gotmpl, path: courier-templates/recovery_code/invalid/email.body.plaintext.gotmpl } + - { key: recovery_code.invalid.email.subject.gotmpl, path: courier-templates/recovery_code/invalid/email.subject.gotmpl } + - { key: recovery_code.valid.email.body.gotmpl, path: courier-templates/recovery_code/valid/email.body.gotmpl } + - { key: recovery_code.valid.email.body.plaintext.gotmpl, path: courier-templates/recovery_code/valid/email.body.plaintext.gotmpl } + - { key: recovery_code.valid.email.subject.gotmpl, path: courier-templates/recovery_code/valid/email.subject.gotmpl } + - { key: verification_code.invalid.email.body.gotmpl, path: courier-templates/verification_code/invalid/email.body.gotmpl } + - { key: verification_code.invalid.email.body.plaintext.gotmpl, path: courier-templates/verification_code/invalid/email.body.plaintext.gotmpl } + - { key: verification_code.invalid.email.subject.gotmpl, path: courier-templates/verification_code/invalid/email.subject.gotmpl } + - { key: verification_code.valid.email.body.gotmpl, path: courier-templates/verification_code/valid/email.body.gotmpl } + - { key: verification_code.valid.email.body.plaintext.gotmpl, path: courier-templates/verification_code/valid/email.body.plaintext.gotmpl } + - { key: verification_code.valid.email.subject.gotmpl, path: courier-templates/verification_code/valid/email.subject.gotmpl } +--- +apiVersion: v1 +kind: Service +metadata: + name: kratos +spec: + selector: { app: kratos } + ports: + - { name: public, port: 4433, targetPort: public } + - { name: admin, port: 4434, targetPort: admin } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hydra +spec: + replicas: 1 + selector: + matchLabels: { app: hydra } + template: + metadata: + labels: { app: hydra } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + initContainers: + - name: migrate + image: oryd/hydra:v26.2.0 + args: ["migrate", "sql", "up", "-e", "--yes", "-c", "/etc/config/hydra/hydra.yml"] + env: + - { name: SQA_OPT_OUT, value: "true" } + - { name: DSN, valueFrom: { secretKeyRef: { name: auth, key: HYDRA_DSN } } } + volumeMounts: + - { name: config, mountPath: /etc/config/hydra } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + containers: + - name: hydra + image: oryd/hydra:v26.2.0 + args: ["serve", "all", "-c", "/etc/config/hydra/hydra.yml"] + env: + - { name: SQA_OPT_OUT, value: "true" } + - { name: LOG_LEVEL, value: info } + - { name: DSN, valueFrom: { secretKeyRef: { name: auth, key: HYDRA_DSN } } } + - { name: SECRETS_SYSTEM, valueFrom: { secretKeyRef: { name: auth, key: HYDRA_SECRETS_SYSTEM } } } + - { name: SECRETS_COOKIE, valueFrom: { secretKeyRef: { name: auth, key: HYDRA_SECRETS_COOKIE } } } + - { name: SECRETS_PAGINATION, valueFrom: { secretKeyRef: { name: auth, key: HYDRA_SECRETS_PAGINATION } } } + ports: + - { name: public, containerPort: 4444 } + - { name: admin, containerPort: 4445 } + readinessProbe: + httpGet: { path: /health/ready, port: admin } + livenessProbe: + httpGet: { path: /health/alive, port: admin } + resources: + requests: { cpu: 10m, memory: 48Mi } + limits: { memory: 256Mi } + volumeMounts: + - { name: config, mountPath: /etc/config/hydra } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + volumes: + - name: config + configMap: { name: hydra-config } +--- +apiVersion: v1 +kind: Service +metadata: + name: hydra +spec: + selector: { app: hydra } + ports: + - { name: public, port: 4444, targetPort: public } + - { name: admin, port: 4445, targetPort: admin } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keto +spec: + replicas: 1 + selector: + matchLabels: { app: keto } + template: + metadata: + labels: { app: keto } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + initContainers: + - name: migrate + image: oryd/keto:v26.2.0 + args: ["migrate", "up", "--yes", "-c", "/etc/config/keto/keto.yml"] + env: + - { name: SQA_OPT_OUT, value: "true" } + - { name: DSN, valueFrom: { secretKeyRef: { name: auth, key: KETO_DSN } } } + volumeMounts: + - { name: config, mountPath: /etc/config/keto } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + containers: + - name: keto + image: oryd/keto:v26.2.0 + args: ["serve", "-c", "/etc/config/keto/keto.yml"] + env: + - { name: SQA_OPT_OUT, value: "true" } + - { name: LOG_LEVEL, value: info } + - { name: DSN, valueFrom: { secretKeyRef: { name: auth, key: KETO_DSN } } } + ports: + - { name: read, containerPort: 4466 } + - { name: write, containerPort: 4467 } + readinessProbe: + httpGet: { path: /health/ready, port: read } + livenessProbe: + httpGet: { path: /health/alive, port: read } + resources: + requests: { cpu: 10m, memory: 32Mi } + limits: { memory: 192Mi } + volumeMounts: + - { name: config, mountPath: /etc/config/keto } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + volumes: + - name: config + configMap: { name: keto-config } +--- +apiVersion: v1 +kind: Service +metadata: + name: keto +spec: + selector: { app: keto } + ports: + - { name: read, port: 4466, targetPort: read } + - { name: write, port: 4467, targetPort: write } +--- +# Traefik forward-auth for admin UIs: GET http://oathkeeper..svc:4456/decisions. No database. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oathkeeper +spec: + replicas: 1 + selector: + matchLabels: { app: oathkeeper } + template: + metadata: + labels: { app: oathkeeper } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + containers: + - name: oathkeeper + image: oryd/oathkeeper:v26.2.0 + args: ["serve", "-c", "/etc/config/oathkeeper/oathkeeper.yml"] + env: + - { name: SQA_OPT_OUT, value: "true" } + - { name: LOG_LEVEL, value: info } + ports: + - { name: api, containerPort: 4456 } + readinessProbe: + httpGet: { path: /health/ready, port: api } + livenessProbe: + httpGet: { path: /health/alive, port: api } + resources: + requests: { cpu: 10m, memory: 24Mi } + limits: { memory: 128Mi } + volumeMounts: + - { name: config, mountPath: /etc/config/oathkeeper } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + volumes: + - name: config + configMap: { name: oathkeeper-config } +--- +apiVersion: v1 +kind: Service +metadata: + name: oathkeeper +spec: + selector: { app: oathkeeper } + ports: + - { name: api, port: 4456, targetPort: api } diff --git a/deploy/k8s/routes.yaml b/deploy/k8s/routes.yaml new file mode 100644 index 0000000..2591aaf --- /dev/null +++ b/deploy/k8s/routes.yaml @@ -0,0 +1,96 @@ +# Traefik routes. Public traffic reaches Traefik's `web` entrypoint through Cloudflare Tunnel; the admin page +# (users.int) is served on `websecure` from the mesh only, behind Oathkeeper. Hosts are the production ones; +# other environments patch them. +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: auth-public +spec: + entryPoints: [web] + routes: + # Form submissions that carry secrets: rate-limited (Kratos has no lockout of its own). + - match: Host(`auth.bulbashenko.com`) && Method(`POST`) && PathRegexp(`^/self-service/(login|recovery|verification|settings)`) + priority: 200 + middlewares: [{ name: limit-ip }, { name: limit-all }, { name: headers }] + services: [{ name: kratos, port: 4433 }] + - match: Host(`auth.bulbashenko.com`) && Path(`/oauth2/token`) + priority: 200 + middlewares: [{ name: limit-ip }, { name: limit-all }, { name: headers }] + services: [{ name: hydra, port: 4444 }] + - match: Host(`auth.bulbashenko.com`) && (PathPrefix(`/self-service/`) || PathPrefix(`/sessions/`) || PathPrefix(`/.well-known/ory/`)) + priority: 100 + middlewares: [{ name: headers }] + services: [{ name: kratos, port: 4433 }] + - match: Host(`auth.bulbashenko.com`) && (PathPrefix(`/oauth2/`) || Path(`/userinfo`) || Path(`/.well-known/openid-configuration`) || Path(`/.well-known/jwks.json`) || Path(`/.well-known/webfinger`)) + priority: 100 + middlewares: [{ name: headers }] + services: [{ name: hydra, port: 4444 }] + - match: Host(`auth.bulbashenko.com`) + priority: 10 + middlewares: [{ name: headers }] + services: [{ name: sso-ui, port: 3000 }] +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: auth-admin +spec: + entryPoints: [websecure] + routes: + - match: Host(`users.int.bulbashenko.com`) + middlewares: + - { name: mesh-only, namespace: kube-system } + - { name: forward-auth } + - { name: headers } + services: [{ name: sso-ui, port: 3000 }] + tls: {} +--- +# Per client (Cloudflare's CF-Connecting-IP): 10 per minute, bursts of 5. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: limit-ip +spec: + rateLimit: + average: 10 + period: 1m + burst: 5 + sourceCriterion: + requestHeaderName: CF-Connecting-IP +--- +# Everyone together: 120 per minute, so rotating addresses do not help either. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: limit-all +spec: + rateLimit: + average: 120 + period: 1m + burst: 20 + sourceCriterion: + requestHost: true +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: headers +spec: + headers: + contentTypeNosniff: true + frameDeny: true + referrerPolicy: strict-origin-when-cross-origin + stsSeconds: 31536000 + stsIncludeSubdomains: true +--- +# For every mesh admin UI: the address uses this namespace's Oathkeeper. Other namespaces reference it as +# -forward-auth@kubernetescrd or by name+namespace in IngressRoutes. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: forward-auth +spec: + forwardAuth: + address: http://oathkeeper.auth.svc.cluster.local:4456/decisions + trustForwardHeader: true + authResponseHeaders: [Remote-User, Remote-Email, Remote-Name] diff --git a/deploy/k8s/ui.yaml b/deploy/k8s/ui.yaml new file mode 100644 index 0000000..9f67102 --- /dev/null +++ b/deploy/k8s/ui.yaml @@ -0,0 +1,93 @@ +# Login, settings, consent and the admin page (users.int). Image built by .github/workflows/image.yml. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sso-ui +spec: + replicas: 1 + selector: + matchLabels: { app: sso-ui } + template: + metadata: + labels: { app: sso-ui } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + containers: + - name: sso-ui + image: ghcr.io/bulbashenko-com/auth-ui:latest + env: + - { name: PUBLIC_URL, value: "https://auth.bulbashenko.com" } + - { name: ADMIN_HOST, value: "users.int.bulbashenko.com" } + - { name: KRATOS_PUBLIC_URL, value: "http://kratos:4433" } + - { name: KRATOS_ADMIN_URL, value: "http://kratos:4434" } + - { name: HYDRA_ADMIN_URL, value: "http://hydra:4445" } + - { name: KETO_READ_URL, value: "http://keto:4466" } + - { name: KETO_WRITE_URL, value: "http://keto:4467" } + ports: + - { name: http, containerPort: 3000 } + readinessProbe: + httpGet: { path: /api/health, port: http } + livenessProbe: + httpGet: { path: /api/health, port: http } + periodSeconds: 20 + resources: + requests: { cpu: 20m, memory: 96Mi } + limits: { memory: 384Mi } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + volumeMounts: + - { name: cache, mountPath: /app/.next/cache } + - { name: tmp, mountPath: /tmp } + volumes: + - { name: cache, emptyDir: {} } + - { name: tmp, emptyDir: {} } +--- +apiVersion: v1 +kind: Service +metadata: + name: sso-ui +spec: + selector: { app: sso-ui } + ports: + - { name: http, port: 3000, targetPort: http } +--- +# Creates or updates the OIDC clients from ory/hydra/clients/*.json. Re-runs when the clients change +# (ConfigMap hash in the pod spec) thanks to Flux's force annotation. +apiVersion: batch/v1 +kind: Job +metadata: + name: hydra-clients + annotations: + kustomize.toolkit.fluxcd.io/force: enabled +spec: + backoffLimit: 10 + template: + spec: + restartPolicy: OnFailure + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + containers: + - name: sync + image: ghcr.io/bulbashenko-com/auth-ui:latest + command: ["node", "scripts/sync-clients.mjs", "/app/clients"] + env: + - { name: HYDRA_ADMIN_URL, value: "http://hydra:4445" } + - { name: OIDC_SITE_CLIENT_SECRET, valueFrom: { secretKeyRef: { name: auth, key: OIDC_SITE_CLIENT_SECRET } } } + - { name: OIDC_COOLIFY_CLIENT_SECRET, valueFrom: { secretKeyRef: { name: auth, key: OIDC_COOLIFY_CLIENT_SECRET } } } + - { name: OIDC_HEADSCALE_CLIENT_SECRET, valueFrom: { secretKeyRef: { name: auth, key: OIDC_HEADSCALE_CLIENT_SECRET } } } + resources: + requests: { cpu: 10m, memory: 48Mi } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + volumeMounts: + - { name: clients, mountPath: /app/clients } + volumes: + - name: clients + configMap: { name: hydra-clients } diff --git a/kustomization.yaml b/kustomization.yaml new file mode 100644 index 0000000..cb215b1 --- /dev/null +++ b/kustomization.yaml @@ -0,0 +1,46 @@ +# Kubernetes base for the Ory stack, consumed by bulbashenko-com/infra (Flux GitRepository pinned to a commit). +# Configs are the same files compose.yml mounts. The deploying side supplies the Secret `auth`, the namespace +# and the auth-ui image tag (same commit as this repo). +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deploy/k8s/ory.yaml + - deploy/k8s/ui.yaml + - deploy/k8s/routes.yaml +configMapGenerator: + - name: kratos-config + files: + - ory/kratos/kratos.yml + - ory/kratos/identity.schema.json + - name: kratos-courier-templates + files: + - recovery_code.invalid.email.body.gotmpl=ory/kratos/courier-templates/recovery_code/invalid/email.body.gotmpl + - recovery_code.invalid.email.body.plaintext.gotmpl=ory/kratos/courier-templates/recovery_code/invalid/email.body.plaintext.gotmpl + - recovery_code.invalid.email.subject.gotmpl=ory/kratos/courier-templates/recovery_code/invalid/email.subject.gotmpl + - recovery_code.valid.email.body.gotmpl=ory/kratos/courier-templates/recovery_code/valid/email.body.gotmpl + - recovery_code.valid.email.body.plaintext.gotmpl=ory/kratos/courier-templates/recovery_code/valid/email.body.plaintext.gotmpl + - recovery_code.valid.email.subject.gotmpl=ory/kratos/courier-templates/recovery_code/valid/email.subject.gotmpl + - verification_code.invalid.email.body.gotmpl=ory/kratos/courier-templates/verification_code/invalid/email.body.gotmpl + - verification_code.invalid.email.body.plaintext.gotmpl=ory/kratos/courier-templates/verification_code/invalid/email.body.plaintext.gotmpl + - verification_code.invalid.email.subject.gotmpl=ory/kratos/courier-templates/verification_code/invalid/email.subject.gotmpl + - verification_code.valid.email.body.gotmpl=ory/kratos/courier-templates/verification_code/valid/email.body.gotmpl + - verification_code.valid.email.body.plaintext.gotmpl=ory/kratos/courier-templates/verification_code/valid/email.body.plaintext.gotmpl + - verification_code.valid.email.subject.gotmpl=ory/kratos/courier-templates/verification_code/valid/email.subject.gotmpl + - name: hydra-config + files: + - ory/hydra/hydra.yml + - name: hydra-clients + files: + - ory/hydra/clients/bulbashenko-site.json + - ory/hydra/clients/coolify.json + - ory/hydra/clients/headscale.json + - ory/hydra/clients/stalwart-webui.json + - ory/hydra/clients/thunderbird.json + - name: keto-config + files: + - ory/keto/keto.yml + - ory/keto/namespaces.keto.ts + - name: oathkeeper-config + files: + - ory/oathkeeper/oathkeeper.yml + - ory/oathkeeper/rules.yml diff --git a/ui/Dockerfile b/ui/Dockerfile index 21adad4..4fb032d 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -14,7 +14,7 @@ RUN npm run build FROM node:24.21.0-alpine AS runtime LABEL org.opencontainers.image.title="auth.bulbashenko.com UI" \ - org.opencontainers.image.source="https://github.com/bulbashenko/auth" \ + org.opencontainers.image.source="https://github.com/bulbashenko-com/auth" \ org.opencontainers.image.licenses="MIT" WORKDIR /app ENV NODE_ENV=production \