mirror of
https://github.com/shadoll/helm-charts.git
synced 2026-08-28 19:43:12 +00:00
feat(traefik-forward-auth): serve several domains and portals from one instance
A session cookie scoped to one registrable domain is never sent to a host under
another, so an app on a second domain could not be protected by this instance at
all. It failed with a 500 naming the cause: "return URL host does not match any
configured cookie domain".
`domains` is now a list of {domain, authHost}, matching the upstream
`server.domains` schema. The chart previously emitted `server.hostname` and
`cookies.domain`, which are not in 4.14.1's documented options — a deprecated
form that still worked. The single-domain values remain as shorthand and fold
into one entry, so an existing release renders the same protection it did before.
`portals` is a list too. A portal is one OAuth2 client, chosen by the middleware
address, so two portals means two Pocket ID applications — which is how access
can be separated per domain rather than merely shared across them. Each portal
gets its own Middleware and its own mounted secret, under
/var/run/secrets/traefik-forward-auth/<portal>/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ apiVersion: v2
|
|||||||
name: traefik-forward-auth
|
name: traefik-forward-auth
|
||||||
description: Forward auth for Traefik (ItalyPaleAle/traefik-forward-auth) with Pocket ID provider
|
description: Forward auth for Traefik (ItalyPaleAle/traefik-forward-auth) with Pocket ID provider
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.6
|
version: 0.2.0
|
||||||
appVersion: "4.14.1"
|
appVersion: "4.14.1"
|
||||||
annotations:
|
annotations:
|
||||||
version-source: github-release:ItalyPaleAle/traefik-forward-auth
|
version-source: github-release:ItalyPaleAle/traefik-forward-auth
|
||||||
|
|||||||
@@ -11,3 +11,59 @@ app.kubernetes.io/name: {{ .Chart.Name }}
|
|||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
The portals, normalised to one shape.
|
||||||
|
|
||||||
|
A portal is a set of identity providers with its own OAuth2 client, and the
|
||||||
|
middleware address is what selects it — so two portals means two clients, and
|
||||||
|
therefore two Pocket ID applications that can be granted to different people.
|
||||||
|
|
||||||
|
`portals` is the general form. The older single-portal values (`portal.name`,
|
||||||
|
`pocketID`, `existingSecret`, `middleware.name`) still work and are folded into
|
||||||
|
the same shape here, so an existing release keeps rendering exactly as before.
|
||||||
|
*/}}
|
||||||
|
{{- define "tfa.portals" -}}
|
||||||
|
{{- if .Values.portals -}}
|
||||||
|
{{- range .Values.portals }}
|
||||||
|
- name: {{ .name | quote }}
|
||||||
|
endpoint: {{ .pocketID.endpoint | quote }}
|
||||||
|
clientID: {{ .pocketID.clientID | quote }}
|
||||||
|
existingSecret: {{ required "each portal needs an existingSecret" .existingSecret | quote }}
|
||||||
|
secretKey: {{ .secretKey | default "client-secret" | quote }}
|
||||||
|
middlewareName: {{ .middlewareName | default (printf "%s-%s" $.Release.Name .name) | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else -}}
|
||||||
|
- name: {{ .Values.portal.name | quote }}
|
||||||
|
endpoint: {{ .Values.pocketID.endpoint | quote }}
|
||||||
|
clientID: {{ .Values.pocketID.clientID | quote }}
|
||||||
|
existingSecret: {{ required "existingSecret is required" .Values.existingSecret | quote }}
|
||||||
|
secretKey: "client-secret"
|
||||||
|
middlewareName: {{ include "tfa.middlewareName" . | quote }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
The domains served, normalised.
|
||||||
|
|
||||||
|
`domain` is the cookie's scope and `authHost` is where this service is reachable
|
||||||
|
for it. A browser will not send a cookie scoped to one registrable domain to a
|
||||||
|
host under another, so an app on a second domain needs its own entry here — not
|
||||||
|
merely a second client.
|
||||||
|
*/}}
|
||||||
|
{{- define "tfa.domains" -}}
|
||||||
|
{{- if .Values.domains -}}
|
||||||
|
{{- range .Values.domains }}
|
||||||
|
- domain: {{ .domain | quote }}
|
||||||
|
authHost: {{ .authHost | default .domain | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else -}}
|
||||||
|
- domain: {{ required "cookieDomain is required when `domains` is not set" .Values.cookieDomain | quote }}
|
||||||
|
authHost: {{ required "hostname is required when `domains` is not set" .Values.hostname | quote }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* Where a portal's client secret is mounted. */}}
|
||||||
|
{{- define "tfa.secretDir" -}}
|
||||||
|
/var/run/secrets/traefik-forward-auth
|
||||||
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
{{- $portals := include "tfa.portals" . | fromYamlArray -}}
|
||||||
|
{{- $domains := include "tfa.domains" . | fromYamlArray -}}
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
@@ -7,21 +9,26 @@ metadata:
|
|||||||
data:
|
data:
|
||||||
config.yaml: |
|
config.yaml: |
|
||||||
server:
|
server:
|
||||||
hostname: {{ .Values.hostname | quote }}
|
# One entry per domain served. `domain` scopes the session cookie and
|
||||||
|
# `authHost` is where this service is reachable for that domain.
|
||||||
|
domains:
|
||||||
|
{{- range $domains }}
|
||||||
|
- domain: {{ .domain | quote }}
|
||||||
|
authHost: {{ .authHost | quote }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
tokens:
|
tokens:
|
||||||
sessionLifetime: {{ .Values.tokens.sessionLifetime }}
|
sessionLifetime: {{ .Values.tokens.sessionLifetime }}
|
||||||
|
|
||||||
cookies:
|
|
||||||
domain: {{ .Values.cookieDomain | quote }}
|
|
||||||
|
|
||||||
portals:
|
portals:
|
||||||
- name: {{ .Values.portal.name | quote }}
|
{{- range $portals }}
|
||||||
|
- name: {{ .name | quote }}
|
||||||
providers:
|
providers:
|
||||||
- pocketID:
|
- pocketID:
|
||||||
endpoint: {{ .Values.pocketID.endpoint | quote }}
|
endpoint: {{ .endpoint | quote }}
|
||||||
clientID: {{ .Values.pocketID.clientID | quote }}
|
clientID: {{ .clientID | quote }}
|
||||||
clientSecretFile: "/var/run/secrets/traefik-forward-auth/client-secret"
|
clientSecretFile: "{{ include "tfa.secretDir" $ }}/{{ .name }}/client-secret"
|
||||||
|
{{- end }}
|
||||||
{{- with .Values.extraConfig }}
|
{{- with .Values.extraConfig }}
|
||||||
{{ . | nindent 4 }}
|
{{ . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- $portals := include "tfa.portals" . | fromYamlArray -}}
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -43,18 +44,24 @@ spec:
|
|||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/traefik-forward-auth
|
mountPath: /etc/traefik-forward-auth
|
||||||
readOnly: true
|
readOnly: true
|
||||||
- name: client-secret
|
{{- range $portals }}
|
||||||
mountPath: /var/run/secrets/traefik-forward-auth
|
# One directory per portal: each has its own OAuth2 client, so each
|
||||||
|
# has its own secret.
|
||||||
|
- name: client-secret-{{ .name }}
|
||||||
|
mountPath: "{{ include "tfa.secretDir" $ }}/{{ .name }}"
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
{{- end }}
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
volumes:
|
volumes:
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: {{ .Release.Name }}-config
|
name: {{ .Release.Name }}-config
|
||||||
- name: client-secret
|
{{- range $portals }}
|
||||||
|
- name: client-secret-{{ .name }}
|
||||||
secret:
|
secret:
|
||||||
secretName: {{ required "existingSecret is required" .Values.existingSecret }}
|
secretName: {{ .existingSecret }}
|
||||||
items:
|
items:
|
||||||
- key: client-secret
|
- key: {{ .secretKey }}
|
||||||
path: client-secret
|
path: client-secret
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
{{- if .Values.middleware.enabled }}
|
{{- if .Values.middleware.enabled }}
|
||||||
|
{{- $portals := include "tfa.portals" . | fromYamlArray -}}
|
||||||
|
{{- range $portals }}
|
||||||
|
---
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: Middleware
|
kind: Middleware
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "tfa.middlewareName" . }}
|
# Referenced by apps as <namespace>-<name>@kubernetescrd. Renaming one detaches
|
||||||
|
# protection from every ingress that names it.
|
||||||
|
name: {{ .middlewareName }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "tfa.labels" . | nindent 4 }}
|
{{- include "tfa.labels" $ | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
forwardAuth:
|
forwardAuth:
|
||||||
address: "http://{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local/portals/{{ .Values.portal.name }}"
|
# The path selects the portal, and therefore which OAuth2 client — which is
|
||||||
trustForwardHeader: {{ .Values.middleware.trustForwardHeader }}
|
# how access is separated between domains.
|
||||||
{{- with .Values.middleware.authResponseHeaders }}
|
address: "http://{{ $.Release.Name }}.{{ $.Release.Namespace }}.svc.cluster.local/portals/{{ .name }}"
|
||||||
|
trustForwardHeader: {{ $.Values.middleware.trustForwardHeader }}
|
||||||
|
{{- with $.Values.middleware.authResponseHeaders }}
|
||||||
authResponseHeaders:
|
authResponseHeaders:
|
||||||
{{- toYaml . | nindent 6 }}
|
{{- toYaml . | nindent 6 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -4,18 +4,45 @@ image:
|
|||||||
# Overrides the image tag; defaults to Chart.AppVersion
|
# Overrides the image tag; defaults to Chart.AppVersion
|
||||||
tag: ""
|
tag: ""
|
||||||
|
|
||||||
# TFA server hostname (used for OIDC redirects). E.g. "auth.example.com"
|
# Domains served by this instance. One entry per registrable domain.
|
||||||
hostname: ""
|
#
|
||||||
|
# `domain` scopes the session cookie; `authHost` is where this service is
|
||||||
|
# reachable for that domain, and must be it or a sub-domain of it.
|
||||||
|
#
|
||||||
|
# A browser will not send a cookie scoped to one registrable domain to a host
|
||||||
|
# under another, so an app on a second domain needs its own entry here. A second
|
||||||
|
# OAuth2 client alone does not solve it.
|
||||||
|
domains: []
|
||||||
|
# - domain: example.com
|
||||||
|
# authHost: auth.example.com
|
||||||
|
# - domain: example.org
|
||||||
|
# authHost: auth.example.org
|
||||||
|
|
||||||
# Cookie domain — scope at which the session cookie is valid.
|
# Single-domain shorthand, used when `domains` is empty. Equivalent to one
|
||||||
# Must be a parent of `hostname` (e.g. "example.com" for "auth.example.com").
|
# `domains` entry of {domain: cookieDomain, authHost: hostname}.
|
||||||
|
hostname: ""
|
||||||
cookieDomain: ""
|
cookieDomain: ""
|
||||||
|
|
||||||
tokens:
|
tokens:
|
||||||
sessionLifetime: 24h
|
sessionLifetime: 24h
|
||||||
|
|
||||||
# Pocket ID OIDC configuration. `clientID` is a public identifier;
|
# Portals. Each is one OAuth2 client, selected by the middleware address
|
||||||
# `clientSecret` is read from `existingSecret` (key: `client-secret`).
|
# (/portals/<name>) — which is how access is separated: grant one Pocket ID
|
||||||
|
# application to one group of people and the other to another, and each portal
|
||||||
|
# admits only its own.
|
||||||
|
#
|
||||||
|
# Each portal mounts its secret at
|
||||||
|
# /var/run/secrets/traefik-forward-auth/<name>/client-secret.
|
||||||
|
portals: []
|
||||||
|
# - name: main
|
||||||
|
# middlewareName: pocket-id-auth # default: <release>-<name>
|
||||||
|
# existingSecret: tfa-main-secret # must define key `client-secret`
|
||||||
|
# secretKey: client-secret # optional, if the key differs
|
||||||
|
# pocketID:
|
||||||
|
# endpoint: https://id.example.com
|
||||||
|
# clientID: "..."
|
||||||
|
|
||||||
|
# Single-portal shorthand, used when `portals` is empty.
|
||||||
pocketID:
|
pocketID:
|
||||||
endpoint: ""
|
endpoint: ""
|
||||||
clientID: ""
|
clientID: ""
|
||||||
|
|||||||
Reference in New Issue
Block a user