열어 두고 잊어버린 포트는 서버에 침입하는 고전적인 경로입니다. 공개 주소에 있는 데이터베이스, 디버그 대시보드, 아무도 원하지 않은 포트를 게시한 컨테이너. 명령 두 개와 함정 하나만 알면 확인은 1분이면 끝납니다. 아래 내용은 모두 당사 이미지로 만든 새 Ubuntu 24.04 서버에서 실행하고 두 번째 머신에서 스캔한 것이므로, 출력은 여러분의 서버에서 보게 될 것과 같습니다.
1. 무엇이 대기 중인가
명령은 ss입니다. 옵션 글자는 TCP, UDP, 대기 중인 소켓, 숫자 포트, 프로세스 이름을 뜻합니다.
sudo ss -tulpn
새 서버에서는 6줄이 출력됩니다.
Netid State Local Address:Port Process
udp UNCONN 127.0.0.54:53 systemd-resolve
udp UNCONN 127.0.0.53%lo:53 systemd-resolve
tcp LISTEN 127.0.0.54:53 systemd-resolve
tcp LISTEN 127.0.0.53%lo:53 systemd-resolve
tcp LISTEN 0.0.0.0:22 sshd
tcp LISTEN [::]:22 sshd
중요한 열은 Local Address입니다. 누가 접속할 수 있는지 알려 줍니다.
| 주소 | 접근 가능한 대상 |
|---|---|
127.0.0.1:… 또는 127.0.0.53 | 이 서버의 프로그램만 |
0.0.0.0:… | 방화벽이 막지 않으면 IPv4로 누구나 |
[::]:… | IPv6로 누구나(보통 IPv4도) |
10.0.0.5:…(특정 주소) | 그 주소에 닿을 수 있는 누구나 |
즉 새 서버가 외부에 노출하는 것은 IPv4와 IPv6의 포트 22 SSH 하나뿐입니다. 127.0.0.53의 DNS 리졸버는 비공개입니다.
인터넷에서 보일 수 있는 것만 나열하려면 루프백 주소를 걸러 냅니다.
sudo ss -tlnp | awk 'NR>1 && $4 !~ /^(127\.|\[::1\])/ {print $4, $6}'
설치할 때마다 실행하세요. nginx, Docker, 작은 Python 앱을 설치했더니 목록이 2줄에서 10줄로 늘었고, 그중에는 0.0.0.0:8081의 docker-proxy와 임의 localhost 포트의 containerd 소켓이 있었습니다.
2. 어떤 프로세스가 포트를 쓰고 있나
포트가 사용 중이고 누가 쓰는지 알고 싶을 때:
sudo lsof -i :80 -P -n
sudo fuser 80/tcp
sudo ss -tlnp 'sport = :80'
가장 읽기 쉬운 것은 lsof입니다. nginx의 경우 root인 마스터 프로세스와 www-data인 워커가 IPv4와 IPv6 모두 *:80에 표시되었습니다. fuser는 PID만 출력해 스크립트에 편합니다. 포트를 비워야 한다면 PID를 kill하지 말고 서비스를 멈추세요. 그렇지 않으면 systemd가 다시 시작합니다: sudo systemctl stop nginx.
3. 인터넷에서는 무엇이 보이나
ss는 서버의 관점을 보여 줍니다. 인터넷의 관점은 다를 수 있습니다. 중간에 방화벽이 있거나 서비스가 127.0.0.1에 바인딩되어 있기 때문입니다. 실제 모습을 보려면 서버를 다른 머신에서 스캔하세요. 노트북이나 두 번째 서버면 됩니다. 도구는 nmap입니다(apt install nmap, Windows와 macOS는 nmap.org의 설치 프로그램).
nmap your-server-ip
nmap -p- your-server-ip
nmap -p 22,80,443,3306 your-server-ip
첫 번째 형식은 가장 흔한 1,000개 포트, 두 번째는 65,535개 전체, 세 번째는 지정한 목록을 검사합니다. 방화벽을 켜기 전 새 서버의 결과입니다.
Not shown: 999 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
Nmap done: 1 IP address (1 host up) scanned in 6.83 seconds
65,535개 포트 전체 스캔은 8.9초 걸렸고 같은 하나의 포트를 찾았습니다. 닫힌 포트는 즉시 리셋으로 응답하므로 스캔이 빠릅니다.
중요한 상태는 세 가지입니다.
- open: 프로그램이 연결을 받아들였습니다. 이것이 공격 표면입니다.
- closed: 그곳에서 아무것도 대기하지 않는다고 서버가 응답했습니다. 안전하지만 서버가 존재하며 무엇을 실행하지 않는지 드러납니다.
- filtered: 응답이 없습니다. 방화벽이 패킷을 버렸습니다. 스캐너는 그 뒤에 서비스가 있는지 알 수 없습니다.
아무것도 설치하지 않고 포트 하나를 확인하려면 nc -zv 서버IP 443이 succeeded 또는 refused를 반환하고, 필터된 포트에서는 타임아웃까지 기다립니다. UDP는 nmap에 -sU를 붙이되 오래 걸린다고 각오하세요. 당사 서버에서는 흔한 UDP 포트 53, 123, 161이 모두 closed였습니다.
NAT 요금제에 대한 참고: 인터넷에서는 당사가 전달해 드린 SSH 포트에만 닿습니다. 공유 주소를 스캔하면 여러분의 서버가 아니라 당사 네트워크의 경계가 보이며, 다른 포트는 무엇이 대기하든 접근할 수 없습니다. 전용 IPv4 주소가 있는 요금제에서는 열린 포트를 모두 직접 관리합니다.
4. 필요 없는 것은 닫기
가장 좋은 방법부터 가장 넓은 방법까지 세 가지입니다.
localhost에 바인딩하기. 서버 안의 프로그램만 쓰는 서비스라면 127.0.0.1에서 대기하게 하세요. --bind 127.0.0.1 8080으로 작은 웹 앱을 실행했더니 ss에는 127.0.0.1:8080으로 나왔고, 외부의 nmap은 8080/tcp closed를 보고했습니다. 데이터베이스에는 이것이 올바른 기본값입니다. Ubuntu의 Redis, MySQL, PostgreSQL은 설치 직후부터 localhost에서 대기하며, 당사의 Redis와 MySQL 가이드도 그대로 둡니다.
서비스 멈추기. sudo systemctl disable --now 이름은 대기 중인 리스너를 완전히 없앱니다. ss로 사라졌는지 확인하세요.
방화벽 켜기. 먼저 SSH를, 그다음 제공하는 서비스를 허용한 뒤 활성화합니다.
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw enable
sudo ufw status verbose
외부에서 본 스캔 결과가 완전히 달라졌습니다.
Not shown: 997 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8081/tcp open blackice-icecap
Nmap done: 1 IP address (1 host up) scanned in 10.18 seconds
허용하지 않은 포트는 모두 closed에서 filtered로 바뀌었고, 65,535개 포트 전체 스캔은 8.9초에서 110초로 늘었습니다. 스캐너는 포트마다 더 오래 걸리고, 서버에 대한 정보는 줄어듭니다. 당사의 UFW 가이드에서 속도 제한, 특정 IP만 허용, IPv6를 다룹니다.
하지만 세 번째 줄을 보세요.
5. Docker의 함정
방화벽이 켜져 있고 허용하는 규칙도 없는데 포트 8081은 열려 있었습니다. 이 포트는 docker run -p 8081:80 nginx:alpine으로 실행한 컨테이너의 것입니다. Docker는 커널 방화벽에 자체 규칙을 추가하고, 이 규칙은 ufw 규칙보다 먼저 평가됩니다. 당사 서버에 기록된 규칙입니다.
-A DOCKER ! -i docker0 -p tcp --dport 8081 -j DNAT --to-destination 172.17.0.2:80
포트 8081로 오는 패킷은 ufw가 판단하기도 전에 모두 컨테이너로 전달됩니다. -p나 ports: 항목이 있는 모든 컨테이너가 이렇게 하며, ss는 이를 정직하게 0.0.0.0:8081의 docker-proxy로 보여 줍니다.
해결책은 localhost에만 게시하는 것입니다.
docker rm -f web
docker run -d --name web -p 127.0.0.1:8081:80 nginx:alpine
Compose에서는 ports:에 "127.0.0.1:8081:80"이라고 씁니다. 변경 후 ss는 127.0.0.1:8081을 보여 주었고, Docker 규칙에는 -d 127.0.0.1/32가 붙었으며, 외부 스캔은 8081/tcp filtered를 보고했습니다. 공개 쪽에는 nginx를 리버스 프록시로 HTTPS와 함께 앞에 두어, 서버에서 열린 포트가 22, 80, 443뿐이 되게 하세요.
변경할 때마다 확인할 것
sudo ss -tulpn:0.0.0.0이나[::]가 있는 줄은 모두 공개하려던 것인가요?- 노트북에서
nmap 서버IP: 열린 포트가 일치하나요? - 공개 주소에 있는 데이터베이스, 캐시, 관리 패널: 오늘 바로
127.0.0.1이나 방화벽 안쪽으로 옮기세요. - Docker의 각
ports:항목: 공개 웹 포트가 아니라면127.0.0.1:로 시작하나요? - SSH가 모두에게 열려 있다면: fail2ban이나 키 전용 로그인을 추가하세요.
Windows Server에서 같은 확인은 Get-NetTCPConnection -State Listen이며, 규칙은 Windows 방화벽에 있습니다. $3 Nano부터 모든 Linux 요금제에는 전용 IPv4 주소가 있는 버전이 있으며, 그곳에서는 ss에 보이는 것이 방화벽이 막는 것을 빼고 그대로 전 세계에 보입니다.
댓글
아직 댓글이 없습니다. 첫 번째가 되세요.