> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# 서버 보안 점검 1편: 외부 노출 포트와 방화벽 상태 확인
- URL: https://mlog.me/server-security-check-external-ports-firewall/
- Published: 2026-08-24T10:26:23.000Z
- Updated: 2026-09-09T01:30:27.000Z
- Description: 외부 Nmap 결과와 서버 내부 리스닝 상태를 대조해 공개 웹 포트와 관리 포트를 점검하고, open과 filtered의 차이를 실제 결과로 정리합니다.
- Author: mLog
- Tags: 보안, 서버 보안, Linux, Docker, #방화벽

운영 서버를 외부에서 점검했을 때 웹 서비스용 포트는 예상대로 `open`이었고, 확인한 관리·데이터 계열 포트는 `filtered`로 관측됐다. 여기서 `filtered`를 “닫힘”으로 기록하지 않는 것이 중요했다. 그 출발지에서는 열림·닫힘을 판별하지 못했다는 뜻이므로, 다음 확인 대상은 서버 내부의 리스닝 주소와 Docker 게시 포트, 호스트·클라우드 방화벽이었다.

이 글은 실제로 진행한 보안 점검 가운데, 다음 관리 서비스 점검을 시작하기 **직전까지의 기록**을 다시 정리한 연재의 첫 편이다. 실제 서버명, 공인·사설 IP, 관리 도메인, 비표준 포트와 사용자명은 공개하지 않는다.

## 세 줄 요약

- 외부 점검에서 웹 서비스용 포트는 예상대로 열려 있었고, 확인한 관리·데이터 계열 포트는 `filtered`로 관측됐다.
- Nmap의 `filtered`는 “닫힘”이 아니라 **필터 때문에 열림·닫힘을 판별하지 못했다**는 뜻이다.
- 외부 스캔 한 번만으로 끝내지 말고 서버의 리스닝 소켓, Docker 게시 포트, 호스트·클라우드 방화벽을 대조해야 한다.

## 점검 범위와 판정 기준

이 글의 결과는 당시 보존된 점검 기록과 명령 출력에 근거한다. 정확한 실행 시각, 스캔 출발지, IPv6 점검 여부와 당시 방화벽 규칙 전문은 남아 있지 않다. 따라서 확인되지 않은 부분은 성공으로 채우지 않고 `확인 필요`로 표시한다.

| 판정    | 의미                              |
| ----- | ------------------------------- |
| 성공    | 보존된 출력에서 기대한 상태가 직접 확인됨         |
| 부분 성공 | 보호 상태는 보였지만 다른 경로나 종단 간 동작이 미확인 |
| 실패    | 명령 오류 또는 기대 결과와 명확히 다른 상태가 확인됨  |
| 확인 필요 | 출력이 없거나 잘렸거나 당시 수행 여부를 확인할 수 없음 |

## 실제 점검 결과

| 항목           | 결과                            | 판정    | 해석                               |
| ------------ | ----------------------------- | ----- | -------------------------------- |
| 공개 웹 경로      | 표준 HTTP·HTTPS 포트가 open        | 성공    | 의도한 웹 서비스가 외부 연결을 받고 있었음         |
| 관리 화면 후보     | 점검한 관리 포트가 filtered           | 부분 성공 | 외부 스캔 지점에서 포트 상태를 확인할 수 없도록 필터링됨 |
| 데이터 계열 후보    | 점검한 DB·Docker 관리 포트가 filtered | 부분 성공 | 직접 공개된 흔적은 없었지만 내부 리스닝 여부와는 별개   |
| 사설 오버레이 네트워크 | 활성 상태 확인                      | 성공    | 관리 통신을 공용망과 분리할 기반이 있었음          |
| IPv6 외부 노출   | 보존된 결과 없음                     | 확인 필요 | IPv4 결과를 IPv6까지 확대 해석할 수 없음      |
| 방화벽 규칙 교차 확인 | 규칙 전문이 보존되지 않음                | 확인 필요 | 클라우드·호스트·Docker 규칙을 다시 대조해야 함    |

확정된 침해나 명확한 외부 관리 포트 노출 실패는 보존 기록에서 확인되지 않았다. 그러나 `filtered`만 보고 “포트가 닫혀 있다”거나 “완전히 안전하다”고 결론 내릴 수도 없다.

Nmap 공식 문서에 따르면 포트 상태는 포트 자체의 영구 속성이 아니라 **해당 위치와 방식으로 스캔했을 때 Nmap이 본 상태**다. 같은 포트도 내부망과 인터넷에서 다르게 보일 수 있다.

## 재현용 점검 명령 1: 외부에서 스캔한다

아래 명령은 당시 터미널 원문이 아니라, 같은 항목을 다시 확인하기 위한 **재현용 읽기 전용 명령**이다. 점검 권한이 있는 자신의 서버에만 사용한다.

먼저 실제 값을 로컬 셸에만 넣는다. 이 값이 들어간 명령 기록과 스크린샷은 게시하지 않는다.

```bash
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을 설정하세요}"

```

서버와 다른 외부 네트워크에서 필요한 포트만 지정해 확인한다.

```bash
nmap -Pn -sT -p "$PORT_SET" "$PUBLIC_DOMAIN"

```

결과를 공개할 때는 IP, 실제 비표준 포트, 서비스 버전과 스캔 출발지를 지우고 다음 정도로만 요약한다.

```text
public_web_ports=open
management_ports=filtered
database_ports=filtered

```

상태는 다음처럼 읽는다.

- `open`: 대상 애플리케이션이 그 포트에서 연결을 받고 있다.
- `closed`: 대상은 도달 가능하지만 그 포트에서 듣는 애플리케이션이 없다.
- `filtered`: 방화벽이나 다른 네트워크 장치 때문에 Nmap이 `open`과 `closed`를 구분하지 못했다.

한 번의 외부 스캔은 **그 시점·그 출발지의 관찰 결과**다. 다른 네트워크, IPv6, 재부팅 이후에도 같은지는 별도로 확인한다.

## 재현용 점검 명령 2: 서버 내부 리스닝 포트를 본다

외부에서 보이지 않더라도 서비스가 모든 인터페이스에 리스닝 중일 수 있다. 서버에서는 소켓 목록을 확인하되, 원문 전체는 내부 IP와 프로세스 정보를 드러낼 수 있으므로 공개하지 않는다.

```bash
sudo ss -lntup

```

확인 기준은 다음과 같다.

- 공개 웹 프록시만 `0.0.0.0` 또는 `[::]`에 바인딩되어 있는가
- 관리 UI가 `127.0.0.1` 또는 승인한 사설 오버레이 주소에만 바인딩되어 있는가
- DB가 예상하지 않은 공용 인터페이스에서 듣고 있지 않은가
- 사용하지 않는 서비스가 남아 있지 않은가

원문 대신 TCP 리스너의 주소 유형별 개수만 기록하려면 다음처럼 축약할 수 있다. 0.0.0.0, \[::\], \* 표시는 와일드카드로 집계하며, ss 조회가 실패하면 개수 대신 실패 상태를 출력한다. 이 예제에는 UDP 소켓이 포함되지 않는다.

```bash
(
  set +x
  if ! mlog_ss_output=$(sudo ss -lntH); then
    printf 'listener_summary=unavailable (ss 조회 실패)\n' >&2
    exit 1
  fi

  printf '%s\n' "$mlog_ss_output" |
    awk '
      NF < 4 {next}
      $4 ~ /^127\./ || $4 ~ /^\[::1\]:/ {loopback++ ; next}
      $4 ~ /^0\.0\.0\.0:/ || $4 ~ /^\[::\]:/ || $4 ~ /^\*:/ {wildcard++ ; next}
      {specific++}
      END {
        printf "loopback=%d wildcard=%d specific=%d\n", loopback, wildcard, specific
      }
    '
)

```

이 집계는 어떤 서비스인지 알려주지 않으므로, 실제 판단은 운영자 로컬에서 원문과 함께 해야 한다.

## 재현용 점검 명령 3: Docker 게시 포트를 분리해서 본다

Docker는 `HOST_PORT:CONTAINER_PORT`만 지정하면 기본적으로 모든 호스트 주소에 포트를 게시한다. 관리 포트는 필요에 따라 loopback이나 승인한 사설 주소를 명시해야 한다.

```bash
docker ps \
  --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

```

컨테이너 전체 설정을 `docker inspect`로 덤프하면 환경변수와 마운트 경로가 노출될 수 있다. 포트 바인딩만 제한해서 본다.

```bash
docker inspect \
  --format '{{json .NetworkSettings.Ports}}' \
  '<CONTAINER_NAME>'

```

아래는 같은 포트 매핑의 변경 전·후 예시다. 두 매핑을 동시에 추가하지 않고, 필요한 접근 범위에 맞는 하나만 사용한다.

변경 전: 호스트 주소를 생략한 매핑

```yaml
services:
  admin:
    ports:
      # 모든 인터페이스에 게시될 수 있음
      - "<HOST_PORT>:<CONTAINER_PORT>"

```

변경 후: 현재 호스트에서 접근하도록 loopback 주소를 지정한 매핑

```yaml
services:
  admin:
    ports:
      # 현재 호스트에서만 접근하도록 loopback에 게시
      - "127.0.0.1:<HOST_PORT>:<CONTAINER_PORT>"

```

loopback 바인딩은 공격 표면을 줄이지만 그 자체가 전체 보안을 완성하지는 않는다. Docker 버전, 프록시, 방화벽, 컨테이너 네트워크와 인증 정책도 함께 본다.

Docker 공식 문서에는 28.0.0 미만 버전에서 같은 L2 네트워크의 호스트가 loopback 게시 포트에 접근할 수 있었던 예외도 명시되어 있다. 오래된 Docker를 쓰고 있다면 바인딩 설정만 보지 말고 지원 버전 업데이트와 외부 재검증을 함께 진행한다.

## 재현용 점검 명령 4: 방화벽 계층을 교차 확인한다

Ubuntu에서 UFW를 쓴다면 다음처럼 현재 상태를 읽는다.

```bash
sudo ufw status verbose

```

Docker bridge 네트워크에서 게시한 포트의 트래픽은 NAT 처리 과정에서 UFW의 `INPUT`·`OUTPUT` 규칙을 우회할 수 있다. 따라서 `ufw status`만으로 Docker 게시 포트가 차단됐다고 판정하지 않는다. 실제 Docker 방화벽 백엔드(`iptables` 또는 `nftables`)와 전달·NAT 규칙, 클라우드 방화벽, 외부 접속 결과를 함께 확인한다. Docker가 만든 규칙을 임의로 삭제하거나 방화벽 관리를 끄는 것은 해결책이 아니다. ([Docker 공식 설명](https://docs.docker.com/engine/network/packet-filtering-firewalls/?ref=mlog.me#docker-and-ufw))

UFW를 사용하지 않는 환경도 실제 호스트 방화벽과 클라우드 보안 목록 또는 보안 그룹을 각각 확인한다. 중요한 것은 규칙 하나의 존재가 아니라 다음 네 계층이 같은 의도를 갖는지다.

1. 클라우드 네트워크 방화벽
2. 호스트 방화벽
3. Docker 게시 주소와 포트
4. 리버스 프록시와 애플리케이션 인증

예를 들어 클라우드 방화벽이 관리 포트를 막고 있어도 Docker가 `0.0.0.0`에 게시 중이면, 다른 네트워크 경로나 향후 규칙 변경 때 노출될 수 있다. 반대로 Docker가 loopback에만 게시되어도 프록시가 인증 없이 다시 공개하면 안전하지 않다.

## 이번 점검에서 성공한 것과 남은 것

성공으로 볼 수 있는 것은 두 가지다. 공개 웹 서비스는 예상한 경로로 응답했고, 확인한 관리·데이터 계열 포트는 외부 스캔에서 직접 노출된 상태로 관측되지 않았다.

다만 다음 항목은 다음 점검에서 반드시 보완해야 한다.

- 동일 포트 세트를 외부 IPv4와 IPv6에서 각각 확인
- 서버 내부 `ss -lntup` 결과와 Docker 게시 포트 대조
- 클라우드 방화벽과 호스트 방화벽 규칙 스냅샷 보관
- 사설 오버레이 네트워크의 ACL 또는 grants 검증
- 재부팅과 Compose 재배포 후 동일한 바인딩이 유지되는지 확인

“외부에서 안 보였다”는 결과와 “서비스가 내부에서도 안전하게 제한돼 있다”는 결론은 같은 말이 아니다. 외부 관찰과 내부 설정을 함께 기록해야 다음 점검에서 변화도 찾을 수 있다.

## 공개 글에 넣지 않은 정보

이 글에서는 실제 서버명, 공인·사설 IP, 관리 도메인, 스캔 출발지, 비표준 SSH 포트, 컨테이너명과 정확한 제품 버전을 모두 제외했다. 보안 글은 명령이 읽기 전용이어도 **출력은 민감할 수 있다**. 터미널 전체 캡처보다 상태와 판정만 공개하는 이유다.

## 다음 글

- 다음 편: **[서버 보안 점검 2편: Nginx Proxy Manager와 Authelia 접근 제어](https://mlog.me/server-security-check-npm-authelia-access-control/)**
- 관련 글: **[Docker Compose version·external.name 경고 해결법](https://mlog.me/docker-compose-version-obsolete-external-name-deprecated/)**

## 공식 문서

- [Nmap Port Scanning Basics](https://nmap.org/book/man-port-scanning-basics.html?ref=mlog.me)
- [Docker Port publishing and mapping](https://docs.docker.com/engine/network/port-publishing/?ref=mlog.me)
- [Docker daemon 접근 보호](https://docs.docker.com/engine/security/protect-access/?ref=mlog.me)