4. 인증/권한(RBAC) 붙이기: auth_request + 헤더 계약 + 경로별 정책
2025. 12. 28. 20:55
TechNote · NGINX · 04

4. 인증/권한(RBAC) 붙이기: auth_request + 헤더 계약 + 경로별 정책

auth_request와 ‘헤더 계약’을 활용해, 경로별 RBAC(권한 분리)를 붙이는 실무 패턴을 정리해 드립니다.

이 글의 결론
포털/대시보드에서 RBAC를 “앱에서 if문”으로만 처리하면 유지보수가 급격히 어려워진다.
NGINX에서 인증 통과 여부(2xx/401/403) + 사용자/그룹 헤더 계약을 먼저 고정해두면, React/Node는 “신뢰된 사용자 정보”만 받아 훨씬 단순해진다. (auth_request 기반)

auth_request 기반 RBAC 흐름(개념도)
auth_request 기반 RBAC 흐름(개념도)


1) RBAC를 NGINX에서 하는 이유

  • 정책의 단일화: 서비스가 늘어도 “접속 정책”은 NGINX 한 곳에서 통제
  • 감사/추적: 접근 차단(403)도 동일한 로깅/알림 체계로 남김
  • 코드 단순화: 앱(React/Node)은 “권한 계산”보다 “업무 로직”에 집중
  • 장애 격리: IdP/인증이 이상해도 영향 범위를 프록시 레벨에서 제어

2) 구조

 1) 사용자가 보호 경로 접근
→ 2) NGINX가 auth subrequest(/_auth)로 인증 확인
→ 3) 2xx면 통과, 401/403이면 차단
→ 4) 통과 시 사용자/그룹 헤더를 업스트림에 전달

✔️ 핵심 포인트는 “인증 결과는 subrequest의 HTTP 코드(2xx/401/403)로 결정”되고,
✔️ “사용자/그룹/역할은 헤더 계약으로 고정해서 Upstream에 넘긴다”는 점입니다.


3) 핵심 개념: auth_request + auth_request_set

auth_request는 “서브요청(subrequest)”을 던져서 그 결과로 접근을 허용/차단합니다. 서브요청이 2xx면 허용, 401/403이면 차단(해당 코드로 응답)입니다.

그리고 auth_request_set로 “인증 게이트웨이가 준 헤더”를 NGINX 변수로 받아서, 업스트림(React/Node)에 전달할 수 있습니다.

예시: /_auth 서브요청 + 사용자/그룹 헤더를 변수로 저장

# /etc/nginx/conf.d/40-auth-request.conf (예시)

# 1) 인증 체크 서브요청 엔드포인트
location = /_auth {
  internal;
  proxy_pass http://127.0.0.1:4180/oauth2/auth;

  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
}

# 2) 서브요청 응답 헤더를 변수로 추출(헤더명은 환경에 맞게 조정)
auth_request_set $auth_user   $upstream_http_x_auth_request_user;
auth_request_set $auth_email  $upstream_http_x_auth_request_email;
auth_request_set $auth_groups $upstream_http_x_auth_request_groups;

⚠️ 운영자 팁(중요)
“RBAC가 안 먹는다”의 80%는 그룹/역할이 어떤 헤더로 오고 있는지 계약이 없어서 생긴다.
일단 로그로 $auth_groups를 찍어 실제 값이 뭔지부터 확인하자.


4) “그룹 → 역할” 매핑(map)으로 경로별 RBAC

이제 $auth_groups(문자열)에 들어있는 그룹을 기준으로 역할을 만들고, /admin, /ops 같은 경로에 정책을 강제합니다.

(A) 그룹 문자열을 역할로 변환(map)

# /etc/nginx/conf.d/50-rbac-map.conf

# auth_groups 예: "admins,operators,readonly" 처럼 들어온다고 가정
# (정규식은 환경에 맞게 튜닝)
map $auth_groups $role {
  default                      "readonly";
  ~*admins                      "admin";
  ~*operators                   "operator";
}

(B) 경로별 정책 강제(403/401 분리)

# server{} 또는 location{} 예시

# 공통 보호 구간
location / {
  auth_request /_auth;
  error_page 401 = /oauth2/sign_in;

  # 업스트림으로 사용자 정보 전달(앱은 이 값만 믿으면 됨)
  proxy_set_header X-Auth-User   $auth_user;
  proxy_set_header X-Auth-Email  $auth_email;
  proxy_set_header X-Auth-Groups $auth_groups;
  proxy_set_header X-Auth-Role   $role;

  proxy_pass http://127.0.0.1:3000;
}

# 관리자 전용 경로
location ^~ /admin/ {
  auth_request /_auth;
  error_page 401 = /oauth2/sign_in;

  # role이 admin 아니면 403
  if ($role != "admin") { return 403; }

  proxy_set_header X-Auth-Role $role;
  proxy_pass http://127.0.0.1:3000;
}

# 운영자/관리자 경로
location ^~ /ops/ {
  auth_request /_auth;
  error_page 401 = /oauth2/sign_in;

  if ($role !~ ^(admin|operator)$) { return 403; }

  proxy_set_header X-Auth-Role $role;
  proxy_pass http://127.0.0.1:3000;
}

왜 401과 403을 분리하나?
401: 로그인/세션 문제(“인증 실패”) → sign_in으로 보내기
403: 로그인은 했는데 권한이 없음(“인가 실패”) → “권한 없음” 안내가 맞음
이 구분이 있어야 운영자/사용자 모두 덜 헷갈린다.


5) 디버깅/검증 커맨드(운영자 필수)

# 1) NGINX 문법 검사
sudo nginx -t

# 2) 헤더/리다이렉트 흐름 확인
curl -kI https://portal.example.com/ | egrep -i 'HTTP/|Location|Set-Cookie'

# 3) 권한 없는 경로(403) 확인
curl -kI https://portal.example.com/admin/ | egrep -i 'HTTP/|Location'

# 4) auth subrequest만 단독으로 확인하고 싶으면(직접 접근은 internal이라 불가)
# → 인증 게이트웨이(/oauth2/auth)를 직접 호출해 202/401 등을 확인
curl -kI https://portal.example.com/oauth2/auth

6) 자주 터지는 포인트(실전 Troubleshooting)

  • 그룹 문자열 형태가 예상과 다름(쉼표/공백/대문자) → map 정규식/파싱 튜닝
  • Host/Proto 헤더가 꼬이면 로그인 리다이렉트가 깨짐 → X-Forwarded-* 확인
  • 403인데 화면이 로그인 페이지로 가면, 401/403 처리(error_page)가 섞인 것
  • 권한 정책을 앱과 NGINX 둘 다에서 하면 “이중 정책”으로 디버깅이 어려움 → 한 곳을 소스로 정하기