Loading the catalog…
Loading the catalog…
개발 서버에서만 열린 API, 로컬에서 호출하려면 알아야 할 것들 0. 들어가며 회사에서 외부 API 연동을 테스트해야 하는 일이 생겼습니. 그런데 해당 API는 내 로컬 PC에서 호출할 수 없고, 개발 서버에서만 접속이 가능한 상태였습니다. 개발 서버에 코드를 배포해서 테스트하는 방법도 있었지만, 수정할 때마다 배포하고 확인하는 건 꽤 번거로운 일이었기 때문에, 로컬에서 바로 테스트를 진행하기 위해 SSH 포트 포워딩 을 사용했습니다. 실제 구현은 JSch 라이브러리를 사용해서 세부 동작을 직접 다룰 일은 없었지만, 적용하면서 공부한 동작 원리를 정리해 보려고 합니다. 포트 포워딩이 트래픽을 어떻게 넘겨주는지, 터널링은 정확히 무슨 의미인지, SSH 연결은 어떤 과정을 거쳐 맺어지는지를 차례로 따라가보겠습니다. 이 글은 그 시리즈의 1편으로, 원리를 이해하는 데 집중할 예정입니다. 1. 문제 상황 1-1. 네트워크 구성 현재 네트워크 구성은 아래와 같습니다. +-------+ +---------+ +------------+ ALLOW +-----------------+ | Local |---->| bastion |---->| dev-server |---------->| api.example.com | +-------+ +---------+ +------------+ +-----------------+ | ^ | DENY | +---------------------------- X ----------------------------+ 로컬에서 개발 서버까지는 bastion 서버를 거쳐 들어갑니다. 외부 API로 나가는 방화벽은 개발 서버에서만 열려있는 상태입니다. 1-2. 해결은 SSH 포트 포워딩. 그런데 이게 정확히 어떻게 동작하는 걸까? 외부 API는 개발 서버에서만 호출할 수 있습니다. 해결을 위해 SSH 포트포워딩 설정을 적용했고, 로컬에서 외부 API를 호출할 수 있게 됐습니다. 어떤 원리로 동작하는 걸까요? 포트 포워딩은 트래픽을 어떻게 넘겨주는 걸까? 흔히 "SSH 터널링"이라고 하던데, 포트 포워딩과 같은 말일까? 중간 서버를 거치는데, 어떻게 안전하게 통신할 수 있을까? 첫 번째 질문은 2장, 두 번째는 3장, 세 번째는 4~6장에서 답해보겠습니다. 2. 포트 포워딩이란 2-1. 정의 포트 포워딩(Port Forwarding) 은 특정 포트로 들어온 트래픽을 다른 주소(IP)와 포트로 넘겨주는 것 입니다. 전달하는 방법에는 두 가지가 있습니다. NAT 방식 : 봉투(패킷)의 주소만 고쳐 쓰고 그대로 보내는 방법 Proxy 방식 : 봉투(패킷)를 뜯어 내용물을 새 봉투(패킷)에 옮겨 담는 방법 2-2. NAT 방식: 패킷 헤더만 바꿔서 통과 NAT(Network Address Translation) 방식은 커널이 패킷 헤더의 목적지 IP와 Port만 바꿔서 넘깁니다. 공유기의 포트 포워딩이 대표적인 예입니다. Router 의 역할을 공유기가 수행합니다. [A] [Router] [B] | dst = Router:8080 | dst = 10.0.0.5:80 | |------------------------>|-------------------------->| | (rewrite dst only) | | | |<=============== TCP connection x 1 ================>| 데이터에는 손대지 않고, TCP 연결은 A와 B 사이에 1개뿐 입니다. 공유기는 연결의 끝점(Endpoint)이 아니라 지나가는 길목일 뿐이죠. 2-3. 프록시 방식: 헤더를 떼고 데이터를 꺼내 새 연결로 전송 프록시(Proxy) 방식은 중간 서버가 포트를 Listen 합니다. A의 연결을 중간 서버가 직접 받고, B와는 본인이 새 TCP 연결을 맺습니다. +-----+ TCP conn #1 +-----------------+ TCP conn #2 +-----+ | A |---------------->| Proxy (Listen) |---------------->| B | +-----+ +-----------------+ +-----+ TCP 연결이 구간별로 따로 있기 때문에, A 입장에서 통신 상대는 B가 아니라 프록시 서버가 됩니다. "헤더를 떼고 데이터를 꺼낸다"는 표현에는 주의할 점이 있는데요. Proxy 방식에서 전달하는 건 TCP의 바이트 스트림(Byte Stream) 입니다. 즉, 아래와 같이 들어온 패킷과 나가는 패킷이 1:1로 대응되지 않습니다. incoming packets [ 300B ][ 1400B ][ 200B ] | v kernel strips headers socket (stream) [ 1900 bytes .............. ] | v kernel builds new packets outgoing packets [ 1000B ][ 900B ] 헤더를 떼고 붙이는 건 커널이 수행합니다. 포트포워딩을 수행하는 프로그램은 소켓에서 바이트를 읽고, 다른 소켓에 쓸 뿐입니다. 구간별 TCP 상태도 독립적입니다. 시퀀스 번호, ACK, 재전송이 구간마다 따로 관리하죠. 예를 들어 A와 프록시 사이에서 패킷이 유실되면 재전송은 그 구간에서만 일어나기 때문에 B는 그 사실을 알지 못합니다. 통신의 중간에 별도의 프로그램이 끼어들었다고 생각하시면 됩니다. 별도의 프로그램이 수행 중이기 때문에 소켓에서 데이터를 read() 하고 다른 소켓에 write() 하는 과정이 발생합니다. 2-4. 방식은 장비가 아니라 동작으로 정해진다 주의할 점은 "공유기는 NAT, 서버는 프록시"처럼 장비에 따라 방식이 나누어지지는 않습니다. 같은 리눅스 서버라도 어떻게 동작시키느냐에 따라 방식이 달라집니다. # NAT 방식: 커널이 목적지 헤더만 바꾼다 iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.5:80 # 프록시 방식: socat이 연결을 받고, 목적지로 새 연결을 맺는다 socat TCP-LISTEN:8080,fork TCP:10.0.0.5:80 iptables의 DNAT는 NAT 방식이고, socat은 프록시 방식입니다. 그리고 SSH 포트 포워딩은 항상 프록시 방식입니다. 그 이유는 6장에서 설명하겠습니다. 2-5. 비교 비교 항목 NAT 방식 프록시 방식 동작 계층 커널의 네트워크 스택 (헤더 수정) 애플리케이션 (소켓 읽기/쓰기) TCP 연결 수 양 끝단 사이 1개 구간별로 따로 (2개 이상) 처리 방식 목적지 IP/Port만 바꿔서 통과 데이터를 받고, 다른 Connection 으로 데이터를 옮김 패킷 대응 1:1 1:1 아님 (바이트 스트림) 암호화 없음 없음 암호화는 포트포워딩 과 관련이 없습니다. 다만 포트포워딩으로 중계하는 데이터 자체를 암호화해서 전달할 수 있습니다. (SSH 는 암호화된 데이터를 전달합니다.) 3. 터널링이란 3-1. 정의: 어떤 프로토콜을 다른 프로토콜 안에 감싸서 운반하는 것 터널링(Tunneling) 은 어떤 프로토콜을 다른 프로토콜 안에 감싸서 운반하는 것 입니다. 이미 포장이 완료된 택배 상자를 배송하는 경우입니다. 물건을 상자에 넣으면 택배 기사는 상자에 붙은 송장만 보고 배달합니다. 상자 안에 무엇이 들었는지는 신경 쓰지 않죠. 상자 안에 또 다른 상자가 있더라도 그런 건 신경쓰지 않습니다. 입구에서 감싸는 것을 캡슐화(Encapsulation) , 출구에서 벗겨내는 것을 역캡슐화(Decapsulation) 라고 합니다. 중간 장비는 바깥쪽 프로토콜만 보고 전달합니다. [Source] [Middle devices] [Target] +------+ +--------+------+ +------+ | DATA | -wrap-> | OUTER | DATA | -unwrap->| DATA | +------+ +--------+------+ +------+ ^ middle devices read OUTER only 💡 재택근무 때 많이 쓰는 IPsec VPN이 대표적인 터널링입니다. 다만 특정 포트가 아닌 네트워크 전체를 감싸는 등 방식이 조금 다릅니다. 3-2. SSH 포트 포워딩은 왜 터널링인가 로컬 PC에서 bastion을 통해 MySQL에 접속한다고 해보겠습니다. [로컬] ==== SSH (TCP 22, 암호화) ====> [bastion] ---- MySQL 프로토콜 (TCP 3306) ----> [DB] 구간 1: SSH 터널 구간 2: 그냥 MySQL 통신 실제로 구간 1번을 지나가는 패킷은 이런 모양이 될겁니다. +------+-----------+------------------------------------+ | IP | TCP(22) | SSH 패킷 (암호화됨) | MySQL data | +------+-----------+------------------------------------+ 3-1의 조건에 하나씩 대입해 보겠습니다. 감싸기: 로컬의 ssh 클라이언트가 MySQL 데이터를 SSH 메시지로 감싸고 암호화합니다. 벗기기: bastion의 sshd(ssh용 프로그램)가 SSH를 벗겨내고 원래 MySQL 데이터를 꺼냅니다. 중간 장비: 22번 포트로 오가는 SSH 트래픽만 봅니다. 벗겨진 MySQL data 는 그대로 DB 로 전달만 됩니다. 중간 장비는 안에 들어있는 Data 가 MySQL 요청인지 신경쓰지 않습니다. 그래서 SSH만 허용된 환경에서도 다른 프로토콜을 통과시킬 수 있죠. 1장의 문제상황도 마찬가지였습니다. 로컬에서 쓸 수 있는 통로는 SSH뿐이었지만 그 통로를 통해 HTTP API 요청을 실어 보낼 수 있었습니다. 3-3. bastion 쉘에서 다시 ssh로 접속하는 건 터널링일까? 여기서 이런 의문이 생겼습니다. 평소 업무 시, 서버에 접속할 때는 PuTTY 같은 터미널 프로그램으로 bastion에 먼저 접속하고, 그 Shell에서 다시 개발 서버로 접속하게 됩니다. 이건 터널링일까요? +-------+ SSH #1 +---------+ SSH #2 +------------+ | My PC |=============>| bastion |=============>| dev-server | +-------+ +---------+ +------------+ ^ #1 ends here, #2 starts here 결론은 터널링이 아닙니다. 두 연결이 이어져 있을 뿐, 하나가 다른 하나를 감싸진 않습니다. 각각 별개의 Connection 일 뿐입니다. 첫 번째 SSH는 키 입력과 화면 출력만 운반합니다. 두 번째 SSH의 패킷을 감싸지 않습니다.. 두 번째 SSH는 bastion이 따로 맺은 별개의 연결입니다.. 데이터가 bastion에서 한 번 복호화 됩니다. 즉, bastion은 평문을 볼 수 있습니다. 반면 문제 해결에 사용한 ProxyJump ( ssh -J )는 두 번째 SSH가 첫 번째 SSH 안에 감싸져서 지나가기 때문에 터널링이라고 할 수 있습니다. 자세한 구조는 2편에서 다루겠습니다. 비교 항목 bastion 쉘에서 다시 ssh ProxyJump ( ssh -J ) 두 번째 SSH를 맺는 쪽 bastion 내 PC 감싸는 구조 없음 (두 연결이 이어짐) 두 번째 SSH가 첫 번째 SSH 안에 감싸짐 터널링 여부 X O bastion이 볼 수 있는 것 평문 (키 입력, 화면 출력) 암호화된 두 번째 SSH 트래픽 3-4. 터널링과 포트 포워딩은 같은 말일까? 같은 말이 아닙니다. 두 개념은 겹칠 수도 있고, 따로 존재할 수도 있습니다. 터널링 O 터널링 X 포트 포워딩 O SSH 포트 포워딩 공유기 NAT 포트 포워딩 X VPN 일반 통신 NAT 포트 포워딩은 헤더만 바꿀 뿐 감싸지 않기 때문에 터널링이 아닙니다. VPN은 감싸지만 특정 포트에 넘겨주는 게 아니기 때문에 포트 포워딩이 아닙니다. SSH 포트 포워딩은 둘 다에 해당합니다. 그래서 "SSH 터널링"은 정확히 말하면 "SSH 터널 위에서 동작하는 포트 포워딩" 입니다. 그리고 2장의 분류로는 프록시 방식에 해당합니다. 4. SSH란 4-1. TCP 위의 애플리케이션 계층 프로토콜 SSH(Secure Shell) 는 TCP 위에서 동작하는 애플리케이션 계층 프로토콜입니다. 기본 포트는 22번. SSH는 먼저 암호화된 통로를 하나 깔고, 그 위에서 쉘 접속, 파일 전송, 포트 포워딩 같은 일을 수행합니다. 4-2. SSH 내부 3계층 SSH는 내부적으로 세 개의 계층으로 나뉩니다. 아래 계층이 준비되어야 위 계층이 동작합니다. +----------------------+---------------------------------------+ | Connection | channels (shell, exec, port forward) | +----------------------+---------------------------------------+ | User Authentication | user auth (password, public key) | +----------------------+---------------------------------------+ | Transport | key exchange, encryption, integrity, | | | server authentication | +----------------------+---------------------------------------+ | TCP (port 22) | reliable byte stream | +----------------------+---------------------------------------+ Transport: 키를 교환하고, 암호화와 무결성을 보장하고, 서버를 인증합니다. User Authentication: 접속하려는 사용자가 누구인지 확인합니다. Connection: 인증된 연결 위에 실제 작업용 채널을 Open 합니다. 5장은 Transport와 User Authentication을, 6장은 Connection을 알아보겠습니다. 5. SSH 연결은 어떻게 맺어질까 이 장의 예시 접속 대상은 모두 bastion입니다. 5-1. 연결 순서 한눈에 보기 ssh user@bastion 을 실행하면 아래 순서로 연결이 맺어집니다. My PC bastion Layer | | | (1) TCP 3-way handshake (port 22) | TCP |<------------------------------------->| | (2) version / algorithm negotiation | Transport |<------------------------------------->| | (3) key exchange (ECDH) | Transport | + host key verification | |<------------------------------------->| ============ encrypted from here ================== | (4) user authentication | User Auth |<------------------------------------->| | (5) open channel | Connection |<------------------------------------->| | (6) shell / port forwarding data | Connection (실제 사용 부분) |<------------------------------------->| Transport 계층에서 수행하는 일은 암호화 알고리즘 결정과 호스트 키 확인, 키 교환입니다. 5-2. 키 교환과 세션 키 (ECDH) 암호화하려면 양쪽이 같은 키를 가져야 합니다. 그런데 그 키를 네트워크로 그냥 보내면 중간에서 훔쳐볼 수 있는 위험이 존재합니다. 그렇다면 키를 어떻게 나눠 가질 수 있을까요? 최신 방식에서는 ECDH(Elliptic Curve Diffie-Hellman) 키 교환을 사용합니다. 쉽게 비유하자면 물감을 섞는 느낌이라고 보시면 됩니다. (실제로는 수학 계산으로 동일한 값을 얻어냅니다.) 두 사람이 공개된 공통 색 하나를 정한다. 각자 남에게 보여주지 않는 비밀 색을 하나씩 고른다. 공통 색에 자기 비밀 색을 섞어서 상대에게 보낸다. 받은 색에 다시 자기 비밀 색을 섞는다. 결과적으로 둘은 같은 색을 얻게 되고, 중간에서 데이터를 엿본 사람이 있더라도 섞인 색만 봤기 때문에 그 색을 만들 수는 없다는 원리입니다. 이렇게 만든 값이 세션 키(Session Key) 의 재료가 됩니다. 이후 데이터는 이 키로 AES-GCM 같은 대칭키 암호화 방식으로 암호화합니다. ECDH 는 최신의 키 교환 방식입니다. SSH 에서는 고전적인 Diffie-Hellman 방식 이 오래 쓰였고, TLS 에서는 RSA 방식 이 자주 쓰였지만 둘 다 보안 취약점이 발견되어 최근은 ECDH 를 사용합니다. 5-3. TLS와 비교: 같은 암호 기술, 다른 프로토콜 먼저 짚고 가자면, SSH는 TLS를 쓰지 않습니다. SSH는 자체 암호화 프로토콜을 사용합니다. 둘은 같은 암호 알고리즘(ECDH, AES-GCM, ChaCha20-Poly1305, Ed25519 등)을 쓸 뿐 별개의 서로 다른 프로토콜입니다. 비교 항목 TLS SSH 키 교환 (EC)DHE ECDH 등 데이터 암호화 대칭키 (AES-GCM, ChaCha20-Poly1305 등) 대칭키 (AES-GCM, ChaCha20-Poly1305 등) 서버 신원 확인 X.509 인증서 + CA 호스트 키 + known_hosts "HTTPS는 인증서로 암호화한다"는 말을 자주 듣는데, 암호화 자체는 인증서가 하는 게 아닙니다. 키 교환으로 세션 키를 만들고, 만들어진 대칭키로 암호화합니다. 과거에는 인증서의 공개키가 암호화 과정에 사용됐지만 RSA 방식이 사양되면서 인증서는 서명에만 사용되게 되었습니다. 즉 인증서로 서버 신원만 수행합니다. 5-4. 호스트 키와 known_hosts (TOFU) SSH는 기본적으로 인증서가 아니라 서버의 공개키 자체를 받는데, 이 키를 호스트 키(Host Key) 라고 합니다. 업무 중에 서버에 처음 접속하는 경우, 아래와 비슷한 메시지를 본 적이 있을 겁니다. The authenticity of host 'bastion (10.0.0.10)' can't be established. ED25519 key fingerprint is SHA256:fAkEf1ngErPr1nTf0rExAmPlE0nLyAbCdEfGh1234. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 를 입력하면 서버의 호스트 키는 클라이언트의 ~/.ssh/known_hosts 에 자동으로 저장됩니다. 이후 접속부터는 저장된 키와 비교만 수행하고, 키가 바뀌면 경고를 띄우고 접속을 막게 됩니다. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 처음 본 키를 믿고, 이후로는 그 키만 믿는 방식입니다. 이를 TOFU(Trust On First Use) 라고 합니다. TOFU는 처음 접속하는 순간에 키가 진짜인지 확인할 수 없다는 약점을 가집니다. 일반적인 HTTP 요청처럼 다수의 모르는 유저가 쓰는 환경에선 사용이 불가능한 방법이겠지만 SSH는 정해진 소수의 서버에 반복해서 접속할 때 사용됩니다. 공격자가 노리려면 하필 처음 접속하는 그 순간에 내 경로 중간에 있어야 하기 때문에 공격 기회가 좁기 때문에 업무 환경에서는 많이 사용됩니다. 약점을 보완하는 방법도 있습니다. 처음 접속할 때 지문을 서버 관리자에게 받은 값과 비교하거나 known_hosts를 미리 배포해 두고 쓰면 첫 접속의 불확실성 자체가 사라집니다 5-5. SSH 인증서 서버가 많아지면 서버마다 키를 확인하고 등록하는 일이 번거롭습니다. 그래서 OpenSSH에는 자체 인증서(Certificate) 기능이 있습니다. X.509가 아닌 Ope
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
[1편] 포트 포워딩, 터널링, 그리고 SSH는 어떻게 동작할까. 개발 서버에서만 열린 API, 로컬에서 호출하려면 알아야 할 것들 0. 들어가며 회사에서 외부 API 연동을 테스트해야 하는 일이 생겼습니. 그런데 해당 API는 내 로컬 PC에서 호출할 수 없고, 개발 서버에서만 접속이 가능한 상태였습니다. 개발 서버에 코드를 배포해서 테스트하는 방법도 있었지만, 수정할 때마다 배포하고 확인하는 건 꽤 번거로운 일이었기 때문에, 로컬에서 바로 테스트를 진행하기 위해 SSH 포트 포워딩 을 사용했습니다. 실제 구현은 JSch 라이브러리를 사용해서 세부 동작을 직접 다룰 일은 없었지만, 적용하면서 공부한 동작 원리를 정리해 보려고 합니다. 포트 포워딩이 트래픽을 어떻게 넘겨주는지, 터널링은 정확히 무슨…
Open source