-L、-R、-Dを混同しない方法
한국어 원문으로 표시합니다.
한 줄 요약
-L 은 내 쪽 포트를 열어 저쪽으로 보내고, -R 은 저쪽 포트를 열어 내 쪽으로 끌어오고, -D 는 내 쪽에 SOCKS 프록시를 세운다. 방향만 정확히 잡으면 나머지는 문법이다.
왜 이게 필요했나
내부망 DB 에 붙어야 하는데 그 DB 는 바깥에서 접근할 수 없다. 배스천 서버에는 SSH 로 붙을 수 있다. 이때 필요한 것이 로컬 포워딩이다. 내 노트북의 15432 로 들어온 연결을 배스천을 거쳐 db.internal:5432 로 보낸다.
ssh -L 15432:db.internal:5432 bastion
읽는 법: -L <내가 열 포트>:<배스천이 볼 때의 목적지>:<그 포트>. 중간의 호스트 이름은 SSH 서버 기준으로 해석된다는 점이 핵심이다. 내 노트북에서 db.internal 이 안 풀려도 상관없다.
어떻게 동작하나
| 옵션 | 포트가 열리는 곳 | 트래픽 방향 | 대표 용도 |
|---|---|---|---|
-L |
로컬 | 로컬 → SSH 서버 → 목적지 | 내부망 DB, 관리 콘솔 접근 |
-R |
원격(SSH 서버) | 원격 → SSH 서버 → 로컬 → 목적지 | 방화벽 뒤 서비스를 밖에 노출, 웹훅 수신 |
-D |
로컬(SOCKS) | 애플리케이션이 고른 임의 목적지 | 브라우저 전체를 내부망 경유 |
터널 전용 연결에는 -N -f 를 함께 쓴다. -N 은 원격 명령을 실행하지 않고, -f 는 백그라운드로 보낸다.
ssh -N -f -L 15432:db.internal:5432 bastion
-R 에는 함정이 하나 있다. 기본적으로 SSH 서버의 루프백에만 바인딩된다. 즉 서버의 다른 사람은 그 포트를 못 쓴다. 밖에 열려면 서버에서 GatewayPorts yes 를 켜야 하고, 이건 보안상 신중해야 한다.
서버 쪽에서 포워딩을 통제하는 지시어들.
AllowTcpForwarding no
AllowAgentForwarding no
GatewayPorts no
PermitOpen 10.0.5.20:5432
PermitOpen 이 특히 실용적이다. 포워딩을 완전히 막지 않으면서 목적지를 화이트리스트로 제한한다. "개발자가 운영 DB 에는 붙어야 하지만 다른 내부 서비스에는 접근하면 안 되는" 상황에 딱 맞는다.
에이전트 포워딩은 위험하다
-A(에이전트 포워딩)는 편리하지만, 경유 서버의 root 권한을 가진 사람이 내 에이전트 소켓을 통해 내 키로 다른 서버에 인증할 수 있다. 배스천 경유가 목적이라면 ProxyJump(-J)를 써라. 에이전트를 노출하지 않고 같은 결과를 얻는다.
ssh -J bastion deploy@10.0.3.14
이스케이프 시퀀스
터널을 쓰다 보면 연결이 멈춘 것처럼 보일 때가 있다. 이때 쓰는 것이 이스케이프 시퀀스다. 줄 맨 처음에서 ~ 를 누른 다음 문자를 입력한다.
| 시퀀스 | 동작 |
|---|---|
~. |
연결 강제 종료 (멈춘 세션 탈출) |
~^Z |
SSH 를 백그라운드로 |
~# |
포워딩 중인 연결 목록 |
~C |
커맨드라인 열기 (실행 중에 -L 추가/삭제) |
~? |
도움말 |
~C 로 접속 중에 포워딩을 추가할 수 있다는 것을 아는 사람이 의외로 적다. 세션을 끊지 않고 터널을 하나 더 파야 할 때 유용하다.
세 방향을 헷갈리지 않는 법
-L, -R, -D 는 문자만 다르고 하는 일이 아예 다르다. 어느 쪽에서 듣고
어느 쪽에서 나가는지만 잡으면 헷갈리지 않는다.
| 옵션 | 듣는 곳 | 나가는 곳 | 쓰는 상황 |
|---|---|---|---|
-L 5432:db:5432 |
내 컴퓨터 | 원격 서버 | 내가 못 닿는 DB 에 붙는다 |
-R 8080:localhost:3000 |
원격 서버 | 내 컴퓨터 | 내 개발 서버를 밖에서 보게 한다 |
-D 1080 |
내 컴퓨터(SOCKS) | 원격 서버 | 여러 목적지를 한 번에 |
-L 의 db:5432 는 원격 서버가 보는 이름이다. 내 hosts 파일이 아니라
서버의 DNS 로 풀린다. 여기서 어긋나는 경우가 가장 많다.
왜 원격 포트가 밖에서 안 열리나
-R 8080:localhost:3000 을 걸어 놓고 다른 장비에서 접속했는데 거절당하는 것은
설정 문제가 아니라 기본값이다. sshd 는 GatewayPorts no 아래에서 전달 포트를
원격 서버의 루프백에만 연다. 서버의 /etc/ssh/sshd_config 에서
GatewayPorts clientspecified 로 바꾸고 -R 0.0.0.0:8080:localhost:3000 처럼
주소를 명시해야 밖으로 열린다. 이 값을 켜는 것은 그 서버에 접속할 수 있는
사람이라면 누구나 내 노트북의 포트를 인터넷에 노출할 수 있게 한다는 뜻이므로,
공용 점프 서버에서는 켜지 않는다.
끊긴 터널을 되살리는 것은 ssh 가 아니다
터널은 반드시 끊긴다. NAT 표가 만료되고, 무선이 끊기고, 서버가 재시작한다.
ssh 자체는 재접속하지 않으므로 다시 여는 일은 밖에서 맡는다.
autossh -M 0 -N \
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-L 5432:db.internal:5432 jump.example.com
ServerAliveInterval 은 조용한 연결이 죽었는지 확인하는 장치다. 15초마다
물어보고 세 번 답이 없으면 끊는다. ExitOnForwardFailure=yes 는 포트를 못 열었을
때 접속을 성공으로 치지 않게 한다. 이것을 빼면 터널 없이 살아 있는 ssh 가
남아서, 연결은 되어 있는데 아무것도 안 되는 상태가 된다.
다중화로 접속을 아낀다
같은 서버에 반복해서 붙는다면 연결 하나를 나눠 쓴다.
Host jump
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
두 번째 접속부터는 핸드셰이크와 인증을 건너뛰므로 체감이 크게 달라진다.
다만 ControlPath 는 소켓 파일이라, 홈 디렉터리가 NFS 위에 있으면 동작하지
않는다. 그럴 때는 /run/user/$UID 아래로 옮긴다.
현장에서 만나는 모습
터널을 열어 두고 잊는다. -N -f 로 백그라운드에 보낸 ssh 프로세스는 조용히 살아 있다. 몇 주 뒤 "누가 이 포트 열었지?" 가 된다. ss -ltnp 에서 프로세스가 ssh 로 보이면 터널이다. 팀 규칙으로 ControlPath 나 프로세스 이름에 용도를 남기는 곳도 있다.
다음 실습에서 할 것
로컬·원격·동적 세 방향 포워딩을 전부 만들어 보고, ss 로 각 터널이 어디에 포트를 열었는지 확인한다. 마지막에는 세 방식의 비교표를 실제 증거와 함께 제출한다.