termal.in

← 블로그

SSH 리버스 터널(ssh -R): 로컬 포트를 원격 서버로 노출하기

· Termalin Team sshport-forwardingtunnelingtutorial

노트북에서 뭔가가 돌고 있습니다 — 개발 서버, API, 또는 집에서 NAT 뒤에 있는 머신이요. 그런데 인터넷 반대편의 무언가가 거기에 닿아야 합니다. 공인 IP도 없고, 공유기 접근 권한도 없고, VPN을 구성할 시간도 없습니다. 이게 바로 SSH 리버스 터널, 즉 ssh -R 명령이 하는 일입니다. 내가 닿을 수 있는 서버에 포트 하나를 띄우고, 거기로 들어오는 모든 트래픽을 조용히 내 머신으로 되돌려 보냅니다.

이 플래그는 방향을 거꾸로 이해하기 쉬운 녀석이니, 먼저 개념부터 바로잡고 실제 사례를 하나씩 짚어 보겠습니다.

핵심 개념: -L을 뒤집은 것

모든 SSH 터널은 한쪽 머신에 수신 포트를 열고, 거기로 들어온 것을 다른 머신에서 닿을 수 있는 어딘가로 실어 나릅니다. 로컬 포워딩(-L)에서는 포트가 내 머신에 열리고 트래픽이 서버 쪽으로 나갑니다. 리버스 터널은 그 정확한 거울상입니다.

-R에서는 수신 포트가 원격 서버에 열리고, 거기로 들어온 연결이 내 쪽으로 나옵니다.

이게 요령의 전부입니다. 인바운드 연결을 받을 수 없는 머신이 대신 아웃바운드 SSH 연결을 걸고, 그 연결을 타고 들어오는 연결을 되받습니다. 둘이 여전히 헷갈린다면, 세 형제의 전체 가족 사진은 SSH 포트 포워딩 완전 정리에 있습니다.

문법, 왼쪽에서 오른쪽으로 읽기

ssh -R [bind_address:]remote_port:localhost:local_port user@server

이렇게 읽으세요. 서버에서 remote_port로 수신하고, 들어오는 것을 — 내 머신 기준으로 해석한 — localhost:local_port로 포워딩하라. 그래서:

ssh -R 8080:localhost:3000 deploy@vps.example.com

는 VPS에 8080 포트를 열고, 거기로 들어오는 모든 연결을 내 노트북의 3000 포트로 보냅니다. 목적지가 꼭 내 노트북 자신일 필요는 없습니다. -R 8080:192.168.1.50:80은 노트북을 출구로 삼아 내 LAN의 다른 머신으로 포워딩합니다.

알아 두면 좋은 플래그 하나: -R 0:localhost:3000은 서버가 빈 포트를 알아서 고르게 하고, Allocated port 43022 for remote forward를 출력해 어떤 포트가 배정됐는지 알려 줍니다.

실제로 이걸로 만드는 것 세 가지

로컬 개발 서버를 클라이언트에게 보여 주기. 작업 중인 결과물이 localhost:3000에서 돕니다. 시연 하나 때문에 어딘가에 배포하는 대신:

ssh -N -R 8080:localhost:3000 deploy@demo.example.com

이제 demo.example.com:8080에 닿을 수 있는 누구나 내 노트북에서 돌아가는 앱을 실시간으로 보게 됩니다. (-N은 원격 셸을 건너뜁니다 — 필요한 건 터널뿐이니까요. 누구나 연결할 수 있는지 아니면 서버 자신만 연결할 수 있는지는 GatewayPorts에 달렸습니다 — 다음 절에서요.)

NAT 뒤의 집 머신에 접근하기. 집에 있는 상자는 연결을 받을 수 없지만, 걸 수는 있습니다. 작은 VPS로 나가는 터널을 쥐고 있게 하세요:

# 집 머신에서
ssh -N -R 2222:localhost:22 tunnel@vps.example.com

이제 VPS의 2222 포트가 곧 집 머신의 SSH 포트이고, 어디서든:

ssh -J vps.example.com -p 2222 you@localhost

로 VPS를 거쳐 NAT 뒤의 상자로 바로 뛰어듭니다. 이것이 집 서버에 SSH로 접속하는 법의 고전적인 셀프 호스팅 해법입니다.

웹훅을 내 노트북으로 받기. 결제 사업자나 git 호스트가 개발 중인 핸들러로 POST를 보내야 합니다. 그들이 닿을 수 있는 엔드포인트를 나에게로 터널링하세요:

ssh -N -R 9000:localhost:9000 deploy@vps.example.com

웹훅을 VPS로 가리키게 하면 됩니다. 터널을 서버의 루프백(기본값)에 묶어 두었다면, VPS에서 127.0.0.1:9000 앞에 nginx나 아무 리버스 프록시를 두세요 — TLS 종단을 공짜로 얻고 생 터널 포트를 절대 노출하지 않습니다.

GatewayPorts: 왜 남들은 내 터널에 닿지 못하나

가장 흔한 놀람: 서버에서 테스트하면(curl localhost:8080이 성공) 터널은 되는데 바깥에서는 아무도 연결하지 못합니다. 이건 의도된 동작입니다 — 기본적으로 sshd는 원격 포워딩 포트를 127.0.0.1에만 묶습니다. 어떤 bind 주소를 요청했든 상관없이요.

터널을 바깥 세상에 열려면, 서버의 /etc/ssh/sshd_config에 다음이 필요합니다:

GatewayPorts clientspecified

그런 다음 sshd를 재시작하고 bind를 명시적으로 요청하세요: ssh -R 0.0.0.0:8080:localhost:3000 .... (GatewayPorts yes는 모든 원격 포워딩을 전체 인터페이스에 묶으므로, 보통 원하는 것보다 범위가 넓습니다.) 이건 클라이언트에서 재정의할 수 없는 서버 쪽 결정이며, 의도된 안전 기본값입니다. 리버스 터널은 안쪽에서 뚫은 구멍이고, 거기로 노출되는 것은 사실상 인터넷에 직접 노출되는 셈이니까요. 그렇게 다루세요.

관련된 서버 쪽 스위치 두 개가 발목을 잡을 수 있습니다: AllowTcpForwarding no는 터널을 통째로 비활성화하고, PermitListen은 -R이 차지할 수 있는 포트를 제한할 수 있습니다.

연결 유지하기

리버스 터널은 TCP 연결 하나이고, 유휴 TCP 연결은 죽습니다. 시연보다 오래 쓸 거라면:

ssh -N -R 2222:localhost:22 \
    -o ServerAliveInterval=30 \
    -o ExitOnForwardFailure=yes \
    tunnel@vps.example.com
  • **ServerAliveInterval 30**은 트래픽을 계속 흐르게 해서 NAT 항목과 유휴 타임아웃이 연결을 거둬 가지 않게 합니다.
  • **ExitOnForwardFailure yes**는 원격 포트를 묶지 못할 때 ssh가 조용히 연결만 하고 아무것도 포워딩하지 않는 대신, 요란하게 죽게 만듭니다.
  • **autossh**는 명령을 감싸서 터널이 끊길 때마다 재시작합니다 — 재부팅과 불안정한 네트워크를 견뎌야 하는 터널을 위한 표준 도구로, 보통 systemd 유닛 아래에서 autossh -M 0 -N -R ...로 돌립니다.

그리고 터널이 영구 자리를 얻으면, RemoteForward 줄과 함께 ~/.ssh/config로 옮겨 호스트 항목과 함께 따라다니게 하세요 — 구조는 ssh_config 가이드(영어)에서 다룹니다:

Host homelink
    HostName vps.example.com
    User tunnel
    RemoteForward 2222 localhost:22
    ServerAliveInterval 30
    ExitOnForwardFailure yes

-L vs -R vs -D 한눈에 보기

플래그포트가 열리는 곳트래픽이 나가는 곳대표 용도
-L내 머신서버 쪽원격 DB/대시보드를 localhost로 끌어오기
-R서버내 쪽로컬 서비스 노출, NAT 뒤 상자 접근
-D내 머신서버 쪽, 임의의 목적지네트워크로 들어가는 SOCKS 프록시

리버스 터널이 잘못된 선택일 때

리버스 터널은 훌륭한 임시 해법입니다. 시연, 웹훅 디버깅, NAT 뒤 머신 구조하기 같은 일이요. 하지만 영구 해법으로는 부실합니다. 노출하려는 대상이 몇 달씩 떠 있어야 하거나, 실사용자를 상대해야 하거나, 내 노트북이 잠들어도 살아남아야 한다면 — 그건 인그레스이고, 인그레스에는 제대로 된 인프라가 필요합니다. 서버 위의 리버스 프록시, VPN이나 오버레이 네트워크, 실제 배포 같은 것 말입니다. 터널의 매력은 상주 흔적 없이 명령 한 줄이라는 점입니다. 터널을 위한 모니터링을 짜기 시작하는 순간, 이미 터널을 졸업한 겁니다.

흔한 실패 해결하기

  • Warning: remote port forwarding failed for listen port 8080 (수신 포트 8080의 원격 포트 포워딩 실패) — 서버에서 그 포트가 이미 점유되어 있습니다. 범인은 흔히 내가 이전에 띄운 터널입니다. 죽은 연결이 타임아웃될 때까지 옛 sshd 프로세스가 포트를 쥐고 있거든요. keepalive가 좀비를 막고, -R 0:...은 충돌을 피해 가며, ExitOnForwardFailure는 그 경고를 놓칠 수 없는 오류로 바꿔 줍니다.
  • 서버에서는 되는데 바깥에서는 거부됨 — 수신 포트가 서버의 루프백에 있습니다. GatewayPorts 기본값이 제 할 일을 하는 것입니다. 설정을 바꾸거나 리버스 프록시를 포트 앞에 두세요.
  • 터널은 연결되는데 서비스가 응답하지 않음 — 로컬 쪽을 확인하세요. 지정한 로컬 포트에서 실제로 뭔가가 수신 중입니까? :3000에 아무것도 없는 -R 8080:localhost:3000은 방문자마다 connection refused를 돌려줍니다.
  • SSH 연결 자체가 안 맺어짐 — 그건 별개의 문제이고 나름의 체크리스트가 있습니다: connection refused와 timed out 진단.

터널이 붙박이가 될 때

한 번 돌리고 마는 리버스 터널은 그때그때 타이핑하면 됩니다. 하지만 홈랩과 시연용 상자, 웹훅 엔드포인트를 떠받치는 터널은, 노트북이 잠들 때마다 아침에 다시 쌓아 올려야 하는 상태입니다 — 그러다 목요일쯤이면 어디에 뭐가 있었는지 놓치게 됩니다. Termalin의 저장된 터널 관리자는 로컬·리모트·다이내믹 포워딩을 각자 속한 호스트에 붙여 둡니다. 클릭 한 번으로 시작하고, 어떤 것이 살아 있는지 한눈에 보며, 셸 히스토리에서 다시 타이핑하는 대신 연결과 함께 자동으로 재수립되게 합니다. 호스트 목록은 — 키, 점프 호스트, keepalive, 터널까지 포함해 — 여러 머신 사이에서 종단 간 암호화되어 동기화됩니다. 그리고 상자가 영영 NAT 뒤에 있다면, 아웃바운드 전용 Connector가 손수 만든 리버스 터널을 통째로 대신합니다 — 인바운드 포트도 없고, 돌볼 autossh 유닛도 없습니다. 명령줄은 -R이 무슨 뜻인지 가르쳐 주고, 클라이언트는 내가 의지하는 터널이 내 기억에 의존하지 않도록 지켜 줍니다.


Termalin은 터널, SFTP, 키 관리자를 기본 내장한 무료 크로스 플랫폼 SSH 클라이언트입니다 — 다운로드하거나, 키를 안전하게 다루는 방식을 확인해 보세요.

호스트 한 대에서 먼저 써 보세요.

Termalin은 여러분과 여러분의 에이전트를 위한 빠른 SSH 클라이언트입니다.

무료 요금제 · Pro 14일 무료 체험 · 요금