TL;DR

ssh -L [로컬포트]:[접속대상주소(원격서버기준)] [사용자명]@[호스트주소]
ssh -L 8080:127.0.0.1:80 root@172.17.0.11

ssh 포트 포워드란

ssh 포트 포워드는 ssh 터널을 이용해 로컬 환경과 원격 서버 사이에 데이터를 전송할 수 있도록 하는 방법이다. 암호화 등 ssh의 장점을 활용할 수 있고, 방화벽 등으로 인해 외부에서 접근할 수 없는 포트에 우회적으로 접근할 수 있다.

ssh 포트 포워드는 3가지 종류가 있다.

  • 로컬 포트 포워드: 로컬 머신의 특정 포트로 들어오는 연결을 원격 서버의 포트로 전달한다.
  • 리모트 포트 포워드: 원격 서버의 특정 포트로 들어오는 연결을 로컬 컴퓨터의 포트로 전달한다.
  • 다이다믹 포트 포워드: ssh 클라이언트가 sock 프록시를 서버 역할을 한다.

이 중 로컬 포트 포워드에 대해 알아보려고 한다.

일반적인 ssh 접속 명령어는 아래와 같다.

ssh [사용자명]@[호스트주소]

ssh 접속 명령어에 -L 옵션을 추가하면 로컬 포트 포워딩이 가능하다.

ssh -L [로컬포트]:[접속대상주소(원격서버기준)] [사용자명]@[호스트주소]

실습 예제

긴 설명보다는 실습 예제를 해보는게 이해하는데 더 도움이 될 것 같아서 예제를 준비했다. 예제에서는 도커 컨테이너를 활용하고 있으나 VM, 베어본, 클라우드 서비스 등 다른 환경을 활용해도 무방하다.

예제에서는 client 역할과 server 역할의 컨테이너를 각각 생성한다. server에서는 nginx를 실행시켜서 80포트에 할당하고 config를 수정해서 외부로부터의 접근을 차단시킨다. 그리고 client에서 ssh 터널을 통한 접근과 일반적인 접근 시 어떤 차이점이 발생하는지 확인해보려고 한다.

먼저 client 역할의 컨테이너와 server 역할의 컨테이너를 각각 생성한다.

$ sudo docker run --name client -d ubuntu:22.04 tail -f /dev/null
$ sudo docker run --name server -d ubuntu:22.04 tail -f /dev/null

server 컨테이너에서 필요한 것들을 설치한다.

$ sudo docker exec -it server bash
server$ apt update
server$ apt install -y curl vim openssh-server nginx

nginx를 실행시키고 80포트에 바인딩한다. 먼저 nginx config를 수정해준다.

server$ vi /etc/nginx/sites-available/default

[ before ]

...
        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }
...

[ after ]

...
        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
                allow 127.0.0.1; # <-- Add here
                deny all; # <-- Add here
        }
...

기본적으로 80포트에 바인딩하도록 되어있으므로 포트 설정은 수정할 필요 없다. 127.0.0.1 외 다른 곳으로부터의 접근은 차단하는 규칙을 추가했다. config 수정이 완료되었으면 nginx를 실행시키자.

server$ nginx

잘 작동하는지 curl 명령어를 통해 확인해보자.

server$ curl <http://127.0.0.1:80>

아래와 같이 html 문서가 response로 오면 잘 작동하고 있는 것이다.

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="<http://nginx.org/>">nginx.org</a>.<br/>
Commercial support is available at
<a href="<http://nginx.com/>">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

ssh 접속이 가능하도록 openssh-server도 실행시키자.

먼저 root 계정의 패스워드를 설정한다. 적당한 걸로 입력하고 기억해두자.

server$ passwd
New password:
Retype new password:

sshd cofing도 수정해서 root 계정으로 로그인이 가능하도록 하자.

server$ vi /etc/ssh/sshd_config
...
PermitRootLogin yes # <-- Add here
...

ssh 서버를 실행시킨다.

server$ service ssh start

ssh 서버가 잘 작동하는지 확인해보자

server$ ssh root@127.0.0.1

다음은 server 컨테이너의 ip를 확인해보자. 본 예제에서는 172.17.0.11 가 server 컨테이너의 ip이다.

$ sudo docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' server

다음에는 client 컨테이너에서 필요한 것을을 준비해보자.

$ sudo docker exec -it client bash
client$ apt update
client$ apt install -y curl openssh-client

server 컨테이너의 80 포트로 request를 날려보자.

client$ curl <http://172.17.0.11:80>

아래와 같이 403 페이지가 response로 오면 정상이다. 127.0.0.1에서 접근한 것이 아니기 때문이 차단이 된다.

<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

이제 로컬 포트 포워딩을 통해 ssh 터널을 열어보자

client$ ssh -L 8080:127.0.0.1:80 root@172.17.0.11

비밀번호를 입력하고 나면 server 컨테이너로 ssh 접속이 된다. 이상하다고 생각할 필요 없다. 정상이다. ssh 터널은 ssh 접속이 유지되는 동안에만 유지된다.

그림때문에 오해할까봐 적자면, ssh 터널이 생성되었다고 ssh 접속이 불가능해지는건 아니다. 그리고 ssh 터널은 동시에 여러 개 생성하는 것도 가능하다.

로컬 포트 포워딩이 잘 되었는지 확인하기 위해서 터미널을 하나 더 열고 client 컨테이너에서 로컬 포트로 request를 날려보자.

$ sudo docker exec -it client bash
client$ curl <http://localhost:8080>

아마 아래와 같은 html을 response를 받을 수 있을 것이다.

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="<http://nginx.org/>">nginx.org</a>.<br/>
Commercial support is available at
<a href="<http://nginx.com/>">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

만약 ssh 포트 포워딩을 종료하면 어떻게 될까? 아까 client 컨테이너에서 server 컨테이너로 ssh 접속한 터미널에서 ssh 연결을 종료해보자.

server$ exit
client$

그리고 다시 client 컨테이너에서 request를 날려보자.

client$ curl <http://localhost:8080>

아마도 아래처럼 connection refused가 발생할 것이다.

curl: (7) Failed to connect to localhost port 8080 after 0 ms: Connection refused

추가 정보 #1

특정 서버와 포트에 고정적으로 ssh 터널링을 자주 하는 상황이라면 ssh config에 등록하는 것이 편리하다.

예를 들어 아래와 같이 ssh 로컬 포워딩 명령어를 자주 실행한다고 하자.

ssh -L 8080:127.0.0.1:80 root@172.17.0.11

~/.ssh/config 에 아래와 같은 내용을 추가하면 된다.

Host my-tunnel
    User root
    HostName 172.17.0.11
    LocalForward 8080 127.0.0.1:80

그리고 터미널에서 아래와 같이 명령어를 입력하자.

client$ ssh my-tunnel

추가 정보 #2

ssh 터널의 목적지가 127.0.0.1일 필요는 없다. client에서는 접근할 수 없지만 server에서는 접근 가능한 곳을 터널 목적지로 설정해서 제 3의 목적지에 접근할 수 있도록 하는 것도 가능하다.

client$ ssh -L 8080:172.16.0.12:80 root@172.17.0.11

추가 정보 #3

앞에서 통신이 암호화된다고 설명했는데, 이게 보안성이 뛰어나 안전하다고만 할 수는 없다. 전송 데이터가 암호화되었다는 측면에서는 안전한 것이 맞다. 그러나 ssh 터널을 생성하면 일종의 개구멍을 뚫어 놓는 것과 마찬가지라서 방화벽 등으로 인해 접속이 불가능한 곳도 접속이 가능해지기 때문에 주의가 필요하다.

참조

https://blog.naver.com/PostView.naver?blogId=alice_k106&logNo=221364560794

https://builtin.com/software-engineering-perspectives/ssh-port-forwarding

ℹ️
soonbee(권순재)
한국 서버 개발