Repository: skypilot-org/skypilot
Pull request #5651:  add oauth2-proxy to the helm chart
Author: login: cg505
Created: 2025-05-20T21:16:06Z | Merged: 2025-05-22T01:50:16Z

<!-- Describe the changes in this PR -->
TODO: Whichever one is merged second, add a link from oauth2 proxy page to helm chart reference

<!-- Describe the tests ran -->
<!-- Unit tests (tests/test_*.py) are part of GitHub CI; below are tests that launch on the cloud. -->

Tested (run the relevant ones):

- [x] Code formatting: install pre-commit (auto-check on commit) or `bash format.sh`
- [x] Manually deployed and tested helm chart.
- [x] Verified helm chart output is equivalent to master when oauth2-proxy is disabled.
<!-- CI commands (/-prefixed) can only be triggered by repo members -->



Diff:
diff --git a/charts/skypilot/templates/auth.yaml b/charts/skypilot/templates/auth.yaml
index 70c492225b0..b2df2a73a81 100644
--- a/charts/skypilot/templates/auth.yaml
+++ b/charts/skypilot/templates/auth.yaml
@@ -1,4 +1,4 @@
-{{- if and (not .Values.ingress.authSecret) .Values.ingress.authCredentials }}
+{{- if and (not .Values.ingress.authSecret) .Values.ingress.authCredentials (not (index .Values.ingress "oauth2-proxy" "enabled")) }}
 apiVersion: v1
 kind: Secret
 metadata:
diff --git a/charts/skypilot/templates/ingress.yaml b/charts/skypilot/templates/ingress.yaml
index 0c6cd6eeb4a..310d24a053a 100644
--- a/charts/skypilot/templates/ingress.yaml
+++ b/charts/skypilot/templates/ingress.yaml
@@ -12,8 +12,15 @@ metadata:
     {{- if not $useNewIngressClass }}
     kubernetes.io/ingress.class: nginx
     {{- end }}
+    {{- if index .Values.ingress "oauth2-proxy" "enabled" }}
+    # OAuth2 Proxy authentication
+    nginx.ingress.kubernetes.io/auth-signin: {{ if index .Values.ingress "oauth2-proxy" "use-http" }}http{{ else }}https{{ end }}://$host/oauth2/start?rd=$escaped_request_uri
+    nginx.ingress.kubernetes.io/auth-url: {{ if index .Values.ingress "oauth2-proxy" "use-http" }}http{{ else }}https{{ end }}://$host/oauth2/auth
+    {{- else }}
+    # Basic authentication
     nginx.ingress.kubernetes.io/auth-type: basic
     nginx.ingress.kubernetes.io/auth-secret: {{ .Values.ingress.authSecret | default (printf "%s-basic-auth" .Release.Name) }}
+    {{- end }}
     nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
     nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
     nginx.ingress.kubernetes.io/proxy-body-size: "1000m"
diff --git a/charts/skypilot/templates/oauth2-proxy-deployment.yaml b/charts/skypilot/templates/oauth2-proxy-deployment.yaml
new file mode 100644
index 00000000000..4bfbab24477
--- /dev/null
+++ b/charts/skypilot/templates/oauth2-proxy-deployment.yaml
@@ -0,0 +1,88 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+{{- $oauth2 := index .Values.ingress "oauth2-proxy" }}
+{{- if not (index $oauth2 "oidc-issuer-url") -}}
+{{- fail "ingress.oauth2-proxy.oidc-issuer-url is required when OAuth2 proxy is enabled" -}}
+{{- end -}}
+{{- if eq (index $oauth2 "client-id") "" -}}
+{{- fail "ingress.oauth2-proxy.client-id is required when OAuth2 proxy is enabled" -}}
+{{- end -}}
+{{- if eq (index $oauth2 "client-secret") "" -}}
+{{- fail "ingress.oauth2-proxy.client-secret is required when OAuth2 proxy is enabled" -}}
+{{- end -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-oauth2-proxy
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-oauth2-proxy
+        skypilot.co/component: oauth2-proxy
+    spec:
+      containers:
+      - name: oauth2-proxy
+        image: {{ index .Values.ingress "oauth2-proxy" "image" | default "quay.io/oauth2-proxy/oauth2-proxy:v7.9.0" }}
+        imagePullPolicy: IfNotPresent
+        args:
+        - --provider=oidc
+        - --oidc-issuer-url={{ index .Values.ingress "oauth2-proxy" "oidc-issuer-url" }}
+        - --email-domain={{ index .Values.ingress "oauth2-proxy" "email-domain" | default "*" }}
+        - --upstream=file:///dev/null
+        - --http-address=0.0.0.0:4180
+        - --skip-provider-button=true
+        - --reverse-proxy=true
+        - --session-store-type={{ index .Values.ingress "oauth2-proxy" "session-store-type" | default "redis" }}
+        {{- if not (index .Values.ingress "oauth2-proxy" "use-https" | default false) }}
+        - --cookie-secure=false
+        {{- end }}
+        {{- if (index .Values.ingress "oauth2-proxy" "cookie-refresh") }}
+        - --cookie-refresh={{ index .Values.ingress "oauth2-proxy" "cookie-refresh" }}
+        {{- end }}
+        {{- if (index .Values.ingress "oauth2-proxy" "cookie-expire") }}
+        - --cookie-expire={{ index .Values.ingress "oauth2-proxy" "cookie-expire" }}
+        {{- end }}
+        {{- if eq (index .Values.ingress "oauth2-proxy" "session-store-type" | default "redis") "redis" }}
+        {{- if (index .Values.ingress "oauth2-proxy" "redis-url") }}
+        - --redis-connection-url={{ index .Values.ingress "oauth2-proxy" "redis-url" }}
+        {{- else }}
+        - --redis-connection-url=redis://$(SERVICE_NAME)-oauth2-proxy-redis:6379/0
+        {{- end }}
+        {{- end }}
+        env:
+        - name: OAUTH2_PROXY_CLIENT_ID
+          value: {{ index .Values.ingress "oauth2-proxy" "client-id" | quote }}
+        - name: OAUTH2_PROXY_CLIENT_SECRET
+          value: {{ index .Values.ingress "oauth2-proxy" "client-secret" | quote }}
+        - name: SERVICE_NAME
+          value: {{ .Release.Name }}
+        - name: OAUTH2_PROXY_COOKIE_SECRET
+          {{- /* Look up existing deployment to get the current cookie secret if available */}}
+          {{- $deploymentName := printf "%s-oauth2-proxy" .Release.Name }}
+          {{- $existingDeployment := lookup "apps/v1" "Deployment" .Release.Namespace $deploymentName }}
+          {{- if and $existingDeployment $existingDeployment.spec.template.spec.containers }}
+            {{- range $container := $existingDeployment.spec.template.spec.containers }}
+              {{- if eq $container.name "oauth2-proxy" }}
+                {{- range $env := $container.env }}
+                  {{- if eq $env.name "OAUTH2_PROXY_COOKIE_SECRET" }}
+          value: {{ $env.value }}
+                  {{- end }}
+                {{- end }}
+              {{- end }}
+            {{- end }}
+          {{- else }}
+          # Generate a random cookie secret if no existing one is found
+          value: {{ randAlphaNum 32 | b64enc | quote }}
+          {{- end }}
+        ports:
+        - containerPort: 4180
+          protocol: TCP
+{{- end }}
diff --git a/charts/skypilot/templates/oauth2-proxy-ingress.yaml b/charts/skypilot/templates/oauth2-proxy-ingress.yaml
new file mode 100644
index 00000000000..7734d1126ba
--- /dev/null
+++ b/charts/skypilot/templates/oauth2-proxy-ingress.yaml
@@ -0,0 +1,33 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+{{- $kubeVersion := .Capabilities.KubeVersion.Major -}}
+{{- $kubeMinorVersion := .Capabilities.KubeVersion.Minor | trimSuffix "+" | int -}}
+{{- $useNewIngressClass := or (gt ($kubeVersion | int) 1) (and (eq ($kubeVersion | int) 1) (ge $kubeMinorVersion 18)) -}}
+
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  annotations:
+    {{- if not $useNewIngressClass }}
+    kubernetes.io/ingress.class: nginx
+    {{- end }}
+spec:
+  {{- if $useNewIngressClass }}
+  ingressClassName: nginx
+  {{- end }}
+  rules:
+  - host: {{ .Values.ingress.host }}
+    http:
+      paths:
+      - path: /oauth2
+        pathType: Prefix
+        backend:
+          service:
+            name: {{ .Release.Name }}-oauth2-proxy
+            port:
+              number: 4180
+{{- end }}
diff --git a/charts/skypilot/templates/oauth2-proxy-redis.yaml b/charts/skypilot/templates/oauth2-proxy-redis.yaml
new file mode 100644
index 00000000000..8d0d68c0bcd
--- /dev/null
+++ b/charts/skypilot/templates/oauth2-proxy-redis.yaml
@@ -0,0 +1,52 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") (eq (index .Values.ingress "oauth2-proxy" "session-store-type" | default "redis") "redis") (not (index .Values.ingress "oauth2-proxy" "redis-url")) -}}
+{{- $redis := index .Values.ingress "oauth2-proxy" "redis" -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Release.Name }}-oauth2-proxy-redis
+  namespace: {{ .Release.Namespace }}
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy-redis
+    skypilot.co/component: oauth2-proxy-redis
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-oauth2-proxy-redis
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-oauth2-proxy-redis
+        skypilot.co/component: oauth2-proxy-redis
+    spec:
+      containers:
+      - name: redis
+        image: redis:7-alpine
+        ports:
+        - containerPort: 6379
+        resources:
+          requests:
+            cpu: 100m
+            memory: 128Mi
+          limits:
+            cpu: 200m
+            memory: 256Mi
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Release.Name }}-oauth2-proxy-redis
+  namespace: {{ .Release.Namespace }}
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy-redis
+    skypilot.co/component: oauth2-proxy-redis
+spec:
+  type: ClusterIP
+  ports:
+  - port: 6379
+    targetPort: 6379
+    protocol: TCP
+    name: redis
+  selector:
+    app: {{ .Release.Name }}-oauth2-proxy-redis
+{{- end -}}
diff --git a/charts/skypilot/templates/oauth2-proxy-service.yaml b/charts/skypilot/templates/oauth2-proxy-service.yaml
new file mode 100644
index 00000000000..b8b55bd54dc
--- /dev/null
+++ b/charts/skypilot/templates/oauth2-proxy-service.yaml
@@ -0,0 +1,18 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+spec:
+  ports:
+  - name: http
+    port: 4180
+    protocol: TCP
+    targetPort: 4180
+  selector:
+    app: {{ .Release.Name }}-oauth2-proxy
+{{- end }}
diff --git a/charts/skypilot/values.yaml b/charts/skypilot/values.yaml
index f09fb253258..e9d416dd89d 100644
--- a/charts/skypilot/values.yaml
+++ b/charts/skypilot/values.yaml
@@ -100,6 +100,32 @@ ingress:
   # Deprecated: use ingress-nginx.controller.service.nodePorts.https instead
   httpsNodePort: 30051 # Set to null to automatically assign a random port
 
+  # OAuth2 Proxy configuration for authentication
+  oauth2-proxy:
+    # Enable/disable OAuth2 Proxy deployment
+    enabled: false
+    # Docker image for OAuth2 Proxy
+    image: "quay.io/oauth2-proxy/oauth2-proxy:v7.9.0"
+    # Okta domain URL for OIDC issuer (required when enabled=true)
+    oidc-issuer-url: null
+    # Client ID from OIDC provider (Okta) (required when enabled=true)
+    client-id: ""
+    # Client Secret from OIDC provider (Okta) (required when enabled=true)
+    client-secret: ""
+    # Use HTTPS (set to true for production environments)
+    use-https: false
+    # Email domains to allow (use "*" for all domains)
+    email-domain: "*"
+    # Session storage settings ("cookie" or "redis")
+    session-store-type: "redis"
+    # Redis connection URL (redis://host[:port][/db-number])
+    # If left as null and session-store-type is redis, a Redis instance will be deployed automatically
+    # If set, this external Redis instance will be used
+    redis-url: null
+    # Cookie timing settings (in seconds)
+    cookie-refresh: null  # refresh tokens after this duration (Access token lifespan minus 1 min)
+    cookie-expire: null  # cookie expiration time (should match refresh token lifespan)
+
 ingress-nginx:
   enabled: true
   controller:
diff --git a/docs/source/reference/api-server/examples/api-server-auth-proxy.rst b/docs/source/reference/api-server/examples/api-server-auth-proxy.rst
index 0ef14ed66c1..0c40ffab462 100644
--- a/docs/source/reference/api-server/examples/api-server-auth-proxy.rst
+++ b/docs/source/reference/api-server/examples/api-server-auth-proxy.rst
@@ -99,8 +99,6 @@ Set up the environment variables for your API server deployment. ``NAMESPACE`` a
 
     NAMESPACE=skypilot # TODO: change to your installed namespace
     RELEASE_NAME=skypilot # TODO: change to your installed release name
-    VERSION=1.0.0-dev20250410 # TODO: change to the version you want to upgrade to
-    IMAGE_REPO=berkeleyskypilot/skypilot-nightly
 
 Use ``helm upgrade`` to redeploy the API server helm chart with the ``skypilot-oauth2-proxy`` deployment. Replace ``<CLIENT ID>`` and ``<CLIENT SECRET>`` with the values from the Okta admin console above, and ``<OKTA URL>`` with your Okta login URL.
 
@@ -113,8 +111,6 @@ Use ``helm upgrade`` to redeploy the API server helm chart with the ``skypilot-o
       --set ingress.oauth2-proxy.client-id=<CLIENT ID> \
       --set ingress.oauth2-proxy.client-secret=<CLIENT SECRET>
 
-If your API server endpoint only has HTTP, not HTTPS, add ``--set ingress.oauth2-proxy.use-http=true``.
-
 To make sure it's working, visit your endpoint URL in a browser. You should be redirected to Okta to sign in.
 
 Now, you can use ``sky api login -e <ENDPOINT>`` to go though the login flow for the CLI.
diff --git a/docs/source/reference/api-server/helm-values-spec.rst b/docs/source/reference/api-server/helm-values-spec.rst
index 79be55e939f..815069deb5c 100644
--- a/docs/source/reference/api-server/helm-values-spec.rst
+++ b/docs/source/reference/api-server/helm-values-spec.rst
@@ -19,7 +19,7 @@ The SkyPilot API server helm chart provides typical `helm values <https://helm.s
 * ``--set``: Specify overrides on the command line.
 
   .. code-block:: bash
-      
+
       helm install $RELEASE_NAME skypilot/skypilot-nightly --set apiService.image="berkeleyskypilot/skypilot:0.9.1"
 
 Values
@@ -69,6 +69,20 @@ Below is the available helm value keys and the default value of each key:
     :ref:`authSecret <helm-values-ingress-authSecret>`: null
     :ref:`authCredentials <helm-values-ingress-authCredentials>`: "username:$apr1$encrypted_password"
     :ref:`path <helm-values-ingress-path>`: '/'
+    :ref:`oauth2-proxy <helm-values-ingress-oauth2-proxy>`:
+      :ref:`enabled <helm-values-ingress-oauth2-proxy-enabled>`: false
+      # Required when enabled:
+      :ref:`oidc-issuer-url <helm-values-ingress-oauth2-proxy-oidc-issuer-url>`: null
+      :ref:`client-id <helm-values-ingress-oauth2-proxy-client-id>`: ""
+      :ref:`client-secret <helm-values-ingress-oauth2-proxy-client-secret>`: ""
+      # Optional settings:
+      :ref:`image <helm-values-ingress-oauth2-proxy-image>`: "quay.io/oauth2-proxy/oauth2-proxy:v7.9.0"
+      :ref:`use-https <helm-values-ingress-oauth2-proxy-use-https>`: false
+      :ref:`email-domain <helm-values-ingress-oauth2-proxy-email-domain>`: "*"
+      :ref:`session-store-type <helm-values-ingress-oauth2-proxy-session-store-type>`: "redis"
+      :ref:`redis-url <helm-values-ingress-oauth2-proxy-redis-url>`: null
+      :ref:`cookie-refresh <helm-values-ingress-oauth2-proxy-cookie-refresh>`: null
+      :ref:`cookie-expire <helm-values-ingress-oauth2-proxy-cookie-expire>`: null
 
   :ref:`ingress-nginx <helm-values-ingress-nginx>`:
     :ref:`enabled <helm-values-ingress-nginx-enabled>`: true
@@ -380,9 +394,9 @@ Default: ``true``
 ``ingress.authSecret``
 ^^^^^^^^^^^^^^^^^^^^^^
 
-Name of the Kubernetes secret containing basic auth credentials for ingress. If not specified, a new secret will be created using ``authCredentials``.
+Name of the Kubernetes secret containing basic auth credentials for ingress. If not specified, a new secret will be created using ``authCredentials``. This is ignored if ``ingress.oauth2-proxy.enabled`` is ``true``.
 
-One of ``ingress.authSecret`` or ``ingress.authCredentials`` must be set.
+One of ``ingress.authSecret`` or ``ingress.authCredentials`` must be set, unless ``ingress.oauth2-proxy.enabled`` is ``true``.
 
 Default: ``null``
 
@@ -396,9 +410,9 @@ Default: ``null``
 ``ingress.authCredentials``
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Basic auth credentials in the format ``username:encrypted_password``. Used only if ``authSecret`` is not set.
+Basic auth credentials in the format ``username:encrypted_password``. Used only if ``authSecret`` is not set. This is ignored if ``ingress.oauth2-proxy.enabled`` is ``true``.
 
-One of ``ingress.authSecret`` or ``ingress.authCredentials`` must be set.
+One of ``ingress.authSecret`` or ``ingress.authCredentials`` must be set, unless ``ingress.oauth2-proxy.enabled`` is ``true``.
 
 Default: ``"username:$apr1$encrypted_password"``
 
@@ -421,6 +435,200 @@ Default: ``'/'``
   ingress:
     path: '/'
 
+.. _helm-values-ingress-oauth2-proxy:
+
+``ingress.oauth2-proxy``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Configuration for the OAuth2 Proxy authentication for the API server. This enables SSO providers like Okta.
+
+If enabled, ``ingress.authSecret`` and ``ingress.authCredentials`` are ignored.
+
+Default: see the yaml below.
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      enabled: false
+      # Required when enabled:
+      oidc-issuer-url: null
+      client-id: ""
+      client-secret: ""
+      # Optional settings:
+      image: "quay.io/oauth2-proxy/oauth2-proxy:v7.9.0"
+      use-https: false
+      email-domain: "*"
+      session-store-type: "redis"
+      redis-url: null
+      cookie-refresh: null
+      cookie-expire: null
+
+.. _helm-values-ingress-oauth2-proxy-enabled:
+
+``ingress.oauth2-proxy.enabled``
+''''''''''''''''''''''''''''''''''''
+
+Enable OAuth2 Proxy for authentication. When enabled, this will deploy an OAuth2 Proxy component and configure the ingress to use it for authentication instead of basic auth.
+
+Default: ``false``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      enabled: true
+
+.. _helm-values-ingress-oauth2-proxy-oidc-issuer-url:
+
+``ingress.oauth2-proxy.oidc-issuer-url``
+''''''''''''''''''''''''''''''''''''''''
+
+The URL of the OIDC issuer (e.g., your Okta domain). Required when oauth2-proxy is enabled.
+
+Default: ``null``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      oidc-issuer-url: "https://mycompany.okta.com"
+
+.. _helm-values-ingress-oauth2-proxy-client-id:
+
+``ingress.oauth2-proxy.client-id``
+''''''''''''''''''''''''''''''''''
+
+The OAuth client ID from your OIDC provider (e.g., Okta). Required when oauth2-proxy is enabled.
+
+Default: ``""``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      client-id: "0abc123def456"
+
+.. _helm-values-ingress-oauth2-proxy-client-secret:
+
+``ingress.oauth2-proxy.client-secret``
+'''''''''''''''''''''''''''''''''''''''''
+
+The OAuth client secret from your OIDC provider (e.g., Okta). Required when oauth2-proxy is enabled.
+
+Default: ``""``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      client-secret: "abcdef123456"
+
+.. _helm-values-ingress-oauth2-proxy-image:
+
+``ingress.oauth2-proxy.image``
+''''''''''''''''''''''''''''''
+
+Docker image for the OAuth2 Proxy component.
+
+Default: ``"quay.io/oauth2-proxy/oauth2-proxy:v7.9.0"``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      image: "quay.io/oauth2-proxy/oauth2-proxy:v7.9.0"
+
+.. _helm-values-ingress-oauth2-proxy-use-https:
+
+``ingress.oauth2-proxy.use-https``
+''''''''''''''''''''''''''''''''''
+
+Set to ``true`` when using HTTPS for the API server endpoint. When set to ``false``, secure cookies are disabled, which is required for HTTP endpoints.
+
+Default: ``false``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      use-https: true
+
+.. _helm-values-ingress-oauth2-proxy-email-domain:
+
+``ingress.oauth2-proxy.email-domain``
+'''''''''''''''''''''''''''''''''''''''
+
+Email domains to allow for authentication. Use ``"*"`` to allow all email domains.
+
+Default: ``"*"``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      email-domain: "mycompany.com"
+
+.. _helm-values-ingress-oauth2-proxy-session-store-type:
+
+``ingress.oauth2-proxy.session-store-type``
+'''''''''''''''''''''''''''''''''''''''''''
+
+Session storage type for OAuth2 Proxy. Can be set to ``"cookie"`` or ``"redis"``. Using Redis as a session store results in smaller cookies and better performance for large-scale deployments.
+
+Default: ``"redis"``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      session-store-type: "redis"
+
+.. _helm-values-ingress-oauth2-proxy-redis-url:
+
+``ingress.oauth2-proxy.redis-url``
+''''''''''''''''''''''''''''''''''
+
+URL to connect to an external Redis instance for session storage. If set to ``null`` and ``session-store-type`` is ``"redis"``, a Redis instance will be automatically deployed. Format: ``redis://host[:port][/db-number]``
+
+Default: ``null``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      redis-url: "redis://redis-host:6379/0"
+
+.. _helm-values-ingress-oauth2-proxy-cookie-refresh:
+
+``ingress.oauth2-proxy.cookie-refresh``
+'''''''''''''''''''''''''''''''''''''''
+
+Duration in seconds after which to refresh the access token. This should typically be set to the access token lifespan minus 1 minute. If not set, tokens will not be refreshed automatically.
+
+Default: ``null``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      cookie-refresh: 3540  # 59 minutes (for a 60-minute access token)
+
+.. _helm-values-ingress-oauth2-proxy-cookie-expire:
+
+``ingress.oauth2-proxy.cookie-expire``
+''''''''''''''''''''''''''''''''''''''
+
+Expiration time for cookies in seconds. Should match the refresh token lifespan from your OIDC provider.
+
+Default: ``null``
+
+.. code-block:: yaml
+
+  ingress:
+    oauth2-proxy:
+      cookie-expire: 86400  # 24 hours
+
 .. _helm-values-ingress-nginx:
 
 ``ingress-nginx``


Reviews:
user: login: Michaelvll
body: LGTM! Thanks @cg505!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-20T22:17:49Z
user: login: aylei
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T01:06:35Z
user: login: Michaelvll
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T01:47:11Z

Review comments:
diff_hunk: @@ -0,0 +1,51 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-oauth2-proxy
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-oauth2-proxy
+        skypilot.co/component: oauth2-proxy
+    spec:
+      containers:
+      - name: oauth2-proxy
+        image: quay.io/oauth2-proxy/oauth2-proxy:latest
+        imagePullPolicy: Always
+        args:
+        - --provider=oidc
+        - --oidc-issuer-url={{ index .Values.ingress "oauth2-proxy" "oidc-issuer-url" }}
+        - --email-domain={{ index .Values.ingress "oauth2-proxy" "email-domain" | default "*" }}
+        - --upstream=file:///dev/null
+        - --http-address=0.0.0.0:4180
+        - --skip-provider-button=true
+        - --reverse-proxy=true
+        {{- if (index .Values.ingress "oauth2-proxy" "use-http") }}
+        - --cookie-secure=false
+        {{- end }}
+        env:
+        - name: OAUTH2_PROXY_CLIENT_ID
+          value: {{ index .Values.ingress "oauth2-proxy" "client-id" | quote }}
+        - name: OAUTH2_PROXY_CLIENT_SECRET
+          value: {{ index .Values.ingress "oauth2-proxy" "client-secret" | quote }}
+        - name: OAUTH2_PROXY_COOKIE_SECRET
+          {{- if index .Values.ingress "oauth2-proxy" "cookie-secret" }}
+          value: {{ index .Values.ingress "oauth2-proxy" "cookie-secret" | quote }}
+          {{- else }}
+          # Generate a random cookie secret if not provided
+          value: {{ randAlphaNum 16 | b64enc | quote }}
+          {{- end }}
+        ports:
+        - containerPort: 4180
+          protocol: TCP
+{{- end }}
path: charts/skypilot/templates/oauth2-proxy-deployment.yaml
user: login: Michaelvll
body: minor: EOF newline
created_at: 2025-05-20T22:14:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,51 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-oauth2-proxy
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-oauth2-proxy
+        skypilot.co/component: oauth2-proxy
+    spec:
+      containers:
+      - name: oauth2-proxy
+        image: quay.io/oauth2-proxy/oauth2-proxy:latest
path: charts/skypilot/templates/oauth2-proxy-deployment.yaml
user: login: aylei
body: Can we use a specific tag for deterministic deployment?
created_at: 2025-05-21T00:55:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,51 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-oauth2-proxy
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-oauth2-proxy
+        skypilot.co/component: oauth2-proxy
+    spec:
+      containers:
+      - name: oauth2-proxy
+        image: quay.io/oauth2-proxy/oauth2-proxy:latest
path: charts/skypilot/templates/oauth2-proxy-deployment.yaml
user: login: aylei
body: Also, might be good to make the image configurable. Uses may use a registry mirror or their private registry.
created_at: 2025-05-21T00:57:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,51 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-oauth2-proxy
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-oauth2-proxy
+        skypilot.co/component: oauth2-proxy
+    spec:
+      containers:
+      - name: oauth2-proxy
+        image: quay.io/oauth2-proxy/oauth2-proxy:latest
+        imagePullPolicy: Always
path: charts/skypilot/templates/oauth2-proxy-deployment.yaml
user: login: aylei
body: If we switch to a specific tag, we can use `IfNotPresent` for a slight optimization
created_at: 2025-05-21T00:57:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,51 @@
+{{- if and .Values.ingress.enabled (index .Values.ingress "oauth2-proxy" "enabled") }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: {{ .Release.Name }}-oauth2-proxy
+    skypilot.co/component: oauth2-proxy
+  name: {{ .Release.Name }}-oauth2-proxy
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-oauth2-proxy
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-oauth2-proxy
+        skypilot.co/component: oauth2-proxy
+    spec:
+      containers:
+      - name: oauth2-proxy
+        image: quay.io/oauth2-proxy/oauth2-proxy:latest
+        imagePullPolicy: Always
+        args:
+        - --provider=oidc
+        - --oidc-issuer-url={{ index .Values.ingress "oauth2-proxy" "oidc-issuer-url" }}
+        - --email-domain={{ index .Values.ingress "oauth2-proxy" "email-domain" | default "*" }}
+        - --upstream=file:///dev/null
+        - --http-address=0.0.0.0:4180
+        - --skip-provider-button=true
+        - --reverse-proxy=true
+        {{- if (index .Values.ingress "oauth2-proxy" "use-http") }}
+        - --cookie-secure=false
+        {{- end }}
+        env:
+        - name: OAUTH2_PROXY_CLIENT_ID
+          value: {{ index .Values.ingress "oauth2-proxy" "client-id" | quote }}
+        - name: OAUTH2_PROXY_CLIENT_SECRET
+          value: {{ index .Values.ingress "oauth2-proxy" "client-secret" | quote }}
+        - name: OAUTH2_PROXY_COOKIE_SECRET
+          {{- if index .Values.ingress "oauth2-proxy" "cookie-secret" }}
+          value: {{ index .Values.ingress "oauth2-proxy" "cookie-secret" | quote }}
+          {{- else }}
+          # Generate a random cookie secret if not provided
+          value: {{ randAlphaNum 16 | b64enc | quote }}
path: charts/skypilot/templates/oauth2-proxy-deployment.yaml
user: login: aylei
body: Looks like the cookie-secret will be updated every time a `hem upgrade` is called if it is not explicitly specified, will this invalidate all the issued cookies?
created_at: 2025-05-21T01:01:51Z
author_association: COLLABORATOR
diff_hunk: @@ -100,6 +100,23 @@ ingress:
   # Deprecated: use ingress-nginx.controller.service.nodePorts.https instead
   httpsNodePort: 30051 # Set to null to automatically assign a random port
 
+  # OAuth2 Proxy configuration for authentication
+  oauth2-proxy:
+    # Enable/disable OAuth2 Proxy deployment
+    enabled: false
+    # Okta domain URL for OIDC issuer
+    oidc-issuer-url: "https://your-okta-domain.okta.com"
path: charts/skypilot/values.yaml
user: login: aylei
body: Since it is a required field, maybe we can leave this null and validate whether it is set when `oauth2-proxy.enabled=true`
created_at: 2025-05-21T01:05:47Z
author_association: COLLABORATOR

PR comments:
user: login: aylei
created_at: 2025-05-21T01:59:23Z
body: Thanks @cg505 ! Looks good to me except the `cookie-secret` generation https://github.com/skypilot-org/skypilot/pull/5651#discussion_r2099102245
author_association: COLLABORATOR
user: login: cg505
created_at: 2025-05-21T19:40:16Z
body: Added redis for shorter cookie token
author_association: COLLABORATOR