서버 보안 점검 1편: 외부 노출 포트와 방화벽 상태 확인
셀프호스팅 서버의 보안 점검은 애플리케이션 설정부터 보기보다 인터넷에서 실제로 무엇이 보이는지 먼저 확인하는 편이 좋다. 웹 서비스만 공개하려 했는데 Docker 관리 포트, 데이터베이스, 프록시 관리자 화면이 함께 노출되어 있다면 내부 설정을 아무리 잘해도 공격 표면은 넓어진다.
이 글은 실제로 진행한 보안 점검 가운데, 다음 관리 서비스 점검을 시작하기 직전까지의 기록을 다시 정리한 연재의 첫 편이다. 실제 서버명, 공인·사설 IP, 관리 도메인, 비표준 포트와 사용자명은 공개하지 않는다.
세 줄 요약
- 외부 점검에서 웹 서비스용 포트는 예상대로 열려 있었고, 확인한 관리·데이터 계열 포트는
filtered로 관측됐다. - Nmap의
filtered는 “닫힘”이 아니라 필터 때문에 열림·닫힘을 판별하지 못했다는 뜻이다. - 외부 스캔 한 번만으로 끝내지 말고 서버의 리스닝 소켓, Docker 게시 포트, 호스트·클라우드 방화벽을 대조해야 한다.
점검 범위와 판정 기준
이 글의 결과는 보존된 대화 기록과 출력에 근거한다. 정확한 실행 시각, 스캔 출발지, IPv6 점검 여부와 당시 방화벽 규칙 전문은 남아 있지 않다. 따라서 확인되지 않은 부분은 성공으로 채우지 않고 확인 필요로 표시한다.
| 판정 | 의미 |
|---|---|
| 성공 | 보존된 출력에서 기대한 상태가 직접 확인됨 |
| 부분 성공 | 보호 상태는 보였지만 다른 경로나 종단 간 동작이 미확인 |
| 실패 | 명령 오류 또는 기대 결과와 명확히 다른 상태가 확인됨 |
| 확인 필요 | 출력이 없거나 잘렸거나 당시 수행 여부를 확인할 수 없음 |
실제 점검 결과
| 항목 | 결과 | 판정 | 해석 |
|---|---|---|---|
| 공개 웹 경로 | 표준 HTTP·HTTPS 포트가 open |
성공 | 의도한 웹 서비스가 외부 연결을 받고 있었음 |
| 관리 화면 후보 | 점검한 관리 포트가 filtered |
부분 성공 | 외부 스캔 지점에서 포트 상태를 확인할 수 없도록 필터링됨 |
| 데이터 계열 후보 | 점검한 DB·Docker 관리 포트가 filtered |
부분 성공 | 직접 공개된 흔적은 없었지만 내부 리스닝 여부와는 별개 |
| 사설 오버레이 네트워크 | 활성 상태 확인 | 성공 | 관리 통신을 공용망과 분리할 기반이 있었음 |
| IPv6 외부 노출 | 보존된 결과 없음 | 확인 필요 | IPv4 결과를 IPv6까지 확대 해석할 수 없음 |
| 방화벽 규칙 교차 확인 | 규칙 전문이 보존되지 않음 | 확인 필요 | 클라우드·호스트·Docker 규칙을 다시 대조해야 함 |
확정된 침해나 명확한 외부 관리 포트 노출 실패는 보존 기록에서 확인되지 않았다. 그러나 filtered만 보고 “포트가 닫혀 있다”거나 “완전히 안전하다”고 결론 내릴 수도 없다.
Nmap 공식 문서에 따르면 포트 상태는 포트 자체의 영구 속성이 아니라 해당 위치와 방식으로 스캔했을 때 Nmap이 본 상태다. 같은 포트도 내부망과 인터넷에서 다르게 보일 수 있다.
재현용 점검 명령 1: 외부에서 스캔한다
아래 명령은 당시 터미널 원문이 아니라, 같은 항목을 다시 확인하기 위한 재현용 읽기 전용 명령이다. 점검 권한이 있는 자신의 서버에만 사용한다.
먼저 실제 값을 로컬 셸에만 넣는다. 이 값이 들어간 명령 기록과 스크린샷은 게시하지 않는다.
set +x
export PUBLIC_DOMAIN='<PUBLIC_DOMAIN>'
export PORT_SET='80,443,<SSH_PORT>,<NPM_ADMIN_PORT>,<DB_PORT>,<DOCKER_API_PORT>,<PORTAINER_PORT>'
: "${PUBLIC_DOMAIN:?PUBLIC_DOMAIN을 설정하세요}"
: "${PORT_SET:?PORT_SET을 설정하세요}"
서버와 다른 외부 네트워크에서 필요한 포트만 지정해 확인한다.
nmap -Pn -sT -p "$PORT_SET" "$PUBLIC_DOMAIN"
결과를 공개할 때는 IP, 실제 비표준 포트, 서비스 버전과 스캔 출발지를 지우고 다음 정도로만 요약한다.
public_web_ports=open
management_ports=filtered
database_ports=filtered
상태는 다음처럼 읽는다.
open: 대상 애플리케이션이 그 포트에서 연결을 받고 있다.closed: 대상은 도달 가능하지만 그 포트에서 듣는 애플리케이션이 없다.filtered: 방화벽이나 다른 네트워크 장치 때문에 Nmap이open과closed를 구분하지 못했다.
한 번의 외부 스캔은 그 시점·그 출발지의 관찰 결과다. 다른 네트워크, IPv6, 재부팅 이후에도 같은지는 별도로 확인한다.
재현용 점검 명령 2: 서버 내부 리스닝 포트를 본다
외부에서 보이지 않더라도 서비스가 모든 인터페이스에 리스닝 중일 수 있다. 서버에서는 소켓 목록을 확인하되, 원문 전체는 내부 IP와 프로세스 정보를 드러낼 수 있으므로 공개하지 않는다.
sudo ss -lntup
확인 기준은 다음과 같다.
- 공개 웹 프록시만
0.0.0.0또는[::]에 바인딩되어 있는가 - 관리 UI가
127.0.0.1또는 승인한 사설 오버레이 주소에만 바인딩되어 있는가 - DB가 예상하지 않은 공용 인터페이스에서 듣고 있지 않은가
- 사용하지 않는 서비스가 남아 있지 않은가
원문 대신 주소 유형별 개수만 기록하려면 다음처럼 축약할 수 있다.
sudo ss -lntH |
awk '{print $4}' |
awk '
/^127\./ || /^\[::1\]:/ {loopback++ ; next}
/^0\.0\.0\.0:/ || /^\[::\]:/ {wildcard++ ; next}
{specific++}
END {
printf "loopback=%d wildcard=%d specific=%d\n", loopback, wildcard, specific
}
'
이 집계는 어떤 서비스인지 알려주지 않으므로, 실제 판단은 운영자 로컬에서 원문과 함께 해야 한다.
재현용 점검 명령 3: Docker 게시 포트를 분리해서 본다
Docker는 HOST_PORT:CONTAINER_PORT만 지정하면 기본적으로 모든 호스트 주소에 포트를 게시한다. 관리 포트는 필요에 따라 loopback이나 승인한 사설 주소를 명시해야 한다.
docker ps \
--format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
컨테이너 전체 설정을 docker inspect로 덤프하면 환경변수와 마운트 경로가 노출될 수 있다. 포트 바인딩만 제한해서 본다.
docker inspect \
--format '{{json .NetworkSettings.Ports}}' \
'<CONTAINER_NAME>'
Compose의 의미는 다음처럼 다르다.
services:
admin:
ports:
# 모든 인터페이스에 게시될 수 있음
- "<HOST_PORT>:<CONTAINER_PORT>"
# 현재 호스트에서만 접근하도록 loopback에 게시
- "127.0.0.1:<HOST_PORT>:<CONTAINER_PORT>"
loopback 바인딩은 공격 표면을 줄이지만 그 자체가 전체 보안을 완성하지는 않는다. Docker 버전, 프록시, 방화벽, 컨테이너 네트워크와 인증 정책도 함께 본다.
Docker 공식 문서에는 28.0.0 미만 버전에서 같은 L2 네트워크의 호스트가 loopback 게시 포트에 접근할 수 있었던 예외도 명시되어 있다. 오래된 Docker를 쓰고 있다면 바인딩 설정만 보지 말고 지원 버전 업데이트와 외부 재검증을 함께 진행한다.
재현용 점검 명령 4: 방화벽 계층을 교차 확인한다
Ubuntu에서 UFW를 쓴다면 다음처럼 현재 상태를 읽는다.
sudo ufw status verbose
UFW를 사용하지 않는 환경은 실제로 사용하는 nftables, 클라우드 보안 목록 또는 보안 그룹을 확인한다. 중요한 것은 규칙 하나의 존재가 아니라 다음 네 계층이 같은 의도를 갖는지다.
- 클라우드 네트워크 방화벽
- 호스트 방화벽
- Docker 게시 주소와 포트
- 리버스 프록시와 애플리케이션 인증
예를 들어 클라우드 방화벽이 관리 포트를 막고 있어도 Docker가 0.0.0.0에 게시 중이면, 다른 네트워크 경로나 향후 규칙 변경 때 노출될 수 있다. 반대로 Docker가 loopback에만 게시되어도 프록시가 인증 없이 다시 공개하면 안전하지 않다.
이번 점검에서 성공한 것과 남은 것
성공으로 볼 수 있는 것은 두 가지다. 공개 웹 서비스는 예상한 경로로 응답했고, 확인한 관리·데이터 계열 포트는 외부 스캔에서 직접 노출된 상태로 관측되지 않았다.
다만 다음 항목은 다음 점검에서 반드시 보완해야 한다.
- 동일 포트 세트를 외부 IPv4와 IPv6에서 각각 확인
- 서버 내부
ss -lntup결과와 Docker 게시 포트 대조 - 클라우드 방화벽과 호스트 방화벽 규칙 스냅샷 보관
- 사설 오버레이 네트워크의 ACL 또는 grants 검증
- 재부팅과 Compose 재배포 후 동일한 바인딩이 유지되는지 확인
“외부에서 안 보였다”는 결과와 “서비스가 내부에서도 안전하게 제한돼 있다”는 결론은 같은 말이 아니다. 외부 관찰과 내부 설정을 함께 기록해야 다음 점검에서 변화도 찾을 수 있다.
공개 글에 넣지 않은 정보
이 글에서는 실제 서버명, 공인·사설 IP, 관리 도메인, 스캔 출발지, 비표준 SSH 포트, 컨테이너명과 정확한 제품 버전을 모두 제외했다. 보안 글은 명령이 읽기 전용이어도 출력은 민감할 수 있다. 터미널 전체 캡처보다 상태와 판정만 공개하는 이유다.
다음 글
- 다음 편: 서버 보안 점검 2편: Nginx Proxy Manager와 Authelia 접근 제어
- 관련 글 제안: Docker Compose
version·external.name경고 해결법 — 기존 글의 실제 URL 확인 후 연결