본문 바로가기
서버 인프라 실무

SSH 포트 포워딩·리버스 터널 완전 정복: 개발/운영 실전 보안 가이드(OpenSSH)

by yamoojin83 2025. 10. 26.

SSH 포트 포워딩·리버스 터널 완전 정복: 개발/운영 실전 보안 가이드(OpenSSH)

VPN 없이도 SSH만으로 내부 포트 접근외부 노출을 유연하게 만들 수 있습니다. 이 글은 로컬/원격/동적(프록시) 포워딩과 리버스 터널까지, 실무에서 쓰는 보안 규칙과 감사(로그)·자동화 팁을 한 번에 정리합니다. Jump 서버(바스천), ProxyJump, ~/.ssh/config 템플릿, systemd 서비스 등록, 위험한 패턴 차단까지 포함합니다.


1) 용어·개념 한 장 요약

  • -L (Local Forward): 내 PC(클라이언트)에 포트를 열고, SSH 서버를 경유해 원격 대상으로 전달.
  • -R (Remote/Reverse Forward): SSH 서버(또는 서버 측)에 포트를 열고, 내 PC로 역방향 전달.
  • -D (Dynamic/SOCKS5): 로컬 SOCKS 프록시를 띄워 여러 목적지로 유연한 트래픽 전달.
  • ProxyJump: 점프호스트(바스천) 한 번에 경유(구식 ProxyCommand 대체).
# 포트 표기 규칙
# 로컬 -L:   [LOCAL_ADDR:]LOCAL_PORT:TARGET_HOST:TARGET_PORT
# 리버스 -R: [REMOTE_ADDR:]REMOTE_PORT:TARGET_HOST:TARGET_PORT
# 동적 -D:   [LOCAL_ADDR:]LOCAL_SOCKS_PORT

2) -L: 로컬 포워딩(내 PC → 원격 내부)

개발/운영에서 가장 흔합니다. 방화벽 밖에서 내부 DB/관리 콘솔에 잠깐 접근할 때 유용합니다.

# 예시: 로컬 15432 → (SSH) → 내부 DB 10.10.0.20:5432
ssh -L 127.0.0.1:15432:10.10.0.20:5432 user@bastion.example.com

# 로컬에서 접속(애플리케이션/CLI)
psql -h 127.0.0.1 -p 15432 -U app appdb
  • 보안 규칙:
    • 로컬 바인드 주소는 127.0.0.1로 제한(외부 공유 금지).
    • 필요 포트만 열고, 세션 종료 시 터널도 닫히므로 일시 사용 원칙.

~/.ssh/config로 단축:

Host bastion
  HostName bastion.example.com
  User user
  IdentityFile ~/.ssh/id_ed25519

Host db-tunnel
  HostName bastion.example.com
  User user
  LocalForward 127.0.0.1:15432 10.10.0.20:5432
  ServerAliveInterval 30
  ServerAliveCountMax 3
# 실행
ssh db-tunnel

3) -R: 리버스(원격) 포워딩(서버 측 → 내 PC/내부)

NAT/방화벽 안쪽에 있는 서버에 외부에서 접근해야 할 때 씁니다. 보안 사고 위험이 크므로 원칙과 제한을 꼭 적용하세요.

# 서버(원격)에 18080 포트를 열고, 내 PC의 8080으로 역방향 전달
ssh -R 127.0.0.1:18080:127.0.0.1:8080 user@bastion.example.com
# 이제 바스천에서 127.0.0.1:18080 접속 → 내 PC 8080으로 전달
  • SSH 서버 설정(서버 측):
    # /etc/ssh/sshd_config
    AllowTcpForwarding yes         # 필요한 계정/그룹만 제한 권장
    GatewayPorts no                # 0.0.0.0 바인드 금지(안전)
    PermitOpen 127.0.0.1:18080     # 허용 대상 고정(또는 여러 줄)
    # 재시작
    sudo systemctl reload ssh
    
  • 규칙: 반드시 127.0.0.1에 바인딩하고, PermitOpen으로 포트를 화이트리스트.

4) -D: 동적(SOCKS5) 포워딩(프록시처럼)

웹/CLI 트래픽을 SSH 경유로 보내고 싶을 때 유용합니다.

# 로컬 1080 SOCKS5 프록시 생성
ssh -D 127.0.0.1:1080 user@bastion.example.com

# 브라우저/툴에서 SOCKS5 프록시 127.0.0.1:1080 설정
# curl 예시
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.io

주의: DNS 누출 방지 위해 --socks5-hostname 혹은 클라이언트에서 “프록시를 통한 DNS” 옵션 사용.


5) Jump 서버 경유(ProxyJump) — 다중 홉 한 번에

바스천을 거쳐 프라이빗 서버로 들어가는 표준입니다.

# 단발성
ssh -J user@bastion.example.com user@private.internal

# ~/.ssh/config
Host bastion
  HostName bastion.example.com
  User user

Host app-internal
  HostName 10.10.0.30
  User ec2-user
  ProxyJump bastion
# 포워딩과도 조합 가능
ssh -J bastion -L 127.0.0.1:15432:10.10.0.20:5432 app-internal

6) 자주 쓰는 운영 레시피

6-1) 사내 단일 포트만 허용된 환경에서 DB, 메시지브로커 접근

# 로컬에서 여러 포트 동시 포워딩
ssh -L 127.0.0.1:15432:10.10.0.20:5432 \
    -L 127.0.0.1:19092:10.10.0.21:9092 \
    -L 127.0.0.1:16379:10.10.0.22:6379 \
    user@bastion

6-2) 운영 점검(내부 대시보드 잠깐 보기)

# 내부 Grafana 3000을 13000으로 포워딩(로컬에서만)
ssh -L 127.0.0.1:13000:10.10.0.40:3000 ops@bastion

6-3) 긴 세션 안정화/자동 복구

# keep-alive와 재연결 옵션
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
    -o ExitOnForwardFailure=yes user@bastion

7) systemd로 “항상 켜지는” 터널 만들기(신중)

자동 터널은 편하지만 보안/감사 책임이 큽니다. IP/포트 화이트리스트와 제한을 먼저 적용하세요.

# /etc/systemd/system/ssh-tunnel.service
[Unit]
Description=SSH Local Forward (DB to Bastion)
After=network-online.target
Wants=network-online.target

[Service]
User=dev
ExecStart=/usr/bin/ssh -N -L 127.0.0.1:15432:10.10.0.20:5432 dev@bastion.example.com \
  -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel
systemctl status ssh-tunnel --no-pager

비밀키: ~dev/.ssh/id_ed25519 권한 600, 필요 시 ssh-agent 사용. 서버측 AuthorizedKeysCommand로 키 관리 중앙화 권장.


8) 보안 정책(필수 체크리스트)

  • 서버 측 sshd_config:
    AllowTcpForwarding yes|local|no   # 필요 범위로
    GatewayPorts no                   # 0.0.0.0 바인딩 금지
    PermitOpen host:port host2:port   # 허용 대상 고정(여러 줄)
    ClientAliveInterval 60
    ClientAliveCountMax 3
    
  • UFW/보안그룹: 바스천에서만 내부로 접근, 관리망은 화이트리스트.
  • 감사: /var/log/auth.log 또는 journald에서 channel open 기록 추적, fail2ban으로 과도 연결 차단.
  • 원격 포워딩은 정말 필요한 서비스/포트만, 만료/점검 루틴 포함.

9) 디버깅·문제 해결

# SSH 디버그(핵심)
ssh -v -L 127.0.0.1:15432:10.10.0.20:5432 user@bastion

# 포트 바인딩 충돌
ss -lntp | grep 15432

# 라우팅/방화벽 확인(서버/대상)
ss -tn dst 10.10.0.20:5432
sudo ufw status numbered
  • “channel 3: open failed: administratively prohibited” → 서버 측 PermitOpen/AllowTcpForwarding 제한.
  • 연결은 되지만 타임아웃 → 대상 호스트 방화벽/보안그룹 점검, 바스천에서 telnet/nc로 직접 확인.
  • SOCKS는 되는데 DNS 누출--socks5-hostname 사용, 브라우저 “프록시 DNS” 옵션 On.

10) 모범 ~/.ssh/config 템플릿(복붙)

Host *
  ServerAliveInterval 30
  ServerAliveCountMax 3
  Compression yes
  IdentityAgent ~/.ssh/agent.sock

Host bastion
  HostName bastion.example.com
  User ops
  IdentityFile ~/.ssh/id_ed25519

Host admin
  HostName 10.10.0.10
  User ubuntu
  ProxyJump bastion
  # 로컬 포워딩 예시
  LocalForward 127.0.0.1:15432 10.10.0.20:5432

Host socks
  HostName bastion.example.com
  User ops
  DynamicForward 127.0.0.1:1080

11) 운영 수칙 요약

  • 기본은 -L(로컬 포워딩). 리버스(-R)는 제한·감사 필수.
  • 바인드 주소는 127.0.0.1 고정, 공개 바인딩(0.0.0.0) 금지.
  • Jump는 ProxyJump로 일원화, 키·접근권한 중앙 관리.
  • 장기 터널은 systemd 서비스로 관리하되, 만료·점검 스케줄 포함.
  • 로그/알림(예: fail2ban, 메일/Slack)으로 이상 연결 감시.

여기 구성만 지켜도 SSH 터널은 필요할 때만, 안전하게 사용할 수 있습니다. 다음 글에서는 Fail2ban·UFW와의 연동 고급편, 그리고 프록시 계층(Nginx/HAProxy)과 SSH 터널의 역할 분리를 더 깊게 다루겠습니다.