Published on

NGINX mTLS+OAuth2로 BFF 인증 우회 막기

Authors

서버 사이드 렌더링(SSR)이나 모바일 앱에서 흔히 쓰는 BFF(Backend For Frontend) 패턴은, 프런트엔드가 직접 여러 백엔드를 호출하지 않고 BFF를 단일 진입점으로 삼아 인증·세션·집계를 책임지게 합니다. 문제는 설계 의도와 달리 BFF 뒤에 있는 내부 API가 외부에서 직접 호출될 수 있는 경로가 남는 순간, 공격자는 BFF가 수행하던 인증·권한·감사 로직을 통째로 우회할 수 있다는 점입니다.

이 글은 그 우회 경로를 NGINX 레벨에서 구조적으로 봉쇄하는 방법을 다룹니다.

  • 1차 방어: mTLSBFF만 내부 API를 호출할 수 있게 네트워크 경계에서 강제
  • 2차 방어: OAuth2(JWT) 검증으로 토큰이 있어도 클라이언트 자격이 맞지 않으면 차단

즉, 네트워크 신원(클라이언트 인증서)애플리케이션 신원(토큰 클레임)을 동시에 요구해 BFF 인증 우회를 막습니다.

관련해서 JWT 검증 과정에서 kid 불일치나 JWKS 캐시 문제로 401이 나는 케이스는 아래 글이 도움이 됩니다.

BFF 인증 우회는 어떻게 발생하나

대표적인 우회 시나리오는 다음과 같습니다.

  1. 사용자는 브라우저에서 BFF에 로그인(세션 쿠키)한다.
  2. BFF는 내부 API를 호출할 때만 필요한 헤더(예: 사용자 식별, 권한 스코프, 감사용 트레이스, 서비스 토큰)를 붙여 호출한다.
  3. 내부 API가 실수로 퍼블릭 인그레스에 노출되거나, 동일 도메인의 다른 경로로 라우팅되어 외부에서 접근 가능해진다.
  4. 공격자는 내부 API 엔드포인트를 직접 호출한다.
    • 내부 API가 BFF가 붙여주던 헤더를 신뢰하거나
    • 내부망에서만 온다고 가정하고 인증을 느슨하게 했다면
    • 실제 권한 체크 없이 데이터가 노출될 수 있다.

이때 “내부 API도 OAuth2 검증을 하면 되지 않나?”라는 질문이 나오는데, 실무에서는 다음 함정이 많습니다.

  • 내부 API가 aud, azp, client_id를 제대로 검증하지 않아 다른 클라이언트 토큰으로도 호출 가능
  • BFF가 세션 기반인데 내부 API는 토큰 기반이라, 내부 API가 토큰 없이도 일부 엔드포인트를 허용
  • 내부 API가 X-User-Id 같은 헤더를 신뢰해 권한 결정을 해버림

그래서 mTLS로 “BFF만 접근 가능”을 먼저 강제하고, 그 위에 JWT 클레임까지 검증해 “BFF가 호출한 것이 맞는지”를 다시 확인하는 구성이 강력합니다.

목표 아키텍처

아래와 같은 흐름을 목표로 합니다.

  • 외부 클라이언트 브라우저/앱BFF만 호출
  • BFFinternal-api를 호출 가능
  • internal-api는 아래를 모두 만족해야 요청을 허용
    • NGINX에서 mTLS 클라이언트 인증서 검증 성공
    • OAuth2 Access Token의 서명 및 클레임 검증 성공

구성 요소 예시

  • Edge NGINX(또는 Ingress) : 외부 트래픽 수신
  • Internal NGINX(또는 서비스 앞단 프록시) : mTLS 종단 + JWT 검증
  • IdP: Keycloak, Auth0, Cognito 등(OIDC)

1차 방어: NGINX mTLS로 BFF만 통과시키기

mTLS는 서버가 클라이언트 인증서를 요구하고, CA 체인으로 유효성을 검증합니다. 여기서 핵심은 “내부 API는 클라이언트 인증서가 없으면 401/403”이 되게 만드는 것입니다.

인증서 전략(실무 팁)

  • 내부 CA를 운영하고, BFF에만 클라이언트 인증서를 발급
  • 인증서 CN 또는 SAN에 서비스 식별자(예: spiffe://... 또는 bff.prod)를 넣고, NGINX에서 매칭
  • 만료/폐기(rotate/revoke) 프로세스를 반드시 포함

NGINX mTLS 설정 예시

아래 예시는 internal-api 앞단 NGINX에서 mTLS를 강제합니다.

server {
  listen 443 ssl;
  server_name internal-api.example.com;

  ssl_certificate     /etc/nginx/tls/server.crt;
  ssl_certificate_key /etc/nginx/tls/server.key;

  # 클라이언트 인증서 검증용 CA
  ssl_client_certificate /etc/nginx/mtls/ca.crt;
  ssl_verify_client on;
  ssl_verify_depth 2;

  # 디버깅/감사용으로 남겨두면 유용
  add_header X-mTLS-Verify $ssl_client_verify always;
  add_header X-mTLS-Client-DN $ssl_client_s_dn always;

  # 검증 실패 시 바로 차단
  if ($ssl_client_verify != SUCCESS) { return 401; }

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Request-Id $request_id;

    # 업스트림(내부 API 앱)
    proxy_pass http://internal-api-upstream;
  }
}

여기까지 적용하면 “외부에서 토큰을 들고 직접 호출”하는 공격의 상당수를 차단할 수 있습니다. 하지만 아직 남는 문제가 있습니다.

  • 내부망에서 인증서를 탈취했거나
  • 같은 클러스터 내부의 다른 워크로드가 인증서를 얻었거나
  • BFF가 아닌 서비스가 실수로 동일 CA로 인증서를 발급받았거나

이런 경우를 대비해 2차 방어가 필요합니다.

2차 방어: OAuth2(JWT) 검증으로 호출 주체를 고정하기

mTLS는 “이 요청은 우리 CA가 발급한 어떤 클라이언트다”까지만 보장합니다. 이제 “그 클라이언트가 BFF인가?”를 애플리케이션 레벨에서 확정해야 합니다.

실무적으로는 다음 중 하나를 선택합니다.

  • NGINX에서 JWT를 직접 검증(상용 NGINX Plus의 auth_jwt 등)
  • NGINX OSS에서는 oauth2-proxy 또는 lua(OpenResty)로 검증
  • 혹은 내부 API 애플리케이션(Spring/Express/FastAPI)이 JWT를 검증하되, mTLS가 통과한 요청만 받도록 구성

여기서는 OSS 환경에서도 많이 쓰는 auth_request 패턴을 예시로 듭니다.

auth_request로 토큰 검증 서비스 붙이기

/oauth2/auth 같은 엔드포인트를 제공하는 검증 컴포넌트를 두고, NGINX가 매 요청마다 서브리퀘스트로 인증을 확인합니다.

server {
  listen 443 ssl;
  server_name internal-api.example.com;

  ssl_certificate     /etc/nginx/tls/server.crt;
  ssl_certificate_key /etc/nginx/tls/server.key;

  ssl_client_certificate /etc/nginx/mtls/ca.crt;
  ssl_verify_client on;

  # mTLS 실패 차단
  if ($ssl_client_verify != SUCCESS) { return 401; }

  location = /_auth {
    internal;

    # 토큰 검증 서비스로 전달
    proxy_pass http://token-introspector/auth;

    # 원 요청의 Authorization 헤더 전달
    proxy_set_header Authorization $http_authorization;

    # mTLS에서 확인한 클라이언트 DN도 함께 전달(로그/검증에 활용)
    proxy_set_header X-mTLS-Client-DN $ssl_client_s_dn;
  }

  location / {
    # 2차 방어: OAuth2/JWT 검증
    auth_request /_auth;

    proxy_set_header Host $host;
    proxy_set_header X-Request-Id $request_id;
    proxy_pass http://internal-api-upstream;
  }
}

검증 서비스는 다음을 확인해야 합니다.

  • 서명 검증(JWKS)
  • iss(issuer), aud(audience) 검증
  • 가능하면 azp 또는 client_id(토큰 발급 주체) 검증
  • 스코프/권한 클레임 검증

특히 BFF 인증 우회 방지의 핵심은 **aud 또는 azp/client_id로 “BFF용 토큰만 허용”**하는 것입니다.

토큰 클레임 검증 예시(개념)

예를 들어 IdP에서 BFF를 client_id=bff로 등록하고, 내부 API 호출용 토큰의 audinternal-api로 고정합니다.

  • 허용 조건
    • audinternal-api 포함
    • azpbff (또는 client_idbff)

이렇게 하면 같은 사용자라도 다른 퍼블릭 클라이언트가 받은 토큰으로는 내부 API가 열리지 않습니다.

mTLS와 OAuth2를 같이 쓸 때의 설계 포인트

1) “사용자 토큰”과 “서비스 토큰”을 분리

BFF는 보통 사용자 세션을 들고 있고, 내부 API 호출은 서비스 간 통신입니다. 내부 API가 사용자 권한이 필요하다면 다음 패턴이 안전합니다.

  • BFF가 사용자 세션을 검증한 뒤
  • 내부 API 호출에는 서비스 토큰(클라이언트 자격)을 사용
  • 사용자 식별은 별도 헤더로 전달하되, 내부 API는 그 헤더를 신뢰하지 말고
    • BFF만 호출 가능(mTLS)
    • BFF만 토큰 발급 가능(azp 검증)
    • 그리고 내부 API에서 추가로 포맷/허용 리스트 검증

2) 내부 API에서 “BFF 전용 헤더”를 화이트리스트로 제한

예: X-User-Id, X-User-Roles 같은 헤더를 그대로 신뢰하면 위험합니다.

  • 헤더 기반 권한은 최소화
  • 필요하면 헤더 값에 대해 서명(HMAC) 또는 JWT 형태로 BFF가 발급한 2차 토큰을 사용

3) 운영에서 자주 터지는 401 원인

  • JWKS 캐시가 오래되어 kid가 바뀐 토큰을 거부
  • aud가 배열인데 문자열로만 비교
  • iss가 환경별로 달라 설정 불일치

이런 케이스는 아래 글처럼 “JWKS 캐시/키 로테이션” 관점에서 점검하면 해결이 빨라집니다.

로컬/스테이징에서 검증하는 방법

curl로 mTLS 확인

클라이언트 인증서 없이 호출하면 실패해야 합니다.

curl -i https://internal-api.example.com/health

BFF 인증서를 붙이면 통과해야 합니다.

curl -i \
  --cert /path/to/bff-client.crt \
  --key /path/to/bff-client.key \
  https://internal-api.example.com/health

토큰까지 함께 확인

mTLS 통과 후에도 토큰이 없거나 잘못되면 차단되어야 합니다.

curl -i \
  --cert /path/to/bff-client.crt \
  --key /path/to/bff-client.key \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  https://internal-api.example.com/v1/orders

여기서 중요한 테스트 케이스는 “다른 클라이언트로 발급받은 토큰”입니다. 그 토큰은 mTLS가 통과하더라도 aud/azp 검증에서 막혀야 합니다.

쿠버네티스(EKS)에서의 배치 팁

  • 인그레스만 믿지 말고, 내부 서비스용 인그레스(또는 내부 NLB)로 분리
  • NetworkPolicy로 BFF 네임스페이스에서만 internal-api 서비스로 접근 가능하게 제한
  • 인증서/키는 Secret로 저장하되, 워크로드에 최소 권한으로 마운트
  • 로테이션 자동화(예: cert-manager) 고려

배포 파이프라인에서 블루-그린으로 프록시 설정을 바꾸는 경우, 인증서/키 배포 타이밍이 엇갈리면 장애가 나기 쉬우니 무중단 배포 전략을 함께 점검하는 것도 좋습니다.

체크리스트: “BFF 인증 우회”를 닫았는지 확인

  • 내부 API 엔드포인트가 퍼블릭 인그레스에 라우팅되지 않는다
  • 내부 API 앞단에서 mTLS가 강제된다(ssl_verify_client on)
  • 클라이언트 인증서의 CN/SAN을 통해 BFF 식별이 가능하다
  • 토큰의 iss/aud/azp(client_id)를 검증한다
  • 내부 API가 헤더 기반 권한 결정을 하지 않거나, 하더라도 BFF만 보낼 수 있게 강제된다
  • 인증서/키/JWKS 로테이션 시나리오에서 401 폭주 없이 운영 가능하다

결론

BFF 패턴의 보안은 “BFF가 인증을 잘한다”만으로 완성되지 않습니다. BFF 뒤 API가 직접 호출 가능한 순간 인증은 우회됩니다. 이를 막는 가장 실전적인 접근은 다음 조합입니다.

  • NGINX mTLS로 BFF만 네트워크 레벨에서 통과
  • OAuth2(JWT) 검증으로 토큰의 발급 주체와 대상(aud/azp)을 고정

이중으로 잠그면, 설령 내부 API URL이 노출되거나 라우팅이 잘못되어도 “토큰만으로는 안 되고, 인증서만으로도 안 되는” 구조가 되어 우회 난이도가 급격히 올라갑니다.