서버에 MySQL을 올려놓고 로컬 워크벤치로 붙으려는데 연결이 안 됩니다. 방화벽에서 3306을 막아놨기 때문입니다. 그렇다고 3306을 전 세계에 열어두는 건 위험합니다. 이럴 때 대부분 “VPN을 깔아야 하나” 고민하다가, 결국 SSH 터널링으로 돌아옵니다.
SSH 터널링은 이미 열려 있는 22번 포트 하나를 통로로 빌려 쓰는 기술입니다. SSH는 원래 원격 쉘을 쓰라고 만든 프로토콜이지만, 그 암호화된 연결 안에 다른 TCP 통신을 얹어 보낼 수 있습니다. 서버에 접속할 수 있는 계정이 이미 있다면 추가로 설치할 것도, 서버 설정을 건드릴 것도 없습니다. 요즘 클라우드 인스턴스나 웹호스팅에서 DB 포트를 기본으로 닫아두는 경우가 많아, 개발자라면 한 번은 마주치게 되는 상황입니다.
한눈에 · 방향 감각 잡기(-L/-R/-D) →
-L로 막힌 DB 붙기 →-R·-D로 방향 뒤집기 → 에러 메시지별 대처 →~/.ssh/config에 넣고 상시 운영
SSH 터널링이 뭐고, 어떤 상황에서 쓰게 되나
가장 흔한 건 앞서 말한 DB 접속입니다. 서버의 3306이 외부에 닫혀 있어도, SSH로는 접속되니까 그 안쪽에서 3306에 연결해 달라고 부탁하는 방식입니다. 서버 입장에서는 자기 자신의 3306에 붙는 것이라 방화벽에 걸리지 않습니다.
반대 방향도 있습니다. 내 노트북에서 돌리는 개발 서버를 잠깐 다른 사람에게 보여줘야 할 때, 공인 IP가 있는 서버 쪽에 임시로 구멍을 내서 연결해 줄 수 있습니다. 카페 와이파이처럼 못 미더운 네트워크에서 브라우저 트래픽 전체를 서버 경유로 돌리는 용도도 있습니다.
여기서 개념을 하나 바로잡고 가야 합니다. 터널링은 “포트를 여는 것”이 아닙니다. 내 PC의 어떤 포트와 원격의 어떤 포트를 한 가닥 줄로 잇는 것입니다. 이렇게 이해해야 뒤에 나오는 -L, -R이 헷갈리지 않습니다. 포트 번호 두 개 이상이 한 명령에 나오는데, 각각이 누구 편인지 구분하는 게 전부입니다.
시작 전에 정리할 것: 로컬·리모트·다이내믹의 방향 감각
개념을 잡았으니 이제 옵션 세 개가 각각 어느 방향으로 흐르는지를 구분해야 합니다. 이 방향만 헷갈리지 않으면 나머지는 문법 문제입니다.
-L은 로컬 포워딩입니다. 내 PC의 포트로 들어온 연결이 SSH를 타고 서버 쪽 목적지로 흘러갑니다. 90%는 이걸 씁니다. -R은 리모트 포워딩이고 방향이 정확히 반대입니다. 서버의 포트로 들어온 연결이 내 PC 쪽 목적지로 옵니다. -D는 다이내믹으로, 목적지를 미리 정하지 않고 SOCKS 프록시처럼 동작합니다.
-L / -R / -D 한 줄 비교표
| 옵션 | 듣는 쪽(포트를 여는 곳) | 흐르는 방향 | 대표 용도 |
|---|---|---|---|
-L | 내 PC | 내 PC → 서버 쪽 목적지 | 막힌 DB, 내부 관리자 페이지 접속 |
-R | 원격 서버 | 서버 → 내 PC 쪽 목적지 | 로컬 개발 서버 외부 노출, 내부망 장비 역접속 |
-D | 내 PC | 내 PC → (목적지 자유) | 브라우저 전체 트래픽 우회, SOCKS 프록시 |
포트 번호를 고를 때 주의할 점
준비물은 단순합니다. SSH로 이미 접속되는 계정, 그리고 로컬에서 비어 있는 포트 하나입니다. 포트는 아무거나 되는 게 아니라 몇 가지 제약이 있습니다.
1024 미만 포트(80, 443 등)를 열려면 관리자 권한이 필요합니다. macOS나 리눅스에서 ssh -L 80:...을 그냥 치면 권한 오류가 납니다. 그래서 보통 3307, 8080, 9000처럼 여유 있는 번호를 씁니다. 원본 서비스 포트에 1을 더한 번호(3306 → 3307)를 쓰면 나중에 봐도 뭘 연결한 건지 알기 쉽습니다.
-L 로컬 포워딩: 막힌 DB와 내부 서비스에 붙기
방향 감각이 잡혔으니 가장 자주 쓰는 -L부터 직접 쳐봅니다. 예시는 서버의 MySQL에 로컬 클라이언트로 붙는 상황입니다.
ssh -L 3307:127.0.0.1:3306 user@서버주소
명령어 한 줄 분해해서 읽기
3307:127.0.0.1:3306 이 부분이 핵심입니다. 콜론으로 세 조각이 나뉘어 있습니다.
| 조각 | 의미 | 기준이 되는 컴퓨터 |
|---|---|---|
3307 | 내 PC에서 열 포트 | 내 PC |
127.0.0.1 | 목적지 주소 | 서버 입장에서 본 주소 |
3306 | 목적지 포트 | 서버 입장 |
가운데 주소는 내 PC가 아니라 서버가 해석하는 주소입니다. 여기서 대부분 헷갈립니다. 127.0.0.1이라고 쓰면 “서버 자기 자신”을 뜻합니다. 그래서 내가 로컬에서 localhost:3307로 접속하면, 그 연결이 SSH 터널을 타고 서버로 건너가서 서버의 3306에 닿습니다.
MySQL 워크벤치나 DBeaver에서는 호스트를 127.0.0.1, 포트를 3307로 넣으면 됩니다. 서버 IP를 넣는 게 아닙니다. 터널이 살아 있는 동안만 연결됩니다.

백그라운드로 띄우고 종료하기
위 명령은 터널을 만들면서 쉘도 같이 띄웁니다. 터미널 하나를 계속 잡아먹으니 불편합니다. 쉘 없이 터널만 백그라운드로 돌리려면 -N과 -f를 붙입니다.
ssh -N -f -L 3307:127.0.0.1:3306 user@서버주소
-N은 원격 명령을 실행하지 않는다는 뜻이고, -f는 인증을 마친 뒤 백그라운드로 내려가라는 뜻입니다. 문제는 프롬프트로 돌아오기 때문에 어떻게 끄는지 모른다는 점입니다. 종료는 프로세스를 찾아서 직접 죽여야 합니다.
# 터널 프로세스 찾기
ps aux | grep "ssh -N -f"
# 또는 포트로 찾기
lsof -i :3307
# PID를 확인한 뒤
kill 12345
pkill -f "3307:127.0.0.1:3306" 처럼 포워딩 문자열로 한 번에 죽이는 방법도 있습니다. 다만 비슷한 터널을 여러 개 띄웠다면 엉뚱한 걸 죽일 수 있으니, 여러 개 운영 중이라면 PID를 확인하고 죽이는 편이 안전합니다.
연결 확인 방법
터널이 제대로 섰는지는 로컬 포트가 LISTEN 상태인지로 봅니다.
lsof -i :3307
# COMMAND PID USER FD TYPE NODE NAME
# ssh 12345 user 5u IPv4 TCP localhost:3307 (LISTEN)
# lsof가 없다면
netstat -an | grep 3307
ss -tlnp | grep 3307
LISTEN이 보이면 로컬 쪽은 정상입니다. 그런데 이게 보인다고 실제 통신까지 되는 건 아닙니다. 클라이언트로 붙어봐야 알 수 있습니다. mysql -h 127.0.0.1 -P 3307 -u dbuser -p 로 한번 찔러보세요. 여기서 실패하면 터널이 아니라 서버 쪽 목적지 문제일 가능성이 큽니다.
목적지가 꼭 서버 자신일 필요는 없습니다. 서버를 발판 삼아 같은 내부망의 다른 장비로 갈 수도 있습니다.
# 서버를 경유해 내부망 10.0.1.50 의 DB로
ssh -L 3307:10.0.1.50:3306 user@서버주소
이 경우 10.0.1.50은 서버에서 접근 가능한 주소여야 합니다. 서버에 접속해서 ping 10.0.1.50이나 nc -zv 10.0.1.50 3306이 되는지 먼저 확인하는 게 순서입니다.
-R 리모트 포워딩과 -D 프록시: 방향을 뒤집는 경우
-L을 손에 익혔다면 -R은 화살표만 반대로 그리면 됩니다. 포트를 여는 쪽이 내 PC가 아니라 원격 서버라는 점만 다릅니다.
-R로 로컬 개발 서버 열어주기
내 노트북에서 localhost:3000으로 개발 서버를 돌리고 있다고 합시다. 이걸 서버의 8080 포트에 연결해 두면, 서버로 들어온 요청이 내 노트북까지 옵니다.
ssh -R 8080:localhost:3000 user@서버주소
여기서 localhost:3000은 SSH 클라이언트, 즉 내 PC 기준 주소입니다. -L에서는 가운데 주소가 서버 기준이었는데 -R에서는 내 PC 기준으로 뒤집힙니다. 이 한 가지가 -R이 헷갈리는 거의 모든 이유입니다.

GatewayPorts 설정 짚기
-R을 띄웠는데 서버에 SSH로 들어가서 curl localhost:8080은 되는데, 외부 브라우저에서 http://서버주소:8080은 안 되는 경우가 아주 흔합니다. 버그가 아니라 기본 동작입니다.
sshd는 리모트 포워딩 포트를 기본적으로 서버의 루프백(127.0.0.1)에만 바인딩합니다. /etc/ssh/sshd_config의 GatewayPorts no가 기본값이기 때문입니다. 외부에서 접근하게 하려면 서버 설정을 바꿔야 합니다.
sudo vi /etc/ssh/sshd_config
GatewayPorts yes
# 또는 클라이언트가 지정한 경우에만 허용
# GatewayPorts clientspecified
sudo systemctl restart sshd
# 데비안/우분투 계열은 ssh 인 경우도 있습니다
sudo systemctl restart ssh
clientspecified로 두면 ssh -R 0.0.0.0:8080:localhost:3000 ... 처럼 클라이언트가 바인딩 주소를 명시할 때만 외부에 열립니다. 이쪽이 더 안전합니다. 물론 서버 방화벽(ufw, 보안 그룹)에서 8080도 함께 열려 있어야 합니다.
-D로 SOCKS 프록시 쓰기
-D는 목적지를 정하지 않습니다. 내 PC에 SOCKS5 프록시를 하나 띄우고, 그 프록시로 들어온 모든 요청을 서버가 대신 처리하게 합니다.
ssh -D 1080 user@서버주소
# 백그라운드로 돌리려면
ssh -N -f -D 1080 user@서버주소
브라우저에서 SOCKS5 호스트를 127.0.0.1, 포트를 1080으로 지정하면 웹 트래픽이 서버를 경유합니다. 터미널 도구는 이렇게 씁니다.
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me
여기서 --socks5-hostname을 쓴 이유가 있습니다. -D는 만능이 아닙니다. 그냥 --socks5나 일부 브라우저 기본 설정에서는 DNS 이름 해석을 로컬에서 먼저 해버려서, 트래픽은 서버를 타는데 어디에 접속하려는지는 로컬 네트워크에 그대로 새어 나갑니다. 파이어폭스라면 about:config에서 network.proxy.socks_remote_dns를 true로 바꿔야 합니다. 크롬은 확장이나 실행 옵션이 필요합니다. 이 부분은 클라이언트마다 동작이 달라서, 프라이버시가 중요한 용도라면 실제로 새는지 직접 확인해 보고 쓰는 게 맞습니다.
자주 막히는 지점과 에러 메시지별 대처
명령은 맞게 쳤는데 안 붙는다면, 대부분 아래 몇 가지 중 하나입니다. 에러 메시지 원문을 그대로 보고 갈래를 나누는 게 가장 빠릅니다.
bind: Address already in use
로컬에서 열려는 포트를 이미 다른 프로세스가 쓰고 있습니다. 앞서 띄운 터널이 안 죽고 남아 있는 경우가 제일 많습니다.
lsof -i :3307
kill <PID>
# 아니면 다른 포트로 우회
ssh -L 3308:127.0.0.1:3306 user@서버주소
channel 2: open failed: connect failed: Connection refused
터널 자체는 만들어졌는데, 서버가 목적지에 연결하지 못한 겁니다. 즉 SSH 문제가 아니라 목적지 서비스 문제입니다. 서버에 들어가서 그 서비스가 진짜 떠 있는지, 어떤 주소에 바인딩돼 있는지 확인합니다.
# 서버에서
ss -tlnp | grep 3306
127.0.0.1:3306으로 떠 있는데 터널 목적지를 서버의 사설 IP로 지정했다면 거부됩니다. 반대로 도커 컨테이너로 돌리면 호스트 루프백이 아니라 브리지 네트워크에 있는 경우도 있습니다. 목적지 주소를 127.0.0.1과 실제 IP로 바꿔가며 시도해 보세요.
Warning: remote port forwarding failed for listen port 8080
-R을 쓸 때 나옵니다. 서버에서 이미 8080을 누군가 쓰고 있다는 뜻입니다. 앞서 붙였다가 비정상 종료된 터널이 남아 있는 경우가 흔합니다. 서버에 들어가 ss -tlnp | grep 8080으로 확인하고 정리하거나, 다른 포트를 쓰면 됩니다.
터널은 붙는데 몇 분 뒤 조용히 끊기는 경우
공유기나 클라우드 NAT가 유휴 연결을 정리해서 그렇습니다. 클라이언트가 주기적으로 신호를 보내게 하면 대부분 잡힙니다.
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-N -f -L 3307:127.0.0.1:3306 user@서버주소
이 설정을 왜 넣는지, 서버 쪽 ClientAliveInterval과는 뭐가 다른지는 SSH 접속이 자꾸 끊길 때, timeout 설정으로 잡는 법에 따로 정리해 뒀습니다.
아무 에러 없이 그냥 안 되는 경우
관리자가 /etc/ssh/sshd_config에서 AllowTcpForwarding no로 막아둔 서버일 수 있습니다. 공용 호스팅이나 사내 점프 서버에 종종 걸려 있습니다. 이 경우 클라이언트에서 할 수 있는 우회는 없습니다. 서버 설정을 yes로 바꾸고 sshd를 재시작하거나, 권한이 없다면 관리자에게 요청하는 수밖에 없습니다.
매번 긴 명령 치기 싫다면: config에 넣고 상시 운영하기
에러를 다 잡았다면 이제 남는 문제는 손가락입니다. ssh -N -f -L 3307:127.0.0.1:3306 -o ServerAliveInterval=30 user@10.20.30.40 같은 명령을 매번 치다 보면 오타가 납니다. ~/.ssh/config에 넣어두면 됩니다.
Host db-tunnel
HostName 10.20.30.40
User deploy
IdentityFile ~/.ssh/id_ed25519
LocalForward 3307 127.0.0.1:3306
ServerAliveInterval 30
Host demo-share
HostName example.com
User deploy
RemoteForward 8080 localhost:3000
Host proxy
HostName example.com
User deploy
DynamicForward 1080
주의할 점은 구분자입니다. 커맨드라인에서는 3307:127.0.0.1:3306처럼 콜론으로 다 붙이지만, config에서는 첫 번째 포트와 나머지를 공백으로 띄웁니다. LocalForward 3307 127.0.0.1:3306 이 형태입니다. 여기서 콜론으로 다 붙여 쓰면 조용히 무시되거나 파싱 에러가 납니다.
이제 ssh -N -f db-tunnel 한 줄이면 끝입니다. config 파일 자체를 처음 쓴다면 SSH config 파일로 서버 별칭 등록하기 쪽을 먼저 보고 오는 게 순서가 맞습니다.
터널을 하루 종일 살려둬야 한다면 autossh 같은 도구가 있습니다. 연결이 끊기면 자동으로 다시 붙여줍니다. 다만 상시 운영이 필요한 수준이라면 터널보다 VPN이나 정식 방화벽 규칙을 검토하는 게 맞는 경우도 많습니다.
마지막으로 보안 이야기를 하나 덧붙입니다. 터널은 본질적으로 접근 통제를 우회하는 도구입니다. 누군가 3306을 막아둔 데는 이유가 있습니다. 개인 서버라면 상관없지만, 회사나 공용 서버에서는 정책을 확인하고 쓰세요. 특히 GatewayPorts yes와 -R 조합은 인증 없는 구멍을 인터넷에 내놓는 것과 같습니다.
다음에 할 일 체크리스트
- [ ]
ssh user@서버주소가 문제없이 되는지 먼저 확인 (터널은 그다음) - [ ] 로컬에서 쓸 포트를
lsof -i :포트로 비어 있는지 확인 - [ ]
ssh -L 3307:127.0.0.1:3306 user@서버주소로 포그라운드 테스트 - [ ] 서버에서
ss -tlnp | grep 3306으로 목적지 서비스 바인딩 주소 확인 - [ ] 잘 되면
-N -f로 백그라운드 전환, 종료용 PID 확인 방법 익혀두기 - [ ]
-R을 쓸 계획이면sshd_config의GatewayPorts값 먼저 확인 - [ ] 끊김이 잦으면
ServerAliveInterval추가 - [ ] 자주 쓰는 터널은
~/.ssh/config의LocalForward로 옮기기 - [ ] 비밀번호 입력이 귀찮다면 키 인증부터 설정 — SSH 키 인증 설정하는 법
