Containers

Migrating from NGINX Ingress to ALB: Handling oauth2-proxy

The NGINX Ingress Controller was retired in March 2026. Many teams running Amazon Elastic Kubernetes Service (Amazon EKS) have already started migrating to the AWS Load Balancer Controller, a fully AWS native solution that provisions Application Load Balancers (ALBs) directly from Kubernetes Ingress resources.

The AWS guide Navigating the NGINX Ingress retirement: a practical guide to migration on AWS covers the core migration: the controller comparison, URI rewriting, and TLS termination. This post covers the one area that guide intentionally left out: preserving your OpenID Connect (OIDC) authentication flow when oauth2-proxy is in the request path.

For teams already running oauth2-proxy with NGINX for OIDC authentication (such as Keycloak and Okta), this migration raises a key question: how do I keep my authentication flow working with ALB?

This post presents two solutions:

  • Solution 1: ALB + oauth2-proxy in reverse proxy mode. The ALB forwards all traffic to oauth2-proxy, which handles authentication and proxies to the backend. Minimal change to your existing auth configuration.
  • Solution 2: ALB native OIDC. The ALB handles authentication itself using its built-in authenticate-oidc action. oauth2-proxy is removed entirely from the request path.

One trade-off to understand upfront: Solution 2 changes the header your backend receives. The ALB forwards the token in x-amzn-oidc-accesstoken, not the standard Authorization: Bearer header that oauth2-proxy sends. If your application expects the standard header, this is a subtle breaking change you will need to handle (covered in Solution 2). Solution 1 preserves the Authorization: Bearer header your backend already expects.

Solution overview

Both solutions work. The right choice depends on your requirements around token handling, the number of components you want to run, and whether your backend can accept a different header. Use this decision framework:

Criteria Solution 1: oauth2-proxy reverse proxy Solution 2: ALB native OIDC
Need custom claims / fine-grained token handling? Yes: full control through oauth2-proxy No: limited to ALB’s OIDC defaults
Want fewer components to run? No: keeps oauth2-proxy Yes: removes oauth2-proxy
Backend header changes acceptable? No change needed (Authorization: Bearer) Backend must read x-amzn-oidc-accesstoken or add a header proxy
Backward-compatible migration (NGINX + ALB coexist)? Yes Partial (auth flow differs)
Amount of configuration change Low: 3 flags + Ingress backend swap Medium: new annotations + identity provider callback change (register /oauth2/idpresponse)

In short: choose Solution 1 for the least disruption and no backend changes. Choose Solution 2 to remove oauth2-proxy when your backend can adapt to ALB’s headers.

Background: How NGINX + oauth2-proxy works today

Most NGINX + oauth2-proxy setups use auth subrequests, an NGINX-specific feature where NGINX validates every incoming request against oauth2-proxy before forwarding to the backend:

Client → NLB → NGINX Ingress Controller → Backend
                     │
                     └── subrequest → oauth2-proxy /oauth2/auth
                                      (returns 200 + headers, or 401)

The Ingress resource uses these annotations:

nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.auth.svc.cluster.local:4180/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://app.example.com/oauth2/start?rd=$escaped_request_uri"
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Auth-Request-User, X-Auth-Request-Email"

The ALB can’t perform subrequests. When you switch from ingressClassName: nginx to ingressClassName: alb, these annotations are silently ignored and traffic flows to the backend without authentication. Both solutions that follow address this.

Solution 1: ALB + oauth2-proxy in reverse proxy mode

Keep oauth2-proxy in your architecture but switch it from auth-subrequest mode (where NGINX called it) to reverse proxy mode (where it receives all traffic directly from the ALB). oauth2-proxy handles both authentication and proxying to the backend. The ALB’s only job is TLS termination and load balancing.

Client → ALB → oauth2-proxy → Backend
                (auth + proxy,
                 adds Authorization: Bearer)

Update oauth2-proxy configuration

Add these arguments to your existing oauth2-proxy deployment:

args:
  # Keep all existing args (provider, issuer-url, cookie settings, etc.)

  # ADD these for reverse proxy mode:
  - --upstream=http://my-backend.default.svc.cluster.local:8080
  - --reverse-proxy=true
  - --pass-authorization-header=true
Flag Required? Purpose
--upstream Yes Where to proxy authenticated requests (your backend)
--reverse-proxy=true Yes Trust X-Forwarded headers from ALB
--pass-authorization-header=true Yes Send Authorization: Bearer <token> to upstream. Replaces NGINX’s auth-response-headers: authorization
--set-xauthrequest=true Optional Send X-Auth-Request-User and X-Auth-Request-Email to upstream. Replaces NGINX’s auth-response-headers: x-auth-request-user, x-auth-request-email
--pass-access-token=true Optional Send X-Forwarded-Access-Token to upstream
--set-authorization-header=true Optional Set Authorization in the response back to the client

To exactly replicate what auth-response-headers: x-auth-request-user, x-auth-request-email, authorization did in NGINX, add all the optional flags.

This change is backward-compatible. oauth2-proxy in reverse proxy mode still responds to /oauth2/auth subrequests. Your existing NGINX Ingress continues working while you validate the ALB path.

Ingress configuration

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-alb
  annotations:
    alb.ingress.kubernetes.io/scheme: internal
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:REGION:ACCOUNT:certificate/CERT-ID
    alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
    alb.ingress.kubernetes.io/healthcheck-path: /ping
    alb.ingress.kubernetes.io/healthcheck-port: "4180"
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
    alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=86400
spec:
  ingressClassName: alb
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: oauth2-proxy    # ALL traffic goes to oauth2-proxy
                port:
                  number: 4180

The backend is oauth2-proxy, not your application. This is the key architectural difference from the NGINX Ingress, which pointed to the backend and used subrequests for auth.

If you use a local certificate (for example, cert-manager with a Vault or Let’s Encrypt issuer), keep the spec.tls section and remove the certificate-arn annotation. The ALB controller reads the cert from the referenced Kubernetes Secret.

Security groups

Add an inbound rule to your cluster/node security group allowing TCP 4180 from the ALB security group.

Solution 2: ALB native OIDC authentication

The ALB has a built-in OIDC authentication action. When configured, the ALB itself performs the full OpenID Connect flow: redirecting unauthenticated users to your identity provider (IdP), exchanging the authorization code for tokens, and setting a session cookie. No oauth2-proxy needed.

Client → ALB (authenticate-oidc action) → Backend
           │
           └── OIDC flow with your IdP (Keycloak, Okta, AWS IAM Identity Center, etc.)

After authentication, the ALB forwards the request to your backend with these headers:

x-amzn-oidc-accesstoken: <access token JWT>
x-amzn-oidc-identity: <user identity>
x-amzn-oidc-data: <ALB-signed JWT with user claims>

Ingress configuration

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    alb.ingress.kubernetes.io/scheme: internal
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:REGION:ACCOUNT:certificate/CERT-ID
    alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06

    # OIDC Authentication
    alb.ingress.kubernetes.io/auth-type: oidc
    alb.ingress.kubernetes.io/auth-idp-oidc: |
      {
        "issuer": "https://your-idp.example.com/realms/your-realm",
        "authorizationEndpoint": "https://your-idp.example.com/realms/your-realm/protocol/openid-connect/auth",
        "tokenEndpoint": "https://your-idp.example.com/realms/your-realm/protocol/openid-connect/token",
        "userInfoEndpoint": "https://your-idp.example.com/realms/your-realm/protocol/openid-connect/userinfo",
        "secretName": "alb-oidc-secret"
      }
    alb.ingress.kubernetes.io/auth-scope: "openid email profile"
    alb.ingress.kubernetes.io/auth-session-timeout: "86400"
    alb.ingress.kubernetes.io/auth-on-unauthenticated-request: authenticate

    # Health check
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
spec:
  ingressClassName: alb
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-backend
                port:
                  number: 8080

The OIDC secret contains your identity provider’s client credentials:

kubectl create secret generic alb-oidc-secret \
  --from-literal=clientId=my-app \
  --from-literal=clientSecret=YOUR_CLIENT_SECRET

Register the redirect URL with your identity provider

Before the flow will work, your identity provider must allow the ALB’s callback URL as a valid redirect/callback URI. The ALB always completes the OpenID Connect (OIDC) code exchange at the /oauth2/idpresponse path on your application’s hostname, so register:
https://app.example.com/oauth2/idpresponse

Use the same hostname your users hit: the load balancer DNS name or its CNAME alias. This is the most common miss with ALB native OIDC. If the redirect URI isn’t allowlisted, the identity provider rejects the callback and users get an error after signing in instead of reaching your application.

The header difference

As noted in the overview, this is the key trade-off of Solution 2. The ALB doesn’t set the standard Authorization: Bearer <token> header. It sets x-amzn-oidc-accesstoken instead. If your backend expects Authorization: Bearer, either modify your application to read x-amzn-oidc-accesstoken directly or place a lightweight proxy in front of your backend to translate the header.

Migration process (both solutions)

Regardless of which solution you choose, the migration follows the same safe pattern:

  1. Prepare – Provision an AWS Certificate Manager (ACM) certificate (or keep your local certificate). Verify subnet tags (kubernetes.io/role/internal-elb: 1). Verify that the alb IngressClass exists.
  2. Deploy in parallel – Create the ALB Ingress with a different name. Both NGINX and ALB coexist, each with its own load balancer. (See the parallel deployment options that follow.)
  3. Validate – Test the ALB path directly using a Host header override or DNS override. Verify the full authentication flow end-to-end.
  4. Cut over – Switch the DNS record to the ALB. For Amazon Route 53 Private Hosted Zones, propagation is near instant.
  5. Decommission – Delete the old NGINX Ingress. Uninstall the NGINX Ingress Controller.

Running NGINX and ALB in parallel

To reduce risk, validate the ALB path before sending real users to it. Because the ALB Ingress and the NGINX Ingress are independent resources, each provisioning its own load balancer, they can run side by side. There are two common approaches:

  • Separate hostname (recommended for validation): Expose the ALB under a distinct hostname such as app-alb.example.com while production traffic stays on app.example.com (NGINX). Run your full authentication test suite against the ALB hostname. When confident, repoint app.example.com to the ALB. This keeps the two paths fully isolated during testing.
  • Weighted DNS (recommended for gradual cutover): Use Route 53 weighted records on the production hostname. Start with a small weight to the ALB (for example, 10 percent) and the remainder to the NGINX Network Load Balancer (NLB). Monitor authentication success rates and error logs, then gradually increase the ALB weight to 100 percent. NGINX remains the fallback throughout the ramp.

Rollback at any point: revert the DNS record (or set the ALB weight back to 0) to send all traffic to the NGINX NLB. Keep the NGINX Ingress and controller in place until the cutover is fully validated.

When to choose each solution

Choose solution 1 (ALB + oauth2-proxy reverse proxy) if:

  • You want minimal change to your existing setup (add three flags, change the Ingress backend).
  • Your backend expects Authorization: Bearer <token> and you don’t want to modify it or add a header proxy.
  • You want a backward-compatible migration where NGINX and ALB coexist during validation.
  • You need custom claims or fine-grained token handling that oauth2-proxy already provides.
  • You already have oauth2-proxy configured and tested with your identity provider.

Choose solution 2 (ALB native OIDC) if:

  • You want to remove oauth2-proxy from the architecture entirely (fewer components to run).
  • Your backend can read x-amzn-oidc-accesstoken directly, or you accept adding a header proxy.
  • You want a minimal ALB configuration with no additional pods.
  • ALB’s built-in OIDC handling meets your token and session requirements.

Conclusion

Migrating from NGINX Ingress to ALB doesn’t mean losing your OIDC authentication. Both solutions preserve a secure authentication flow:

  • Solution 1 (oauth2-proxy reverse proxy) keeps your existing auth logic intact. Add three flags, change the Ingress backend, and you’re done. It supports a fully backward-compatible, parallel migration with the least disruption.
  • Solution 2 (ALB native OIDC) removes oauth2-proxy entirely. The ALB handles the OIDC flow natively. This recommended when your backend can accept ALB’s headers or you’re willing to adapt.

For most teams migrating from an existing NGINX + oauth2-proxy setup, Solution 1 gets you to ALB with the least risk and the fewest surprises. Pair this post with the broader Navigating the NGINX Ingress retirement guide for the complete migration picture.

 


About the author

Ali Sanhaji

Ali Sanhaji

Ali is a Technical Account Manager at AWS, working with enterprise platform teams on cloud-native architectures.