diff --git a/traefik-forward-auth/Chart.yaml b/traefik-forward-auth/Chart.yaml index 8ce2626..f61c0a3 100644 --- a/traefik-forward-auth/Chart.yaml +++ b/traefik-forward-auth/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: traefik-forward-auth description: Forward auth for Traefik (ItalyPaleAle/traefik-forward-auth) with Pocket ID provider type: application -version: 0.1.6 +version: 0.2.0 appVersion: "4.14.1" annotations: version-source: github-release:ItalyPaleAle/traefik-forward-auth diff --git a/traefik-forward-auth/templates/_helpers.tpl b/traefik-forward-auth/templates/_helpers.tpl index 44f416f..bb2cb02 100644 --- a/traefik-forward-auth/templates/_helpers.tpl +++ b/traefik-forward-auth/templates/_helpers.tpl @@ -11,3 +11,59 @@ app.kubernetes.io/name: {{ .Chart.Name }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- 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 -}} diff --git a/traefik-forward-auth/templates/configmap.yaml b/traefik-forward-auth/templates/configmap.yaml index 531fb0c..d82d659 100644 --- a/traefik-forward-auth/templates/configmap.yaml +++ b/traefik-forward-auth/templates/configmap.yaml @@ -1,3 +1,5 @@ +{{- $portals := include "tfa.portals" . | fromYamlArray -}} +{{- $domains := include "tfa.domains" . | fromYamlArray -}} apiVersion: v1 kind: ConfigMap metadata: @@ -7,21 +9,26 @@ metadata: data: config.yaml: | 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: sessionLifetime: {{ .Values.tokens.sessionLifetime }} - cookies: - domain: {{ .Values.cookieDomain | quote }} - portals: - - name: {{ .Values.portal.name | quote }} + {{- range $portals }} + - name: {{ .name | quote }} providers: - pocketID: - endpoint: {{ .Values.pocketID.endpoint | quote }} - clientID: {{ .Values.pocketID.clientID | quote }} - clientSecretFile: "/var/run/secrets/traefik-forward-auth/client-secret" + endpoint: {{ .endpoint | quote }} + clientID: {{ .clientID | quote }} + clientSecretFile: "{{ include "tfa.secretDir" $ }}/{{ .name }}/client-secret" + {{- end }} {{- with .Values.extraConfig }} {{ . | nindent 4 }} {{- end }} diff --git a/traefik-forward-auth/templates/deployment.yaml b/traefik-forward-auth/templates/deployment.yaml index 64f0f6e..d9d1aab 100644 --- a/traefik-forward-auth/templates/deployment.yaml +++ b/traefik-forward-auth/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{- $portals := include "tfa.portals" . | fromYamlArray -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -43,18 +44,24 @@ spec: - name: config mountPath: /etc/traefik-forward-auth readOnly: true - - name: client-secret - mountPath: /var/run/secrets/traefik-forward-auth + {{- range $portals }} + # 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 + {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} volumes: - name: config configMap: name: {{ .Release.Name }}-config - - name: client-secret + {{- range $portals }} + - name: client-secret-{{ .name }} secret: - secretName: {{ required "existingSecret is required" .Values.existingSecret }} + secretName: {{ .existingSecret }} items: - - key: client-secret + - key: {{ .secretKey }} path: client-secret + {{- end }} diff --git a/traefik-forward-auth/templates/middleware.yaml b/traefik-forward-auth/templates/middleware.yaml index cc433be..e2cddbb 100644 --- a/traefik-forward-auth/templates/middleware.yaml +++ b/traefik-forward-auth/templates/middleware.yaml @@ -1,16 +1,24 @@ {{- if .Values.middleware.enabled }} +{{- $portals := include "tfa.portals" . | fromYamlArray -}} +{{- range $portals }} +--- apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: - name: {{ include "tfa.middlewareName" . }} + # Referenced by apps as -@kubernetescrd. Renaming one detaches + # protection from every ingress that names it. + name: {{ .middlewareName }} labels: - {{- include "tfa.labels" . | nindent 4 }} + {{- include "tfa.labels" $ | nindent 4 }} spec: forwardAuth: - address: "http://{{ .Release.Name }}.{{ .Release.Namespace }}.svc.cluster.local/portals/{{ .Values.portal.name }}" - trustForwardHeader: {{ .Values.middleware.trustForwardHeader }} - {{- with .Values.middleware.authResponseHeaders }} + # The path selects the portal, and therefore which OAuth2 client — which is + # how access is separated between domains. + address: "http://{{ $.Release.Name }}.{{ $.Release.Namespace }}.svc.cluster.local/portals/{{ .name }}" + trustForwardHeader: {{ $.Values.middleware.trustForwardHeader }} + {{- with $.Values.middleware.authResponseHeaders }} authResponseHeaders: {{- toYaml . | nindent 6 }} {{- end }} {{- end }} +{{- end }} diff --git a/traefik-forward-auth/values.yaml b/traefik-forward-auth/values.yaml index 05773ca..3509389 100644 --- a/traefik-forward-auth/values.yaml +++ b/traefik-forward-auth/values.yaml @@ -4,18 +4,45 @@ image: # Overrides the image tag; defaults to Chart.AppVersion tag: "" -# TFA server hostname (used for OIDC redirects). E.g. "auth.example.com" -hostname: "" +# Domains served by this instance. One entry per registrable domain. +# +# `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. -# Must be a parent of `hostname` (e.g. "example.com" for "auth.example.com"). +# Single-domain shorthand, used when `domains` is empty. Equivalent to one +# `domains` entry of {domain: cookieDomain, authHost: hostname}. +hostname: "" cookieDomain: "" tokens: sessionLifetime: 24h -# Pocket ID OIDC configuration. `clientID` is a public identifier; -# `clientSecret` is read from `existingSecret` (key: `client-secret`). +# Portals. Each is one OAuth2 client, selected by the middleware address +# (/portals/) — 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//client-secret. +portals: [] + # - name: main + # middlewareName: pocket-id-auth # default: - + # 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: endpoint: "" clientID: ""