개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › 네트워크 › 웹 서버 앞의 장비

프록시와 리버스 프록시

프록시 뒤의 서버는 누가 요청했는지 어떻게 아나

프록시는 요청을 대신 받아 다른 곳으로 넘겨 주는 중간 서버입니다. 클라이언트 쪽에 서서 클라이언트를 대신하면 포워드 프록시, 서버 쪽에 서서 서버를 대신하면 리버스 프록시입니다. 어느 쪽이든 서버와 실제로 연결을 맺는 것은 프록시라서, 서버가 보는 상대 주소는 클라이언트가 아니라 프록시의 주소가 됩니다. 그래서 프록시가 X-Forwarded-For 같은 헤더로 원래 정보를 전해 주는데, 이 헤더는 클라이언트도 마음대로 써넣을 수 있습니다.

실행으로 대조 · 루프백 주소 셋(127.0.0.10·20·30)에 Node 24로 만든 프록시와 서버를 띄우고 Node http·https와 curl 8.21로 42가지 조합을 실행해 대조. 프록시는 직접 만든 시험용이며 nginx 같은 제품의 동작은 문서 기준

이 실습에 나오는 개념

포워드 프록시
클라이언트 쪽에 서서 클라이언트의 요청을 대신 바깥 서버로 보내 주는 중간 서버다. 클라이언트는 프록시의 주소를 설정해 두고 일부러 그곳을 거치며, 서버가 보는 연결 상대는 클라이언트가 아니라 프록시가 된다. 회사나 학교에서 밖으로 나가는 요청을 한곳에 모아 기록하거나 막을 때, 또는 클라이언트의 주소를 가릴 때 쓴다. 서버 쪽에 서서 서버를 대신하는 리버스 프록시와는 누구를 대신하는지가 반대다.
리버스 프록시
클라이언트의 요청을 대신 받아 내부의 서버로 전달하고 그 응답을 클라이언트에 돌려주는 서버다. 클라이언트는 실제 서버를 직접 알지 못한다. 여러 서버로의 요청 분산, TLS 처리, 캐싱, 내부 서버 보호 등의 역할을 한다.
X-Forwarded-For
프록시가 요청을 넘길 때 원래 클라이언트의 주소를 적어 주는 요청 헤더다. 프록시 뒤의 서버가 보는 연결 상대는 프록시이므로, 실제 클라이언트의 주소는 이 헤더로만 전해진다. 프록시를 여러 대 거치면 각 프록시가 자신에게 연결한 상대의 주소를 쉼표로 뒤에 덧붙인다. 요청 헤더라서 클라이언트도 마음대로 써넣을 수 있으므로, 서버는 자신이 관리하는 믿을 수 있는 프록시가 붙인 값(뒤쪽 값)만 써야 하고 프록시 뒤에 있지 않다면 읽지 말아야 한다. 표준이 아닌 관례이며, 같은 일을 하는 표준 헤더로 Forwarded가 있다.
X-Forwarded-Proto와 TLS 종료
TLS 종료는 리버스 프록시나 로드밸런서가 클라이언트의 HTTPS 연결을 받아 암호화를 풀고, 뒤의 서버에는 암호화하지 않은 HTTP로 넘기는 구성이다. 인증서 관리와 암호화 연산을 한곳에 모을 수 있다. 이때 뒤의 서버는 HTTP 연결만 보게 되므로, 프록시가 X-Forwarded-Proto: https 헤더로 클라이언트가 원래 쓴 프로토콜을 알려 준다. 서버가 이 헤더를 읽지 않으면 리다이렉트할 주소를 http://로 만들거나 보안 쿠키 조건을 잘못 판단할 수 있다. X-Forwarded-For와 마찬가지로 믿을 수 있는 프록시가 붙인 값만 써야 한다.
HTTPS와 TLS
HTTPS는 HTTP 통신을 TLS로 암호화한 것이다. TLS는 통신 내용을 제3자가 볼 수 없게 하고(기밀성), 중간에서 바꿀 수 없게 하며(무결성), 접속한 서버가 진짜인지 인증서로 확인한다(인증). SSL은 TLS의 전신인 프로토콜로, 지금은 폐기되어 실제로는 TLS만 쓰이지만 관습적으로 SSL이라는 이름이 함께 불린다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 클라이언트(203.0.113.7)가 https://shop.example/whoami를 요청합니다. shop.example이라는 이름이 가리키는 곳은 실제로는 리버스 프록시입니다. 클라이언트는 따로 설정한 것이 없고, 지금 연결한 상대를 서버라고 여깁니다. 그 뒤에 진짜 서버가 따로 있다는 것을 알지 못합니다. 이 클라이언트는 요청에 X-Forwarded-For: 198.51.100.99 헤더를 직접 써넣었습니다. 요청 헤더는 보내는 쪽이 마음대로 적을 수 있는 글자입니다.
  2. 리버스 프록시가 HTTPS 암호화를 풀고, 내부망의 진짜 서버(10.0.0.9)에 새 TCP 연결을 맺어 요청을 HTTP로 넘깁니다. 이 프록시는 자신에게 연결한 상대의 주소(203.0.113.7)를 X-Forwarded-For에 적어 넘깁니다. 이미 값이 들어 있으므로 그 뒤에 쉼표로 이어 덧붙입니다. 클라이언트가 원래 쓴 프로토콜도 X-Forwarded-Proto: https로 함께 적습니다.
  3. 서버가 연결 상대 주소를 확인합니다. 10.0.0.5, 프록시의 주소입니다. 서버와 TCP 연결을 맺은 것은 프록시이기 때문입니다. 연결만 봐서는 실제 클라이언트가 누구인지 알 수 없습니다. 받은 연결도 암호화되지 않은 HTTP입니다.
  4. 서버가 받은 요청의 X-Forwarded-For는 "198.51.100.99, 203.0.113.7"입니다. 앞의 값은 클라이언트가 써넣은 것이고, 뒤의 값은 프록시가 덧붙인 것입니다. 헤더만 봐서는 둘을 구별할 수 없고, 위치로만 알 수 있습니다. X-Forwarded-Proto: https도 함께 왔습니다.
  5. 서버는 X-Forwarded-For의 맨 앞 값을 클라이언트의 주소로 삼습니다. 결과는 198.51.100.99입니다. 클라이언트가 직접 써넣은 가짜 주소입니다. 서버는 위조된 주소를 믿었습니다.

실행 결과

코드

// 리버스 프록시: HTTPS로 받아 뒤의 서버(10.0.0.9)로 넘긴다
https.createServer(tls, (req, res) => {
    const headers = { ...req.headers };
    const peer = req.socket.remoteAddress;               // 이 프록시에 연결한 상대의 주소
    const old = headers['x-forwarded-for'];
    headers['x-forwarded-for'] = old ? old + ', ' + peer : peer;   // 받은 값 뒤에 덧붙인다
    headers['x-forwarded-proto'] = 'https';                  // 클라이언트가 쓴 원래 프로토콜
    const to = { host: '10.0.0.9', port: 8080, path: req.url };
    http.request({ ...to, method: req.method, headers }, up => {
        res.writeHead(up.statusCode, up.headers);
        up.pipe(res);                                      // 서버의 응답을 클라이언트에 그대로 돌려준다
    }).end();
});

// 서버: 누가 요청했는지 판단한다
http.createServer((req, res) => {
    const xff = req.headers['x-forwarded-for'];
    const client = xff ? xff.split(',')[0].trim()             // 맨 앞 값
                       : req.socket.remoteAddress;
    res.end(client);
});

// 클라이언트
// curl -H 'X-Forwarded-For: 198.51.100.99' https://shop.example/whoami

이 모형의 전제

서로 다른 42가지 조합(직접 연결에는 프록시 설정이 쓰이지 않음)을 이 PC 안에서 실행해 다섯 열이 모두 모형과 같은 것을 확인했습니다. 클라이언트, 프록시, 서버를 이 PC의 루프백 주소 셋(127.0.0.10, 127.0.0.20, 127.0.0.30)에 나눠 띄우고, Node.js 24로 만든 작은 프록시와 서버에 두 가지 클라이언트(Node의 http·https 모듈, curl 8.21)로 요청해 서버가 실제로 본 연결 상대 주소와 헤더를 기록했습니다. 두 클라이언트의 결과는 모든 조합에서 같았습니다. 리버스 프록시는 시험용 인증서로 HTTPS를 받아 서버에는 HTTP로 넘겼고, 포워드 프록시에는 curl의 -x 옵션으로 요청해 요청 줄에 전체 주소가 실려 오는 것을 확인했습니다. 화면의 주소(203.0.113.7, 198.51.100.99, 192.0.2.50)는 문서 예시용으로 정해진 주소이고 10.0.0.x는 사설망 주소이며, 시험에 쓴 루프백 주소를 바꿔 적은 것입니다. 프록시는 시험용으로 직접 만든 것이라 실제 제품의 기본 동작과는 다를 수 있습니다. 예를 들어 nginx는 따로 설정하지 않으면 받은 헤더를 그대로 넘기고, proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for가 "덧붙이기", $remote_addr가 "새로 쓰기"에 해당합니다(nginx 문서 기준이며 실행으로 확인하지 않았습니다). X-Forwarded-For와 X-Forwarded-Proto는 표준이 아니라 널리 쓰이는 관례이고, 같은 정보를 담는 표준 헤더로 Forwarded(RFC 7239)가 있습니다. HTTPS 요청을 포워드 프록시로 보낼 때는 프록시가 내용을 볼 수 없는 통로(CONNECT)만 이어 주므로 헤더를 덧붙일 수 없는데, 이 실습의 포워드 프록시는 HTTP 요청만 다뤘습니다. 서버가 헤더가 없을 때 연결 상대 주소를 대신 쓰는 것은 이 모형에서 정한 규칙입니다. 프록시가 여러 대 이어진 경우와 로드밸런서의 분산은 다루지 않았습니다.

앞 실습: 로드밸런서와 스케일 아웃 · 다음 실습: CDN

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요