Stan 기술블로그

NPM 리버스 프록시 기초

TL;DR 사수 과제로 Nginx Proxy Manager(NPM)를 다루며 리버스 프록시의 개념부터 접근제어 두 축(IP·경로)까지 실습했다. whoami를 뒤에 세운 “거울 백엔드”로 두면 프록시가 뒤로 뭘 넘기는지 눈으로 보이고, X-Forwarded-For가 왜 필요한지 자연스럽게 이해된다. 프록시는 요청에 드러난 것만(도메인·경로·헤더) 본다. 토큰 안 정보(예: 어느 렐름에서 로그인)는 앱의 몫. 사수가 말한 “렐름 단위 접근제어의 범위”가 정확히 이 경계선이었다.


배경

사수가 이런 과제를 줬다.

“특정 도메인으로 들어올 때 접근 방식을 NPM에서 다뤄봐. 도메인 단위가 아니라 렐름(realm) 단위로 접근제어할 수 있는 범위까지 공부해봐.”

용어부터 막혔다. NPM? 렐름 단위 접근제어? 하나씩 풀어봐야 했다.

리버스 프록시를 이렇게 이해했다 — 건물 안내 데스크. 바깥에서 오는 요청은 전부 안내 데스크로 온다. 데스크는 “어느 도메인 찾나?”를 보고 뒤에 있는 진짜 서버로 안내한다. 손님(브라우저)은 뒤에 서버가 몇 대인지, 뭐가 있는지 알 필요가 없다. 정문은 하나, 안내만 잘하면 됨.

사용자 ──"auth.example.com 달라"──▶ [안내 데스크 / 리버스 프록시] ──▶ 실제 서버

Nginx Proxy Manager(NPM) 는 이 안내 데스크를 웹 GUI로 쉽게 만들게 해주는 도구다. nginx 설정 파일을 손으로 안 짜도, 클릭 몇 번으로 “이 도메인 오면 저 서버로” 규칙을 만들 수 있다.

이 글에서 남기고 싶은 건 두 가지다.

  1. whoami 같은 거울 백엔드는 프록시의 동작을 눈으로 보게 해준다 — 이론이 아니라 헤더로 확인하는 학습법.
  2. 프록시가 “볼 수 있는 것”의 경계선이 곧 “렐름 단위 접근제어의 범위”였다. 이 경계 위에서만 프록시가 렐름을 제어할 수 있다.

실습 환경 — 운영 서버 위에 테스트 얹기

운영 중인 서버(이미 여러 서비스가 도는)에 테스트 NPM을 설치하려니 첫 번째 교훈이 왔다.

NPM은 기본적으로 80/443/81 포트를 먹으려 한다. 그런데 운영 서비스가 이미 그 포트를 쓰고 있을 수 있다.

시작은 항상 “뭘 건드리면 안 되는지” 파악부터다.

# 실제로 호스트가 열어놓은 포트 확인
sudo ss -tlnp

# 도커 컨테이너들의 포트 매핑 확인
docker ps --format "table {{.Names}}\t{{.Ports}}"

여기서 알게 된 것: docker ps에서 9000/tcp처럼 IP 없이 포트만 있는 건 컨테이너 내부 전용이라 호스트와 안 겹친다. 0.0.0.0:8080->8080처럼 0.0.0.0:이 붙은 것만 실제 호스트 포트를 점유한다.

포트 충돌을 피하려고 테스트 NPM은 대체 포트로 격리했다.

services:
  npm-test:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '8090:80'              # 프록시 HTTP
      - '8453:443'             # 프록시 HTTPS
      - '127.0.0.1:8181:81'    # 관리자 UI — localhost만
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

관리자 UI(81)는 127.0.0.1에만 묶었다. 반쯤 설정된 프록시 관리자가 인터넷에 노출되면 안 되니까. 접속은 SSH 터널로.

# 내 노트북에서
ssh -L 8181:127.0.0.1:8181 사용자@서버
# → 브라우저 http://localhost:8181

whoami — 프록시 뒤 서버가 보는 값의 실체

라우팅을 연습하려면 뒤에 서버가 있어야 하는데, 진짜 앱은 복잡하고 위험하다. 그래서 연습용 백엔드로 traefik/whoami를 썼다.

whoami는 딱 한 가지만 한다. 자기한테 온 요청을 전부 화면에 그대로 뱉어준다. 리버스 프록시가 뒤로 뭘 어떻게 넘기는지 눈으로 보는 거울이다.

  whoami:
    image: traefik/whoami
    restart: unless-stopped

같은 docker-compose에 넣으면 같은 네트워크라 컨테이너 이름으로 프록시할 수 있다(호스트 포트도 필요 없음). NPM에서 Proxy Host 하나 만들고:

  • Domain: whoami.test.local
  • Forward Hostname: whoami, Port: 80

요청을 쏴본다.

curl -H "Host: whoami.test.local" http://127.0.0.1:8090
Hostname: 322fcb1f40dc
RemoteAddr: 172.20.0.2:38868      ← whoami가 본 "날 부른 사람" = 프록시(NPM)
Host: whoami.test.local           ← 프록시가 라우팅 판단에 쓴 도메인
X-Forwarded-For: 172.20.0.1       ← 프록시가 "원래 요청자는 이 IP였어"라고 적어준 값
X-Forwarded-Proto: http

여기서 뒤에 나올 접근제어의 핵심이 하나 나온다.

리버스 프록시 뒤의 서버는 RemoteAddr로 원래 요청자가 아니라 프록시를 본다. 진짜 요청자 IP는 프록시가 X-Forwarded-For 헤더에 담아 넘겨준다.

whoami를 하나 더(whoami2) 띄우고 도메인을 갈라봤다.

curl -s -H "Host: whoami.test.local"  http://127.0.0.1:8090 | grep Hostname
# Hostname: 322fcb1f40dc
curl -s -H "Host: whoami2.test.local" http://127.0.0.1:8090 | grep Hostname
# Hostname: 7d6be2c4c16d   ← 다른 컨테이너

같은 정문(8090)으로 들어왔는데 도메인만 보고 서로 다른 뒷단으로 갈렸다. 이게 리버스 프록시의 심장이다.


커스텀 에러 페이지 — Advanced 탭의 첫 사용

NPM의 Advanced(고급) 설정 — 이 버전에선 톱니바퀴 아이콘 — 에 nginx 설정을 직접 넣어 커스텀 에러 페이지를 만들 수 있다.

error_page 502 503 504 /maintenance_50x.html;

location = /maintenance_50x.html {
    internal;
    default_type text/html;
    return 200 '<h1>서비스 점검 중이다</h1>';
}

뒷단 컨테이너를 docker stop 하고 요청하면, 기본 502 대신 이 페이지가 뜬다. Advanced 탭은 뒤에서 나올 경로 단위 접근제어에도 쓰이는 도구라 손에 익혀둘 가치가 있었다.


접근제어 두 축 — IP(Access List) vs 경로(Custom Location)

접근제어에는 축이 두 개다.

축 하나. IP 화이트리스트 (Access List)

NPM의 Access List는 손님 명단이다. 명단에 있는 IP만 통과, 나머지는 403.

명단을 만들고 Proxy Host에 붙인 뒤, 응답 코드만 확인해봤다.

curl -s -o /dev/null -w "%{http_code}\n" -H "Host: whoami.test.local" http://127.0.0.1:8090
# 명단에 없을 때 → 403
# 우리 IP를 명단에 추가하면 → 200

403 → 200. 이게 “특정 IP만 들여보낸다”의 실체다. 사수 과제의 “특정 외부만 접근 가능하게”가 바로 이거였다.

축 둘. 경로(렐름) 단위 — 이번 여정의 하이라이트

여기서 과제의 진짜 목표가 나온다. 지금까지는 도메인 전체를 열고 닫았다. 이번엔 같은 도메인 안에서 경로별로 다르게 제어한다.

왜 “렐름 단위”라는 말이 나왔을까. Keycloak 같은 인증 서버는 렐름(realm, 인증 경계 단위)을 URL 경로로 노출한다.

auth.example.com/realms/internal-realm/...
auth.example.com/realms/public-realm/...
                   └── 렐름 이름이 경로에 그대로 박혀있음

렐름이 URL에 보인다는 건, 리버스 프록시가 경로를 보고 렐름별로 접근제어를 걸 수 있다는 뜻이다. NPM의 Custom Locations로 특정 경로에만 규칙을 건다.

# /realms/public-realm → 아무나 (규칙 없음, 기본 열림)
# /realms/internal-realm → 사내 IP만
location /realms/internal-realm {
    allow 10.0.0.0/8;   # 사내 대역(예시)
    deny all;
    # ... proxy_pass ...
}
curl -s -o /dev/null -w "public : %{http_code}\n" ... /realms/public-realm
# public : 200
curl -s -o /dev/null -w "internal: %{http_code}\n" ... /realms/internal-realm
# internal: 403  (사내 IP 아님)

같은 도메인인데 경로에 따라 한쪽은 열리고 한쪽은 막혔다. 이게 경로(렐름) 단위 접근제어다. 보안 정책이 바뀌어 “이 렐름은 이제 외부에도 열자”가 되면, 해당 경로의 Access List만 바꾸면 된다.

참고: 여기까진 whoami 백엔드로 “개념”만 증명한 것. 실제 Keycloak을 붙였을 땐 이 Custom Location 방식이 오히려 앱을 죽였다. 그 이야기는 다음 편(Keycloak 실전 삽질 3종 편)에서.


진짜로 배운 것

첫째. 거울 백엔드는 프록시의 “행동”을 이론이 아니라 헤더로 보게 해준다. whoami 같은 백엔드가 없었다면 X-Forwarded-For가 왜 필요한지, RemoteAddr가 왜 프록시로 보이는지, 도메인 라우팅이 실제로 어떻게 갈리는지 — 다 문서로만 읽고 넘어갔을 것이다. 거울 백엔드는 학습 도구지만 나중에 실제 앱에서 헤더 문내가 터졌을 때도 첫 진단 도구가 된다.

둘째. 프록시가 볼 수 있는 것의 경계선이 곧 접근제어의 범위다.

프록시가 보는 것 프록시가 못 보는 것
도메인 (Host 헤더) “이 사용자가 어느 렐름에서 로그인했나”
경로 (URL) → 토큰(JWT) 안에 있음
IP, 헤더 → 앱만 안다

그래서 렐름 단위 제어는 두 경우로 갈린다.

  • 인증 서버 자신(auth.example.com) — 렐름이 URL 경로에 드러남 → 프록시가 제어 가능
  • 앱(app.example.com) — 어느 렐름에 붙는지는 앱의 OIDC 설정과 토큰 안에 있음 → URL만으론 구분 불가 → 프록시가 아니라 앱이 처리

사수가 말한 “렐름 단위로 접근제어할 수 있는 범위”가 정확히 이 경계선이었다. 굳이 앱 도메인에서 프록시가 렐름을 보게 하려면 토큰을 까서 읽는 auth_request / oauth2-proxy 같은 훨씬 무거운 세팅이 필요하고, 보통은 앱이 직접 한다.


체크리스트 — 다음번 리버스 프록시 붙일 때

  1. 운영 서버 위에 테스트 얹을 땐 포트 충돌부터 — sudo ss -tlnp, docker ps로 확인. 0.0.0.0: 붙은 것만 호스트 점유.
  2. 관리자 UI는 localhost + SSH 터널 — 반쯤 설정된 프록시 관리자가 인터넷에 노출되면 안 된다.
  3. 거울 백엔드(traefik/whoami)를 붙여라 — 프록시가 뒤로 뭘 넘기는지 헤더로 보인다. 헤더 문제 첫 진단 도구.
  4. 접근제어 두 축을 분리해서 생각한다 — 도메인 전체는 Access List(IP), 경로별은 Custom Location. 어느 축으로 할지 먼저 정한다.
  5. 프록시가 볼 수 있는 것만 프록시가 제어한다 — 토큰 안 정보는 앱의 몫. 렐름이 URL에 있으면 O, 토큰에 있으면 X.

참고

  • 대상 도구: Nginx Proxy Manager(NPM, jc21/nginx-proxy-manager), traefik/whoami
  • 배포 환경: 운영 서버 위에 테스트 컨테이너 격리 (포트 8090/8453/8181)
  • 후속 예고: 이 원리를 실제 Keycloak 앞단에 적용한 삽질 3종(Custom Location 죽음·볼륨 소멸·슬래시 우회) — Keycloak 실전 삽질 3종 편에서.

리버스 프록시·Keycloak 카테고리의 글