diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ffa8c15..e2e26d5 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -160,10 +160,18 @@ jobs: fi done + - name: Pre-upgrade ingress migration + run: | + # team-devoops-plain previously contained the /auth (Keycloak) path. + # It has been moved to team-devoops-open. Delete the old ingress so + # the nginx admission webhook does not reject the new one for duplicate paths. + kubectl -n "$NAMESPACE" delete ingress team-devoops-plain --ignore-not-found + - name: Helm upgrade run: | helm upgrade --install team-devoops infra/helm/team-devoops \ --namespace "$NAMESPACE" \ --set global.image.tag=${{ github.sha }} \ --set keycloak.hostname=https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/auth \ + --set forwardAuth.cookieSecret="${{ secrets.FORWARD_AUTH_COOKIE_SECRET }}" \ --rollback-on-failure --timeout 15m diff --git a/infra/helm/team-devoops/files/realm-config.json b/infra/helm/team-devoops/files/realm-config.json index 091e210..f9648eb 100644 --- a/infra/helm/team-devoops/files/realm-config.json +++ b/infra/helm/team-devoops/files/realm-config.json @@ -70,10 +70,12 @@ "directAccessGrantsEnabled": false, "redirectUris": [ "https://team-devoops.uaenorth.cloudapp.azure.com/_oauth", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/oauth2/callback", "http://localhost/_oauth" ], "webOrigins": [ "https://team-devoops.uaenorth.cloudapp.azure.com", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de", "http://localhost" ] } diff --git a/infra/helm/team-devoops/templates/forward-auth.yaml b/infra/helm/team-devoops/templates/forward-auth.yaml new file mode 100644 index 0000000..e8d904d --- /dev/null +++ b/infra/helm/team-devoops/templates/forward-auth.yaml @@ -0,0 +1,81 @@ +{{- if .Values.forwardAuth.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oauth2-proxy + labels: + {{- include "team-devoops.labels" (dict "name" "oauth2-proxy" "root" $) | nindent 4 }} +spec: + replicas: 1 + strategy: + type: RollingUpdate + selector: + matchLabels: + {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 6 }} + template: + metadata: + labels: + {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 8 }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 4180 + env: + - name: OAUTH2_PROXY_PROVIDER + value: oidc + - name: OAUTH2_PROXY_OIDC_ISSUER_URL + value: {{ .Values.forwardAuth.oidcIssuerUrl | quote }} + - name: OAUTH2_PROXY_CLIENT_ID + value: {{ .Values.forwardAuth.clientId | quote }} + - name: OAUTH2_PROXY_CLIENT_SECRET + value: {{ .Values.forwardAuth.clientSecret | quote }} + - name: OAUTH2_PROXY_COOKIE_SECRET + value: {{ .Values.forwardAuth.cookieSecret | quote }} + - name: OAUTH2_PROXY_EMAIL_DOMAINS + value: "*" + - name: OAUTH2_PROXY_UPSTREAM + value: "static://202" + - name: OAUTH2_PROXY_HTTP_ADDRESS + value: "0.0.0.0:4180" + - name: OAUTH2_PROXY_REDIRECT_URL + value: {{ printf "https://%s/oauth2/callback" .Values.ingress.host | quote }} + - name: OAUTH2_PROXY_COOKIE_SECURE + value: "true" + - name: OAUTH2_PROXY_SKIP_PROVIDER_BUTTON + value: "true" + - name: OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL + value: "true" + - name: OAUTH2_PROXY_OIDC_EMAIL_CLAIM + value: "sub" + - name: OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST + value: "true" + - name: OAUTH2_PROXY_COOKIE_CSRF_EXPIRE + value: "5m" + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 50m + memory: 32Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: oauth2-proxy + labels: + {{- include "team-devoops.labels" (dict "name" "oauth2-proxy" "root" $) | nindent 4 }} +spec: + selector: + {{- include "team-devoops.selectorLabels" (dict "name" "oauth2-proxy") | nindent 4 }} + ports: + - port: 80 + targetPort: 4180 +{{- end }} diff --git a/infra/helm/team-devoops/templates/ingress.yaml b/infra/helm/team-devoops/templates/ingress.yaml index 61a8484..8e25ca1 100644 --- a/infra/helm/team-devoops/templates/ingress.yaml +++ b/infra/helm/team-devoops/templates/ingress.yaml @@ -1,10 +1,12 @@ {{- if .Values.ingress.enabled }} {{- $host := .Values.ingress.host }} {{- $tls := .Values.ingress.tls }} +{{- $fa := .Values.forwardAuth }} # --------------------------------------------------------------------------- # Stripped ingress: services whose path prefix must be removed before the # request reaches the backend (Traefik stripPrefix parity). Uses a regex # capture group so `/api/v1/members/foo` -> `/foo`. +# Auth-protected when forwardAuth is enabled. # --------------------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -18,6 +20,10 @@ metadata: {{- if and $tls.enabled $tls.clusterIssuer }} cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} {{- end }} + {{- if $fa.enabled }} + nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.{{ $.Release.Namespace }}.svc.cluster.local/oauth2/auth" + nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/oauth2/start?rd=$escaped_request_uri" + {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} {{- if $tls.enabled }} @@ -46,6 +52,7 @@ spec: --- # --------------------------------------------------------------------------- # Plain ingress: services served at their path as-is (web-client, api-docs). +# Auth-protected when forwardAuth is enabled. # --------------------------------------------------------------------------- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -53,10 +60,14 @@ metadata: name: team-devoops-plain labels: {{- include "team-devoops.labels" (dict "name" "ingress-plain" "root" $) | nindent 4 }} - {{- if and $tls.enabled $tls.clusterIssuer }} annotations: + {{- if and $tls.enabled $tls.clusterIssuer }} cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} - {{- end }} + {{- end }} + {{- if $fa.enabled }} + nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.{{ $.Release.Namespace }}.svc.cluster.local/oauth2/auth" + nginx.ingress.kubernetes.io/auth-signin: "https://{{ $host }}/oauth2/start?rd=$escaped_request_uri" + {{- end }} spec: ingressClassName: {{ .Values.ingress.className }} {{- if $tls.enabled }} @@ -72,7 +83,7 @@ spec: http: paths: {{- range $name, $svc := .Values.services }} - {{- if not $svc.stripPrefix }} + {{- if and (not $svc.stripPrefix) (not $svc.open) }} - path: {{ $svc.path }} pathType: Prefix backend: @@ -82,6 +93,35 @@ spec: number: {{ $svc.port }} {{- end }} {{- end }} +--- +# --------------------------------------------------------------------------- +# Open ingress: Keycloak (auth provider) and the forward-auth OAuth callback +# must never be behind forward-auth to avoid redirect loops. +# --------------------------------------------------------------------------- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: team-devoops-open + labels: + {{- include "team-devoops.labels" (dict "name" "ingress-open" "root" $) | nindent 4 }} + {{- if and $tls.enabled $tls.clusterIssuer }} + annotations: + cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if $tls.enabled }} + tls: + - hosts: + - {{ $host | quote }} + {{- if $tls.secretName }} + secretName: {{ $tls.secretName }} + {{- end }} + {{- end }} + rules: + - host: {{ $host | quote }} + http: + paths: {{- if .Values.keycloak.enabled }} - path: {{ .Values.keycloak.path }} pathType: Prefix @@ -91,4 +131,24 @@ spec: port: number: 8080 {{- end }} + {{- if $fa.enabled }} + - path: /oauth2/ + pathType: Prefix + backend: + service: + name: oauth2-proxy + port: + number: 80 + {{- end }} + {{- range $name, $svc := .Values.services }} + {{- if $svc.open }} + - path: {{ $svc.path }} + pathType: Prefix + backend: + service: + name: {{ $name }} + port: + number: {{ $svc.port }} + {{- end }} + {{- end }} {{- end }} diff --git a/infra/helm/team-devoops/values.yaml b/infra/helm/team-devoops/values.yaml index f6a8c7c..521f4d3 100644 --- a/infra/helm/team-devoops/values.yaml +++ b/infra/helm/team-devoops/values.yaml @@ -95,6 +95,21 @@ ingress: # Adds the cert-manager.io/cluster-issuer annotation on the ingresses. clusterIssuer: letsencrypt-prod +# --------------------------------------------------------------------------- +# Forward-auth: deploys oauth2-proxy as an OIDC session proxy and wires all +# nginx ingresses through it via auth_request. Keycloak and /oauth2/ are +# excluded from auth to prevent redirect loops. +# --------------------------------------------------------------------------- +forwardAuth: + enabled: true + oidcIssuerUrl: "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/auth/realms/devops" + clientId: "traefik-forward-auth" + clientSecret: "traefik-forward-auth-secret" + # 32+ character random string used to sign session cookies. + # Override at deploy time: --set forwardAuth.cookieSecret="$FORWARD_AUTH_COOKIE_SECRET" + # Generate with: openssl rand -base64 32 + cookieSecret: "" + # Rolling update strategy — maxSurge: 0 ensures the old pod is terminated before # scheduling the new one, which is required to stay within the namespace CPU quota. strategy: @@ -216,6 +231,7 @@ services: port: 8080 db: false stripPrefix: false + open: true resources: requests: cpu: 50m diff --git a/infra/keycloak/realm-config.json b/infra/keycloak/realm-config.json index 091e210..f9648eb 100644 --- a/infra/keycloak/realm-config.json +++ b/infra/keycloak/realm-config.json @@ -70,10 +70,12 @@ "directAccessGrantsEnabled": false, "redirectUris": [ "https://team-devoops.uaenorth.cloudapp.azure.com/_oauth", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de/oauth2/callback", "http://localhost/_oauth" ], "webOrigins": [ "https://team-devoops.uaenorth.cloudapp.azure.com", + "https://ge83mom-devops26.stud.k8s.aet.cit.tum.de", "http://localhost" ] }