6P by GN⁺ | ★ favorite | 댓글 1개
  • SSH 터널링은 보안 SSH 연결 위로 TCP 트래픽을 실어, 레거시 프로토콜 암호화·관리 패널 접근·NAT 뒤 서버 접속처럼 제한된 경로를 안전하게 여는 데 쓰임
  • 서버 측에서는 AllowTcpForwarding yes가 필요하며, 루프백이 아닌 인터페이스에 포트를 열려면 GatewayPorts yes 설정과 SSH 서버 재시작이 필요함
  • ssh -J, ssh -L, ssh -R, ssh -D는 각각 점프호스트, 로컬 포워딩, 원격 포워딩, SOCKS5 기반 동적 포워딩을 담당함
  • 터널을 백그라운드로 유지하려면 ssh -fN을 쓰고, 끊김 감지는 ClientAliveIntervalClientAliveCountMax 같은 하트비트 설정으로 조정함
  • SSH 터널링은 UDP를 직접 지원하지 않고 TCP-over-TCP에서 지연과 처리량 문제가 생길 수 있어, VPN 대체재보다는 특정 TCP 경로를 여는 도구에 가까움

SSH 터널링을 쓰는 상황

  • SSH 터널링과 포트 포워딩은 SSH 연결을 통해 TCP 트래픽을 클라이언트에서 서버로, 또는 서버에서 클라이언트로 전달함
  • TCP 포트와 UNIX 소켓을 사용할 수 있지만, 예시는 TCP 포트 중심임
  • 대표적인 활용 방식은 보안, 문제 해결, 연결 우회로 나뉨
    • 보안
      • FTP 같은 안전하지 않은 연결이나 레거시 프로토콜을 암호화함
      • 공개키 인증 기반 SSH 터널로 웹 관리 패널에 접근함
      • 80/443 같은 추가 포트를 노출하지 않고 22번 포트만 열 수 있음
    • 문제 해결
      • 방화벽이나 콘텐츠 필터를 우회함
      • 다른 네트워크 경로를 선택함
    • 연결
      • NAT 뒤에 있는 서버에 접근함
      • 점프호스트를 통해 인터넷에서 내부 서버로 들어감
      • 로컬 포트를 인터넷에 노출함

포트 포워딩 전 확인할 설정

  • 예시의 옵션들은 환경에 맞게 조합해 설정할 수 있음
  • bind_address를 지정하지 않으면 기본값은 localhost
  • 로컬과 원격 사용자는 각 머신에서 포트를 열 권한이 필요함
    • 0-1024번 포트는 별도 설정이 없으면 root 권한이 필요함
    • 그 외 포트는 일반 사용자도 설정할 수 있음
    • 클라이언트와 네트워크 방화벽도 포워딩 경로에 맞게 열려 있어야 함
  • SSH 서버에서는 포트 포워딩이 활성화되어 있어야 함
    • AllowTcpForwarding yes
    • 기본적으로 활성화된 경우가 많지만 서버 설정 확인이 필요함
  • 127.0.0.1이 아닌 인터페이스로 포트를 포워딩하려면 SSH 서버에서 GatewayPorts를 활성화함
    • GatewayPorts yes
    • 설정 후 SSH 서버 서비스를 재시작해야 함

SSH 점프호스트와 다단계 터널

  • ssh -J는 하나 이상의 호스트를 거쳐 원격 호스트에 투명하게 연결할 때 사용함
ssh -J user@REMOTE-MACHINE:22 -p 22 user@10.99.99.1
  • 기본 포트 22를 사용할 때는 포트 표기를 생략할 수 있음
  • REMOTE-MACHINE을 점프호스트로 사용하면 연결은 다음처럼 확인됨
[user@REMOTE-MACHINE]$ ss | grep -i ssh
tcp ESTAB 0 0 167.135.173.108:ssh 192.160.140.207:45960
tcp ESTAB 0 0 10.99.99.2:49770 10.99.99.1:ssh
  • 예시 주소의 역할은 다음과 같음
    • 167.135.173.108: REMOTE-MACHINE의 공개 IP
    • 192.160.140.207: LOCAL-MACHINE의 공개 IP
    • 10.99.99.2: REMOTE-MACHINE의 내부 IP
    • 10.99.99.1: REMOTE-WEBAPP의 내부 IP
  • 여러 점프호스트를 사용할 때는 쉼표로 구분함
ssh -J user@REMOTE-MACHINE:22,user@ANOTHER-REMOTE-MACHINE:22 -p 22 user@10.99.99.1

로컬 포트 포워딩

  • 로컬 포트 포워딩은 ssh -L 옵션을 사용함
  • 첫 번째 예시는 LOCAL-MACHINE의 10.10.10.1:8001을 REMOTE-MACHINE의 localhost:8000으로 전달함
ssh -L 10.10.10.1:8001:localhost:8000 user@REMOTE-MACHINE
  • REMOTE-MACHINE에서 127.0.0.1에만 바인딩된 웹서버 접근 로그는 다음처럼 보임
127.0.0.1 - - [30/Dec/2022 18:05:15] "GET / HTTP/1.1" 200
  • 이 요청은 LOCAL-MACHINE에서 시작됨
  • 두 번째 예시는 로컬 8001을 REMOTE-MACHINE을 통해 10.99.99.1:8000으로 전달함
ssh -L 8001:10.99.99.1:8000 user@REMOTE-MACHINE
  • REMOTE-WEBAPP의 웹서버 접근 로그에는 REMOTE-MACHINE의 내부 IP에서 온 요청으로 남음
10.99.99.2 - - [30/Dec/2022 21:28:42] "GET / HTTP/1.1" 200

원격 포트 포워딩

  • 원격 포트 포워딩은 ssh -R 옵션을 사용함
  • 예시는 REMOTE-MACHINE의 8000을 로컬 쪽 localhost:8001 또는 10.10.10.2:8001로 전달함
ssh -R 8000:localhost:8001 user@REMOTE-MACHINE
ssh -R 8000:10.10.10.2:8001 user@REMOTE-MACHINE
  • 특정 원격 인터페이스인 10.99.99.2:8000에서 수신하도록 설정할 수도 있음
ssh -R 10.99.99.2:8000:10.10.10.2:8001 user@REMOTE-MACHINE
  • 루프백 인터페이스가 아닌 곳에서 수신하려면 SSH 서버에 GatewayPorts yes 가 활성화되어 있어야 함

동적 포트 포워딩과 SOCKS5

  • 여러 포트를 전달할 때 SSH는 SOCKS 프로토콜을 사용함
  • SOCKS는 투명 프록시 프로토콜이며, SSH는 최신 버전인 SOCKS5를 사용함
  • SOCKS5 서버의 기본 포트는 RFC 1928에 정의된 1080
  • 클라이언트는 애플리케이션 계층이나 OS 계층에서 SOCKS 프록시를 사용하도록 설정되어야 함
  • ssh -D 예시는 10.10.10.1:5555에 SOCKS 프록시를 엶
ssh -D 10.10.10.1:5555 user@REMOTE-MACHINE
  • LOCAL 클라이언트에서는 curl로 연결 경로를 테스트할 수 있음
curl -L -x socks5://10.10.10.1:5555 brrl.net/ip
  • 정상 동작하면 REMOTE-MACHINE의 공개 IP가 반환됨

SSH TUN/TAP 터널링

  • -w 플래그로 양방향 터널을 만들 수 있음
  • 인터페이스는 미리 생성되어 있어야 함
  • 이 방식은 테스트되지 않았다는 caveat가 남아 있음
-w local_tun[:remote_tun]

백그라운드 실행과 종료

  • 터널을 네이티브 방식으로 백그라운드 실행하려면 -fN을 사용함
    • -f: 백그라운드 실행
    • -N: 셸을 열지 않음
ssh -fN -L 8001:127.0.0.1:8000 user@REMOTE-MACHINE
  • 그 외에는 screen이나 다른 도구를 사용할 수 있음
  • 백그라운드에서 실행 중인 SSH는 프로세스를 찾아 PID로 종료함
user@pleasejustwork:~$ ps -ef | grep ssh
[...]
user 19255 1 0 11:40 ? 00:00:00 ssh -fN -L 8001:127.0.0.1:8000 user@REMOTE-MACHINE
[...]
kill 19255

SSH 연결 유지와 재접속

  • SSH 연결 유지 방법은 여러 가지가 있음
  • 타임아웃 처리를 위한 하트비트 옵션은 클라이언트, 서버, 또는 양쪽 모두에 설정할 수 있음
  • ClientAliveInterval은 연결 유지를 위해 매 n초마다 요청을 보냄
ClientAliveInterval 15
  • ClientAliveCountMax는 반대편에서 응답을 받지 못한 뒤 연결을 종료하기 전까지 보내는 하트비트 요청 수
ClientAliveCountMax 3
  • 3은 기본값이고, 0으로 설정하면 연결 종료를 비활성화함
  • 예시 설정에서는 응답이 없으면 약 45초 뒤 연결이 끊어짐
  • 연결 종료 후 재접속에는 autossh, 스크립트, cronjob 등을 사용할 수 있음

제한과 보안상 주의점

  • UDP

    • SSH는 올바른 복호화를 위해 신뢰성 있는 전달에 의존함
    • UDP는 신뢰성을 제공하지 않아 SSH 터널에서 지원되지 않고 권장되지 않음
    • UDP over SSH tunneling을 다룬 방법이 있지만 테스트되지 않았다는 caveat가 있음
  • TCP-over-TCP

    • 오버헤드 때문에 처리량이 낮아지고 지연이 증가함
    • 패킷 손실이나 높은 지연이 있는 연결에서는 TCP meltdown이 발생할 수 있음
    • TCP over TCP 관련 글을 참고할 수 있음
    • OpenVPN-over-TCP를 한동안 사용했을 때 처리량은 UDP보다 낮았지만 안정적으로 동작했으며, 결과는 구성에 크게 의존함
  • VPN 대체재로 쓸 때의 한계

    • SSH 터널링을 VPN처럼 사용할 수는 있지만, 더 나은 성능에는 VPN이 더 적합함
  • 보안 위험

    • 해당 기능이 필요 없다면 비활성화하는 것이 권장됨
    • 공격자가 SSH 터널링과 포트 포워딩을 사용해 방화벽과 다른 보안 수단을 피할 수 있음
  • 참고 문서

댓글과 토론

Hacker News 의견들
  • 2024년에는 SSH 명령을 길게 직접 쓰기보다 ~/.ssh/configLocalForward, RemoteForward, ProxyJump를 설정하는 편이 좋음
    중간 SSH 연결을 여러 번 거쳐야 하는 원격 서버에 ssh, scp, rsync로 접근할 때 시간을 크게 줄여줌
    예를 들어 jump-host-1, jump-host-2, jump-host-3ProxyJump로 이어두고 target-server에 별칭으로 접속하면 됨
    LocalForward 0.0.0.0:8080 0.0.0.0:80은 원격의 80번 포트를 로컬 8080으로 전달하고, RemoteForward 0.0.0.0:9022 0.0.0.0:22는 원격 9022로 들어온 SSH 요청을 로컬 22번으로 전달함
    LocalForwardRemoteForward에서는 왼쪽이 대상 서버, 오른쪽이 로컬이며, localhost127.0.0.1 대신 0.0.0.0을 쓰라는 팁도 있음

    • ssh_config 팁을 공유하자면, 집 안이나 자체 호스팅 VPN 안에서는 각 장비에 직접 SSH하고, 밖에서는 점프 호스트를 거치도록 자동 분기해두는 구성이 유용함
      Match host *.example.org exec "getent ahosts router.example.org | grep -q ^10.0.0.1"처럼 라우터 주소로 집/VPN 여부를 먼저 감지하고, arp로 MAC 주소가 기대값과 다르면 ProxyJump jump.example.org를 쓰게 만들 수 있음
      순서가 중요해서 VPN/집 네트워크 감지를 먼저 하고, 그다음 외부일 때 점프 호스트를 적용해야 함
    • 가끔은 한 번만 쓰고 끝낼 일이라 설정 파일까지 만들고 싶지 않을 때도 있음
      짧게 쓸 VPS를 만들어 SSH를 SOCKS 프록시로 쓰면서 지역 제한을 우회하거나, 다른 팀을 잠깐 도와주느라 평소 접근하지 않는 서버에 한 번 들어가야 하는 경우에는 명령행 옵션이 더 낫다고 봄
    • 0.0.0.0을 쓰는 건 위험할 수 있음
      모든 네트워크 인터페이스에서 포트를 열 수 있고, 특히 원격 서버에 방화벽이 없으면 무언가를 인터넷 전체에 노출하게 됨
      자주 쓰는 터널링이나 포트 전달이라면 설정 파일이 좋지만, 일회성이나 드문 작업이면 명령행 옵션이 더 낫다고 봄
    • 이런 SSH 설정을 여러 로컬 머신에 표준화해서 배포하려면 어떻게 관리하는지 궁금함
      단일 사용자/여러 머신에 맞고, 별도 서버를 항상 켜둘 필요 없이 GitHub 같은 곳에서 설정을 가져오는 GitOps 비슷한 해법을 찾고 있음
    • 항상 셸이 열리기를 원하는 건 아니라서, 기본적인 bash/fish 함수를 만들어 최소한의 인자만 기억하면 되게 해뒀음
      잊어버리면 문서화해둔 함수 정의를 보면 됨
  • 내가 일하는 말도 안 되는 기업 환경에서는 SSH 터널링이 필수임
    접근 권한을 받거나 포트를 열거나 방화벽/VPN 예외를 얻는 데 관료주의 때문에 몇 주씩 걸릴 때가 있음
    이 글은 -D를 언급하지만 그 위력을 충분히 설명하지는 않음
    ssh -D 8888 someserver를 실행하고 브라우저의 SOCKS 프록시를 localhost:8888로 설정하면, 브라우저 트래픽이 전부 someserver를 통해 라우팅됨
    Firefox는 아직도 시스템 기본값을 바꾸지 않고 이 설정을 할 수 있어서 매우 유용함

    • 2000년대 중반에는 집 밖에서 웹을 볼 때 거의 표준 방식으로 그렇게 썼음
      그런데 회사에 들어가 보니 IP 허용 목록 때문에 인터넷의 임의 서버로 SSH 연결조차 못 했고, 너무 괴로워서 HTTP 터널을 만들고 내가 제어하는 서버를 허용 목록에 넣는 방법까지 알아보다가 결국 이직했음
    • 기업 네트워크를 우회하는 건 좋은 생각이 아님
      그런 제한은 이유가 있고, 우회는 조직의 절차와 보안에 대한 전문성 부족과 무시로 보일 수 있음
      접근 권한을 받는 데 몇 주나 몇 달이 걸리면 그렇게 기다려야 하며, 기밀 정보에 백도어를 열거나 보안을 훼손한 책임을 지고 싶지는 않을 것임
  • 새벽 3시에 해본 가장 지저분한 SSH 터널링 해킹은 세 곳의 데이터센터를 잇는 작업이었음
    같은 도시권에 있는 세 시설이 인터넷 상위망에는 연결돼 있었지만, 이상한 라우팅 정책 때문에 A는 B에만, B만 C에 연결할 수 있었음
    결국 A의 데이터를 B를 거쳐 C로 옮기기 위해 rsync + SSH 터널 + 키 + 라우팅 꼼수를 뒤섞어야 했고, 주문 같은 명령을 몇 번 고쳐 맞춘 뒤 전체가 한 번에 움직이는 걸 보니 마법 같았음
    지금 보면 어떻게 할지 너무 명확하지만, 당시 초보였던 입장에서는 큰 성취였고 동기화가 끝난 걸 확인했을 때의 짜릿함이 아직 기억남

    • ~/.ssh/configProxyJump를 쓰면 A, B, C, D, E처럼 몇 단계든 사실상 점프해서 이동할 수 있음
  • 시각화의 세부 정보가 좋음
    네트워킹에는 특히 더 낮은 수준의 연결에서 트래픽을 시각적으로 표현하는 도구가 훨씬 많아졌으면 함

  • 리눅스 서버나 IoT 장비가 방화벽 뒤에 있고 고정 IP도 없는데 SSH로 접속하고 싶다면, https://sshreach.me 같은 터널링 서비스를 쓸 수 있음

  • 몇 년 동안 터널링을 꽤 많이 썼는데 -J 옵션은 몰랐음
    몇 달에 한 번 터널이 필요할 때마다 30분씩 설정하느니, 터널을 구성해주는 시각적 도구가 있었으면 좋겠음

  • TCP-over-TCP가 오버헤드를 늘려 처리량을 낮추고 지연 시간을 높이며, 패킷 손실이나 위성망처럼 지연이 큰 연결에서 TCP 멜트다운을 일으킬 수 있다는 설명이 있음
    하지만 SSH 터널에서는 TAP/TUN을 쓰지 않는 한 실제로는 문제가 되지 않음
    SSH가 TCP 스트림을 풀어서 전달하기 때문임
    다만 여러 채널을 쓸 때는 헤드 오브 라인 블로킹 때문에 성능이 떨어질 수 있음

  • 약 15년 전 대학 네트워크의 방화벽을 우회하려고 SSH 터널을 배웠고, 기본 포트를 443번으로 바꿔야 했음
    그 뒤로는 방화벽 우회보다 훨씬 다양한 용도로 계속 쓰고 있음

    • 방화벽 우회나 로컬 서비스를 네트워크 너머로 노출하는 것 말고 어떤 용도로 재미를 봤는지 궁금함
  • sshuttle을 써보면 터널링이 훨씬 편함
    sshuttle -r user@host 10.0.0.0/8처럼 쓰면 10/8 대역은 자동으로 터널링되고, 사실상 SSH 위의 VPN처럼 동작함

  • SSH 자체에 리다이렉트 기능이 있는지 궁금함
    예를 들어 A가 B에 SSH하려고 하면 B가 C로 접속하라고 알려주고, A가 투명하게 C에 직접 연결되며, B는 더 이상 핵심 데이터 경로에 남지 않는 식의 기능이 있는지 궁금함

    • 가상 IP로 비슷하게 할 수 있을 것 같음
      내 방화벽/라우터는 DHCP option 67을 제대로 전달하지 않고 무조건 자기 주소를 보내서, 해당 포트의 PXE 부팅 트래픽이 라우터 IP로 가면 실제 PXE 부팅 서버로 라우팅되도록 가상 IP/규칙을 설정해야 했음
    • A가 실제 대상이 C라는 걸 모르면 오해의 소지가 있음
      그렇지 않다면 점프 기능을 쓰면 됨: ssh -J B C
      B가 경로에 있을 필요가 없고 C에 직접 연결할 수 있다면 그냥 C로 직접 연결하면 되고, 그게 안 된다면 어차피 B는 홉으로 들어가야 함
    • B가 C로 포트 전달, 즉 패킷 라우팅을 할 수는 있겠지만 HTTP의 영구 리다이렉트 같은 동등한 기능은 없는 것 같음
      문제를 더 설명하면 더 적합한 해법이 있을 수 있음
      어느 정도 임베디드된 호스트라면 DNS가 “라우팅”을 맡게 할 수 있지만, 이 경우 호스트 키 지문 검증은 직접 처리해야 함