- Published on
EKS ALB Ingress 401 반복 - OIDC·JWT·헤더 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 살아 있고(200을 내야 할 것 같은데) ALB Ingress에서만 401이 반복되는 상황은 생각보다 흔합니다. 특히 AWS Load Balancer Controller(ALB Ingress) + OIDC(Authenticate) + 애플리케이션 JWT 검증이 섞이면, 인증이 두 번 적용되거나(중복 인증), 토큰이 헤더로 전달되지 않거나(헤더 유실), 토큰의 audience/issuer가 불일치하는 식으로 401 루프가 발생합니다.
이 글은 “EKS에서 ALB Ingress 401 반복”을 OIDC·JWT·헤더 관점에서 빠르게 좁혀가는 체크리스트로 구성했습니다. (ALB 5xx 계열 문제와 달리 401은 대부분 설정/정책/헤더 계층에서 발생합니다.)
> 참고로 5xx(502/504)와 섞여 보이면 먼저 네트워크/헬스체크를 분리해서 보세요: EKS ALB Ingress 504(5xx) 간헐 발생 원인·해결, EKS에서 ALB Ingress 502 Bad Gateway 원인 9가지
1) 먼저 “누가 401을 내는지”부터 분리
401의 주체가 다르면 접근법이 완전히 달라집니다.
1-1. ALB가 401을 내는 경우(Authenticate 단계)
ALB OIDC 인증(Authenticate OIDC/Cognito)이 실패하면 ALB가 직접 401/302를 반환합니다.
- 브라우저에서는 로그인 페이지로 302 리다이렉트가 반복되거나
- API 호출에서는 401과 함께
www-authenticate또는 OIDC 관련 쿠키/리다이렉트 힌트가 보일 수 있습니다.
1-2. 애플리케이션이 401을 내는 경우(JWT 검증 단계)
ALB는 요청을 정상적으로 백엔드로 전달했지만, 앱이 JWT를 검증하다가 401을 내는 케이스입니다.
- 응답 헤더에 앱 서버/프레임워크 특징이 보이거나
- ALB Access Log에서
target_status_code=401형태로 확인됩니다.
1-3. 빠른 판별 방법: ALB 액세스 로그
ALB 액세스 로그를 켜고(또는 이미 켜져 있다면) 아래 필드를 봅니다.
elb_status_code= ALB가 클라이언트에 준 코드target_status_code= 타겟(Pod/Service)이 준 코드
elb_status_code=401이고 target_status_code=-면 ALB 인증 단계에서 막힌 것입니다. elb_status_code=401이면서 target_status_code=401이면 앱도 401을 내고 있을 가능성이 큽니다(혹은 ALB가 401을 그대로 전달).
2) ALB Ingress OIDC 설정에서 가장 흔한 함정
AWS Load Balancer Controller에서 OIDC 인증은 보통 Ingress annotation으로 구성합니다.
2-1. auth-type/auth-idp-oidc JSON 오타 및 필수값 누락
대표적으로 JSON 문자열 escaping 문제, issuer/authorizationEndpoint/tokenEndpoint/userInfoEndpoint 누락, secretName 불일치가 401 원인이 됩니다.
아래는 예시입니다(값은 환경에 맞게 변경).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
# OIDC 인증
alb.ingress.kubernetes.io/auth-type: oidc
alb.ingress.kubernetes.io/auth-on-unauthenticated-request: authenticate
alb.ingress.kubernetes.io/auth-scope: openid profile email
alb.ingress.kubernetes.io/auth-session-timeout: '3600'
alb.ingress.kubernetes.io/auth-session-cookie: AWSELBAuthSessionCookie
alb.ingress.kubernetes.io/auth-idp-oidc: >-
{"issuer":"https://idp.example.com","authorizationEndpoint":"https://idp.example.com/oauth2/authorize","tokenEndpoint":"https://idp.example.com/oauth2/token","userInfoEndpoint":"https://idp.example.com/oauth2/userinfo","secretName":"oidc-client-secret"}
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
점검 포인트:
secretName이 존재하는지- secret에
clientID,clientSecret키가 기대 형식인지(컨트롤러 버전에 따라 키 이름이 다를 수 있어 문서/예제 확인 필요) issuer가 정확히 일치하는지(끝 슬래시 유무 포함)
2-2. 콜백 URL/리다이렉트 URI 불일치
OIDC 공급자(IdP) 콘솔에서 등록한 redirect URI가 ALB의 콜백 경로와 다르면 인증 후 다시 401/302 루프가 납니다.
- 일반적으로 ALB는
https://<host>/oauth2/idpresponse경로를 사용합니다(구성에 따라 다를 수 있음). X-Forwarded-Proto/HTTPS 종단이 꼬이면 IdP에http://로 등록되어 실패하는 경우도 있습니다.
2-3. 쿠키 도메인/SameSite 문제(특히 크로스 도메인)
프론트(app.example.com)와 API(api.example.com)가 분리되어 있고 브라우저 기반 호출이라면, OIDC 세션 쿠키가 의도대로 전송되지 않아 401이 반복될 수 있습니다.
- SameSite 정책 때문에 third-party로 간주되면 쿠키가 빠짐
- 도메인/Path가 맞지 않으면 쿠키가 저장/전송되지 않음
이 경우는 브라우저 개발자 도구에서 Set-Cookie와 실제 요청의 Cookie 헤더를 비교하면 바로 드러납니다.
3) “ALB OIDC + 앱 JWT”를 같이 쓸 때 생기는 401 루프
가장 흔한 구조는 다음 중 하나입니다.
- ALB에서 OIDC 인증을 하고, 앱은 “인증된 사용자 헤더”를 신뢰
- 앱이 Bearer JWT를 검증하고, ALB는 단순 L7 라우팅
- 둘 다 켜져 있는데(중복), 서로 기대하는 토큰/헤더가 달라서 401 반복
3-1. 중복 인증: ALB는 OIDC 세션 쿠키, 앱은 Authorization: Bearer 기대
ALB OIDC 인증은 기본적으로 브라우저 세션(쿠키) 기반으로 동작합니다. 반면 많은 API 서버는 Authorization: Bearer <jwt>를 기대합니다.
- 클라이언트가 Bearer 토큰을 보내지 않는데 앱이 Bearer를 강제하면 → 앱 401
- 클라이언트가 Bearer 토큰을 보내지만 ALB가 먼저 OIDC를 강제하면 → ALB 401/302
해결 방향(택1):
- API는 Bearer로만 가고 싶다 → Ingress에서
auth-type제거(또는 경로별로 분리) - ALB OIDC를 쓰고 앱은 헤더 기반으로 신뢰 → 앱의 JWT 검증을 끄거나, ALB가 주는 클레임 헤더를 검증하도록 변경
3-2. ALB가 주는 OIDC 관련 헤더를 앱이 못 읽는 경우
ALB OIDC 인증을 통과하면 요청에 사용자 정보 헤더가 추가될 수 있습니다(구성/버전에 따라 다름). 앱이 이를 기대하는데 실제로는 전달이 안 되는 경우가 있습니다.
대표 원인:
- Ingress/TargetGroup의 헤더 허용/차단(프록시/미들웨어에서 drop)
- 앱 서버(예: Spring, Express, Envoy)에서 underscores/대문자 헤더 처리가 달라 누락
- CORS preflight(OPTIONS)는 인증 제외가 필요하지만 인증이 걸려 401
4) JWT 검증 관점 체크리스트(issuer/audience/alg/kid)
앱이 401을 반환한다면 JWT 검증 실패가 핵심입니다.
4-1. issuer(iss) 불일치
iss는 문자열 완전 일치가 기본입니다.
https://idp.example.comvshttps://idp.example.com/차이- Cognito의 경우 리전/유저풀 경로가 정확해야 함
4-2. audience(aud) 불일치
SPA/모바일/서버 간에 client_id가 다르면 aud가 달라집니다.
- 프론트에서 받은 토큰은
aud=frontend-client - 백엔드가 기대하는
aud=api-client
이 경우 “토큰은 유효한데 우리 서비스용 토큰이 아님”으로 401이 납니다.
4-3. alg, kid, JWKS 회전
IdP가 키를 회전하면 kid가 바뀌고, 앱의 JWKS 캐시가 오래되면 검증 실패가 발생합니다.
- 증상: 특정 시점부터 401 급증, 시간이 지나면 자연 회복/반복
- 해결: JWKS 캐시 TTL 단축, 실패 시 재조회 로직 추가
아래는 Python(PyJWT)에서 JWKS를 사용해 검증하는 최소 예시입니다.
import time
import requests
import jwt
from jwt import PyJWKClient
ISSUER = "https://idp.example.com"
JWKS_URL = f"{ISSUER}/.well-known/jwks.json"
AUDIENCE = "api-client-id"
_jwk_client = PyJWKClient(JWKS_URL)
def verify(token: str) -> dict:
signing_key = _jwk_client.get_signing_key_from_jwt(token).key
return jwt.decode(
token,
signing_key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=ISSUER,
options={"require": ["exp", "iat", "iss"]},
)
if __name__ == "__main__":
t = "<paste jwt>"
print(verify(t))
4-4. clock skew(서버 시간 오차)
exp, nbf, iat 검증에서 서버 시간이 어긋나면 간헐 401이 납니다.
- 노드 NTP 동기화
- 허용 skew(예: 60~120초) 적용
5) 헤더 전달/변조 문제: ALB → TargetGroup → Pod
401이 “토큰이 없어서”라면 대개 헤더가 중간에서 사라졌습니다.
5-1. 먼저 Pod에서 실제로 어떤 헤더를 받는지 덤프
가장 빠른 방법은 디버그용 echo 서버를 붙여보는 것입니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: header-echo
spec:
replicas: 1
selector:
matchLabels:
app: header-echo
template:
metadata:
labels:
app: header-echo
spec:
containers:
- name: echo
image: ealen/echo-server:0.9.2
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: header-echo
spec:
selector:
app: header-echo
ports:
- port: 80
targetPort: 80
Ingress를 이 서비스로 잠깐 붙인 뒤, 클라이언트가 보내는 Authorization/쿠키가 Pod까지 오는지 확인합니다.
5-2. curl로 “ALB 앞단”과 “Pod 직통” 비교
# 1) ALB 도메인으로 호출
curl -vk https://api.example.com/me \
-H 'Authorization: Bearer <JWT>'
# 2) 포트포워딩으로 Pod/Service 직통
kubectl port-forward svc/api-svc 18080:80
curl -vk http://127.0.0.1:18080/me \
-H 'Authorization: Bearer <JWT>'
- Pod 직통은 200인데 ALB에서는 401 → ALB/OIDC/헤더 전달 계층 문제
- 둘 다 401 → 앱 JWT 검증/설정 문제
5-3. OPTIONS 프리플라이트에 인증이 걸려 401
브라우저에서 CORS 요청이면 먼저 OPTIONS가 날아갑니다.
- OPTIONS에 Authorization이 없는데 앱/ALB가 인증을 강제하면 401
- 해결: OPTIONS 경로/메서드는 인증 제외 또는 CORS 미들웨어에서 먼저 처리
ALB Ingress에서 경로를 분리하거나(예: /api/*만 인증), 앱에서 OPTIONS를 먼저 허용하는 방식이 일반적입니다.
6) AWS Load Balancer Controller/Ingress 리소스 점검 명령
컨트롤러가 실제로 어떤 규칙/액션을 만들었는지 확인해야 “내가 의도한 OIDC 액션이 적용됐는지”를 알 수 있습니다.
# Ingress annotation/이벤트 확인
kubectl describe ingress api
# 컨트롤러 로그에서 auth 관련 에러 탐색
kubectl -n kube-system logs deploy/aws-load-balancer-controller \
| egrep -i 'oidc|authenticate|unauth|error|denied'
또한 AWS CLI로 리스너 규칙을 직접 확인하면, Ingress annotation이 어떤 Listener Rule로 변환됐는지 볼 수 있습니다.
aws elbv2 describe-load-balancers --names <your-alb-name>
aws elbv2 describe-listeners --load-balancer-arn <alb-arn>
aws elbv2 describe-rules --listener-arn <listener-arn>
여기서 Rule action에 authenticate-oidc가 붙어 있는지, 우선순위/조건(host/path)이 의도대로인지 확인합니다.
7) 실전에서 가장 많이 맞닥뜨린 401 원인 TOP 7
현장에서 재현 빈도가 높은 순서로 정리하면 다음과 같습니다.
- OIDC redirect URI 불일치로 로그인 후 다시 401/302 루프
- ALB OIDC와 앱 Bearer JWT를 동시에 강제(중복 인증)
- audience 불일치(프론트 토큰을 백엔드에서 검증)
- issuer 문자열 불일치(슬래시/경로 차이)
- JWKS kid 회전 + 캐시로 특정 시점부터 401 급증
- CORS preflight(OPTIONS) 401
- 쿠키 SameSite/도메인 문제로 세션 쿠키 미전송
8) 권장 아키텍처 패턴: “인증 책임”을 한 군데로 모으기
401 반복은 대부분 책임 분리가 애매할 때 생깁니다. 아래 중 하나로 단순화하면 운영이 쉬워집니다.
패턴 A: ALB에서 OIDC 종료, 앱은 신뢰 헤더 기반
- 장점: 앱이 OIDC/JWKS를 몰라도 됨
- 단점: 헤더 신뢰 경계(내부망/ALB 뒤) 설계 필요
패턴 B: 앱에서 JWT 검증, ALB는 라우팅만
- 장점: API/모바일/서버 간 통일된 Bearer 인증
- 단점: JWKS 캐시/회전/스큐 등 앱에서 구현 필요
둘을 섞고 싶다면 “경로 기반 분리”가 안전합니다.
/web/*는 ALB OIDC/api/*는 Bearer JWT
9) 마무리: 10분 내 진단 루틴
마지막으로, 401 반복을 빠르게 끝내는 루틴을 요약합니다.
- ALB 액세스 로그로 elb_status_code vs target_status_code 확인(주체 분리)
curl -v로 리다이렉트/Set-Cookie/WWW-Authenticate 확인- Pod 직통(port-forward)과 ALB 경유를 비교해 헤더 유실 여부 확인
- JWT라면
iss/aud/exp/kid를 로컬에서 검증(샘플 코드 활용) - OIDC라면 redirect URI, issuer, secretName, listener rule을 AWS CLI로 재확인
401은 “서버가 죽어서”가 아니라 “서버가 의도적으로 거부해서” 생깁니다. 따라서 누가 거부했는지를 먼저 확정하고, 그 계층의 입력값(토큰/헤더/쿠키/리다이렉트)을 역추적하면 대부분 짧은 시간 안에 해결됩니다.
추가로 ALB 레이어에서의 다른 장애 패턴(502/504, idle timeout, health check 등)까지 함께 점검이 필요하다면 아래 글도 같이 보면 원인 분리가 빨라집니다: AWS ALB 502·504 난사 - 원인별 해결 체크리스트