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 터널의 역할 분리를 더 깊게 다루겠습니다.