TLS를 어디서 끝내느냐는 인증서 위치만 정하는 일이 아니다. 그 지점이 곧 "여기부터는 평문을 본다"와 "여기부터는 앞 구간이 알려 준 클라이언트 IP와 프로토콜을 믿는다"는 신뢰 경계가 되고, 경계를 잘못 그으면 위조한 X-Forwarded-For 한 줄로 관리자 IP 제한이 풀린다. 이 글은 TLS 종료 방식 세 가지를 비교하고, 구간마다 무엇을 믿어야 하는지, 프록시와 WAS 설정을 어떻게 맞추는지 정리한 것이다.

한눈에 보기
| 구간 | 암호화 | 이 구간의 받는 쪽이 믿는 것 | 확인 방법 |
|---|---|---|---|
| 클라이언트 → L4·WAF | TLS (공개 인증서) | 아무것도 믿지 않는다 | 들어온 X-Forwarded-*는 버리거나 덮어쓴다 |
| L4·WAF → Reverse Proxy | TLS 그대로 통과 또는 종료 | L4의 출발지 IP 보존 방식 | PROXY protocol 또는 원본 IP 유지 설정 |
| Reverse Proxy → WAS | 재암호화 또는 mTLS | 프록시가 쓴 헤더만 | 프록시 IP 목록 + 백엔드 인증서 검증 |
| WAS → DB | DB 드라이버 TLS | WAS 계정 | 계정·접속 IP 제한 |

그림의 핵심은 두 가지다. TLS를 끝내는 Reverse Proxy가 클라이언트 정보를 "다시 쓰는" 유일한 곳이고, WAS는 그 프록시에서 온 요청만 받는다.
TLS를 어디서 끝낼지: 세 가지 방식
통과(passthrough)
- L4 장비나 프록시가 TLS를 풀지 않고 바이트만 넘긴다. 인증서와 개인키는 WAS에만 있다.
- 장점은 프록시가 평문을 보지 않는다는 것이다. 단점은 프록시가 경로·헤더를 볼 수 없어 WAF 검사, 경로별 라우팅, 헤더 정리를 할 수 없다는 것이다.
- 클라이언트 IP는 TCP 출발지로만 전달된다. L4가 출발지를 바꾸면(SNAT) PROXY protocol 같은 별도 수단이 필요하다.
종료(termination)
- 프록시에서 TLS를 풀고 뒤로는 평문 HTTP로 보낸다. 설정이 가장 단순하고 흔하다.
- 프록시와 WAS 사이 구간이 평문이 된다. 같은 망의 다른 서버가 이 구간을 엿보거나 WAS 포트에 직접 요청을 넣을 수 있다면, 그 구간은 신뢰 경계 안이 아니다.
재암호화(re-encryption)
- 프록시에서 풀어 검사·라우팅을 한 뒤, 뒤 구간을 다시 TLS로 감싼다.
- 프록시가 WAS 인증서를 검증하지 않으면 암호화만 있고 상대 확인은 없다. 여기에 WAS가 프록시의 클라이언트 인증서를 요구하면(mTLS) 양쪽이 서로를 확인한다.
선택 기준은 "프록시 뒤 구간을 누가 만질 수 있는가"다. 프록시와 WAS가 전용 구간에 있고 WAS 포트가 프록시 IP에만 열려 있으면 종료 방식으로 충분할 수 있다. 여러 팀의 서버가 같은 망을 쓰거나 개인정보가 오가면 재암호화와 mTLS를 기본으로 둔다.
신뢰 경계는 "누가 헤더를 쓸 수 있나"로 정한다
TLS를 프록시에서 끝내면 WAS는 진짜 클라이언트를 직접 보지 못한다. WAS가 보는 TCP 상대는 프록시이고, 원래 클라이언트 IP와 https 여부는 헤더로 전달받는다.
X-Forwarded-For: 거쳐 온 클라이언트·프록시 IP 목록X-Forwarded-Proto: 클라이언트가 쓴 프로토콜(https)X-Forwarded-Host: 클라이언트가 요청한 호스트Forwarded: 위 세 가지를 한 헤더로 표준화한 형식(RFC 7239). 예:Forwarded: for=192.0.2.43;proto=https
문제는 이 헤더를 클라이언트도 보낼 수 있다는 점이다. 그래서 규칙은 하나다. 헤더는 내가 아는 프록시가 쓴 부분만 믿는다. 신뢰 경계 바깥에서 온 값은 덮어쓰거나 버린다.
X-Forwarded-For는 각 프록시가 받은 요청의 출발지를 오른쪽에 덧붙인다. 클라이언트가 X-Forwarded-For: 10.0.0.1을 미리 넣어 보내면 프록시를 지난 뒤 값은 10.0.0.1, 203.0.113.7이 된다. 맨 왼쪽 값을 클라이언트 IP로 쓰면 공격자가 고른 IP가 그대로 들어간다. 올바른 해석은 오른쪽부터 읽으면서 신뢰하는 프록시 IP를 하나씩 걷어내고, 처음 나오는 신뢰하지 않는 IP를 클라이언트로 본다는 것이다.
앞단 프록시: 들어온 헤더를 덮어쓴다
인터넷에서 처음 요청을 받는 프록시는 클라이언트가 보낸 X-Forwarded-*를 믿을 이유가 없다. 이어 붙이지 말고 덮어쓴다. nginx 예시는 이렇다.
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/tls/app.crt;
ssl_certificate_key /etc/nginx/tls/app.key;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass https://was_backend;
# 첫 프록시는 이어 붙이지 않고 덮어쓴다 ($proxy_add_x_forwarded_for 금지)
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Forwarded "";
proxy_set_header Host $host;
}
}
$proxy_add_x_forwarded_for는 들어온 값 뒤에 덧붙이므로, 앞에 다른 신뢰 프록시가 없는 첫 프록시에서는 쓰지 않는다.- 쓰지 않는
Forwarded헤더도 빈 값으로 지운다. WAS가 그 헤더를 우선해서 읽는 경우가 있기 때문이다. - 앞에 L4나 CDN이 하나 더 있으면, 그 장비 IP를 nginx
realip모듈에 등록하고(set_real_ip_from,real_ip_header,real_ip_recursive on) 거기서 얻은$remote_addr를 쓴다.
WAS: 아는 프록시만 믿는다
WAS 쪽 설정은 "어느 IP에서 온 헤더를 믿을지"를 좁히는 일이다. Spring Boot 내장 Tomcat이면 이렇게 둔다.
server:
forward-headers-strategy: native
tomcat:
remoteip:
# 정규식. 실제 Reverse Proxy 두 대만 적는다
internal-proxies: "10\\.0\\.1\\.11|10\\.0\\.1\\.12"
remote-ip-header: x-forwarded-for
protocol-header: x-forwarded-proto
native는 Tomcat의RemoteIpValve를 켠다. 요청이internal-proxies에 맞는 IP에서 왔을 때만 헤더로getRemoteAddr(),isSecure(), 스킴을 바꾼다.internal-proxies기본값은 사설 대역 전체를 포함한다. 그대로 두면 내부망의 아무 서버나 헤더를 위조해 클라이언트 IP를 바꿀 수 있으므로, 프록시 IP만 남긴다.framework값은 Spring의ForwardedHeaderFilter로 헤더를 반영한다. 이 방식은 출발지 IP로 거르지 않으므로 앞단 프록시가 헤더를 반드시 덮어쓰고, WAS 포트가 프록시에만 열려 있을 때만 쓴다.- 외장 Tomcat이면
server.xml의RemoteIpValve에internalProxies를, Apache HTTP Server가 앞단이면mod_remoteip의RemoteIPHeader,RemoteIPInternalProxy를 같은 원칙으로 맞춘다.
헤더 설정만으로는 부족하다. 방화벽이나 보안 그룹에서 WAS 포트를 프록시 IP에만 연다. 그래야 "프록시를 건너뛰고 WAS에 직접 헤더를 넣는" 경로가 사라진다.
프록시 뒤 구간: 재암호화와 mTLS
재암호화를 켰다면 프록시가 WAS 인증서를 검증하게 해야 한다. nginx는 기본값이 검증하지 않음이다.
location / {
proxy_pass https://was_backend;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/tls/internal-ca.crt;
proxy_ssl_name was.internal;
proxy_ssl_server_name on;
# mTLS: WAS가 프록시를 확인할 수 있도록 클라이언트 인증서 제시
proxy_ssl_certificate /etc/nginx/tls/proxy-client.crt;
proxy_ssl_certificate_key /etc/nginx/tls/proxy-client.key;
}
- 내부 구간 인증서는 내부 CA로 발급한다. 공개 인증서를 내부 호스트 이름에 쓰려다 이름 검증을 끄는 일이 생기지 않게 한다.
- WAS는 내부 CA가 서명한 클라이언트 인증서만 받도록 설정한다. 이러면 WAS 포트에 닿더라도 프록시의 키가 없는 서버는 TLS 연결부터 실패한다.
- mTLS를 쓰면 "프록시 IP 목록"과 "프록시 인증서" 두 가지로 프록시를 확인하게 된다. 둘 중 하나만 뚫려서는 헤더 위조가 통하지 않는다.
HSTS와 인증서 관리
HSTS
Strict-Transport-Security는 TLS를 끝내는 곳, 즉 클라이언트가 실제로 보는 응답을 만드는 프록시에서 붙인다. WAS가 평문으로 응답을 받는 구조에서는 WAS가 붙이는 것을 잊기 쉽다.includeSubDomains는 하위 도메인 전체가 HTTPS를 지원할 때만 넣는다. 아직 HTTP만 되는 하위 서비스가 있으면 접속이 끊긴다.- 브라우저 사전 등록(preload)은 되돌리기 어렵다. 도메인 전체 HTTPS가 확정된 뒤에 검토한다.
인증서
- TLS 종료 지점이 늘어날수록 개인키 사본도 늘어난다. 공개 인증서는 종료 지점에만 두고, 서버마다 복사해 두지 않는다.
- 만료일을 모니터링 대상에 넣는다. 만료 30일 전과 7일 전에 알림을 받게 하고, 갱신 절차(발급, 배포, 재시작 없는 리로드)를 문서로 남긴다.
- 내부 CA 인증서도 만료된다. 내부 구간의 만료가 더 늦게 발견되므로 같은 목록으로 관리한다.
무엇을 어디서 막는가
| 통제 | 두는 곳 | 없으면 생기는 일 |
|---|---|---|
들어온 X-Forwarded-* 덮어쓰기 |
첫 Reverse Proxy | 위조한 IP로 관리자 IP 제한·접속 로그를 속임 |
| 신뢰 프록시 IP 목록 | WAS (internal-proxies) |
내부망 아무 서버나 클라이언트 IP를 바꿈 |
| WAS 포트 접근 제한 | 방화벽·보안 그룹 | 프록시를 건너뛰고 WAS에 직접 요청 |
X-Forwarded-Proto 반영 |
프록시 + WAS | 리다이렉트가 http://로 나가고 쿠키 Secure가 빠짐 |
X-Forwarded-Host 덮어쓰기 |
첫 Reverse Proxy | 비밀번호 재설정 메일 링크가 공격자 도메인으로 만들어짐 |
| 백엔드 인증서 검증·mTLS | 프록시 ↔ WAS | 내부 구간에서 가짜 WAS나 가짜 프록시가 끼어듦 |
| HSTS | TLS 종료 지점 | 첫 접속을 HTTP로 유도하는 공격에 노출 |
자주 하는 실수
X-Forwarded-For의 맨 왼쪽 값을 클라이언트 IP로 쓴다. 맨 왼쪽은 클라이언트가 마음대로 넣을 수 있는 자리다.- 첫 프록시에서
$proxy_add_x_forwarded_for를 쓴다. 위조된 값이 그대로 남아 뒤에서 오른쪽부터 읽어도 신뢰 목록 계산이 꼬인다. internal-proxies기본값을 그대로 둔다. 사설 대역 전체가 프록시로 취급되어, 내부망의 다른 서버가 헤더를 위조할 수 있다.- 재암호화하면서 백엔드 인증서 검증을 끈다. 암호화는 되지만 상대가 누구인지는 확인하지 않는다.
X-Forwarded-Proto를 넘기지 않는다. WAS가 요청을 HTTP로 알고http://리다이렉트를 만들어 루프가 생기거나, 세션 쿠키에Secure가 빠진다.
점검표
- TLS 종료 지점이 구성도에 표시되어 있고, 구간마다 평문·TLS·mTLS 여부가 적혀 있다
- 첫 Reverse Proxy가
X-Forwarded-For,X-Forwarded-Proto,X-Forwarded-Host를 덮어쓴다 - 클라이언트가 보낸
Forwarded헤더를 지우거나 덮어쓴다 - WAS의 신뢰 프록시 목록에 실제 프록시 IP만 있다
- WAS 포트는 방화벽에서 프록시 IP에만 열려 있다
- 위조한
X-Forwarded-For를 붙여 요청했을 때 접속 로그에 실제 IP가 찍히는 것을 확인했다 - 재암호화 구간에서 프록시가 백엔드 인증서를 검증한다
- HSTS 헤더가 클라이언트 응답에 붙고,
includeSubDomains적용 범위를 확인했다 - 공개·내부 인증서의 만료일이 모니터링되고 알림이 온다
- 인증서 갱신 절차가 문서로 있고 한 번 이상 실제로 해 봤다
TLS 종료 지점은 암호를 푸는 곳이자 클라이언트 정보를 새로 쓰는 곳이므로, 그 한 곳에서 헤더를 덮어쓰고 뒤의 모든 구간은 그 지점만 믿도록 설정과 네트워크를 함께 묶어야 한다.
댓글 0개
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.