📚 네트워크 · 패킷 분석 › 02. 포트 · 프로토콜 분석 — 83편
이전 글: 82. 메일 프로토콜 SMTP·POP3·IMAP · 다음 글: 84. Linux Service Port
참고(리눅스 시스템 기초): 37. Listening Port 보안 점검 —ss로 LISTEN 포트를 확인하는 기본 절차
데이터베이스는 조직이 지켜야 할 데이터가 실제로 저장되는 곳입니다. 그런데 DB 포트는 설정 한 줄로 외부 전체에 열리기 쉬운 포트이기도 합니다.
0.0.0.0으로 바꾸고 방화벽까지 열어 두는 경우0.0.0.0/0으로 허용한 경우인터넷에 노출된 DB 포트는 자동화된 스캔과 무차별 대입의 대상이 되며, 인증 없는 서비스라면 데이터 유출이나 서버 장악으로 이어질 수 있다는 점이 널리 알려져 있습니다. 이 글은 "이 포트에서 어떤 DB 서비스가 통신하는가, 그리고 누구에게 열려 있는가"를 확인하는 방법을 정리합니다.
| 포트 | 서비스 | 프로토콜 특징 |
|---|---|---|
| 3306/TCP | MySQL / MariaDB | 서버가 먼저 인사(greeting) 패킷을 보내며 버전 문자열 포함. TLS는 연결 후 전환 방식 |
| 1433/TCP | Microsoft SQL Server | TDS 프로토콜. 이름 있는 인스턴스는 SQL Browser(UDP 1434)로 포트를 알려줌 |
| 5432/TCP | PostgreSQL | 클라이언트가 먼저 시작 메시지 전송. SSLRequest로 TLS 전환 |
| 1521/TCP | Oracle DB Listener | TNS 프로토콜. 리스너가 연결을 받아 DB 인스턴스로 전달 |
| 6379/TCP | Redis | RESP라는 단순 텍스트 기반 프로토콜. 기본 평문 |
| (참고) 27017/TCP | MongoDB | 인증 설정이 없던 과거 기본값으로 노출 사고가 잦았던 서비스 |
포트 번호는 관례일 뿐이며 바꿀 수 있습니다(02편, 20편 참고). 그래서 점검은 포트 번호가 아니라 프로세스에서 출발해야 정확합니다.
DB 포트가 "외부에서 접근 가능하다"는 것은 다음 세 층이 모두 열려 있다는 뜻입니다.
| 층 | 설정 위치 | 안전한 기본 예 |
|---|---|---|
| ① 바인드 주소 | DB 설정 파일 | 127.0.0.1 또는 내부 전용 IP |
| ② 네트워크 접근 제어 | 호스트 방화벽, 보안 그룹, 네트워크 방화벽 | 애플리케이션 서버 IP만 허용 |
| ③ DB 계정의 허용 호스트 | DB 내부 계정 테이블, pg_hba.conf 등 | 'app'@'192.168.10.30' 처럼 출발지 한정 |
한 층만 막혀 있어도 직접 접속은 어렵지만, 한 층에만 의존하면 설정 실수 한 번에 바로 노출됩니다. 그래서 세 층을 모두 점검합니다.
| DB | 바인드 설정 | 접근 제어 설정 |
|---|---|---|
| MariaDB (Rocky) | /etc/my.cnf.d/mariadb-server.cnf의 bind-address | mysql.user 테이블의 host 값 |
| MariaDB (Ubuntu) | /etc/mysql/mariadb.conf.d/50-server.cnf의 bind-address (기본 127.0.0.1) | 동일 |
| PostgreSQL | postgresql.conf의 listen_addresses (기본 localhost) | pg_hba.conf |
| Redis | redis.conf의 bind, protected-mode | requirepass 또는 ACL |
| Oracle | listener.ora의 HOST | 리스너·DB 계정 설정 |
Rocky의 MariaDB는 설치 버전·설정에 따라 bind-address가 주석 처리되어 모든 인터페이스에서 LISTEN할 수 있으므로, 설정 파일보다 ss 결과로 실제 상태를 확인하는 것이 확실합니다.
애플리케이션 서버가 DB 서버에 접속할 때 거치는 판단 순서입니다.

그림 1. 같은 3306 포트라도 바인딩 주소와 방화벽에 따라 위험도가 다릅니다
[App 서버 192.168.10.30] [DB 서버 192.168.10.40]
│
│ TCP SYN → 192.168.10.40:3306
↓
② 방화벽: 출발지 192.168.10.30 허용? ── 아니오 → 차단 (응답 없음 또는 거부)
│ 예
↓
① 소켓: 3306이 192.168.10.40 또는 0.0.0.0에 바인드? ── 아니오 → RST (연결 거부)
│ 예
↓
TCP 연결 성립 → MariaDB가 greeting(버전 정보) 전송
↓
③ 계정: 'app'@'192.168.10.30' 존재 + 비밀번호 일치? ── 아니오 → Access denied
│ 예
↓
쿼리 수행 (TLS 미설정 시 쿼리·결과가 평문으로 전송)
여기서 중요한 관찰 포인트가 있습니다. ①·② 단계에서 막히면 DB 로그에는 아무것도 남지 않고, ③ 단계까지 와야 DB 로그에 인증 실패가 남습니다. 따라서 DB 로그에 외부 IP의 인증 실패가 보인다는 것은 이미 ①·②가 열려 있다는 증거입니다.
실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu). 인터페이스 ens33은 예시이며, DB 서버 192.168.10.40, 앱 서버 역할의 두 번째 VM 192.168.10.30도 본인 소유 VM입니다.
# Rocky Linux
sudo dnf install -y mariadb-server
sudo systemctl enable --now mariadb
# Ubuntu
sudo apt update && sudo apt install -y mariadb-server
sudo systemctl enable --now mariadb
sudo ss -tlnp | grep -E ':(3306|1433|5432|1521|6379)\b'
127.0.0.1:3306이면 로컬 전용, 0.0.0.0:3306 또는 *:3306이면 모든 인터페이스입니다. 앱 서버에서 접속이 필요한 경우 내부 IP로만 바인드합니다.
# Rocky: /etc/my.cnf.d/mariadb-server.cnf / Ubuntu: /etc/mysql/mariadb.conf.d/50-server.cnf
# [mysqld] 섹션에 작성
bind-address = 192.168.10.40
sudo systemctl restart mariadb
sudo ss -tlnp | grep ':3306'
# Rocky (firewalld): 앱 서버 IP만 허용
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.30/32" service name="mysql" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --list-rich-rules
# Ubuntu (ufw)
sudo ufw allow from 192.168.10.30 to any port 3306 proto tcp
sudo ufw status numbered
sudo mysql -e "SELECT user, host FROM mysql.user;"
host가 %인 계정은 어디서든 접속 가능한 계정입니다. 실습용 애플리케이션 계정은 출발지를 한정해 만듭니다.
sudo mysql -e "CREATE USER 'app'@'192.168.10.30' IDENTIFIED BY '실습용_비밀번호';"
# 192.168.10.30(허용)과 그 외 VM(비허용)에서 각각 실행해 비교
timeout 3 bash -c '</dev/tcp/192.168.10.40/3306' && echo "open" || echo "closed/filtered"
📷 [실습 화면 삽입] bind-address 변경 전후
ss -tlnp | grep 3306결과 비교
📷 [실습 화면 삽입] 허용 VM과 비허용 VM에서의 접속 테스트 결과 비교
ss -tlnp 출력 형식 예시(값은 환경마다 다름)
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 80 192.168.10.40:3306 0.0.0.0:* users:(("mariadbd",pid=1532,fd=19))
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=988,fd=6))
SELECT user, host 결과 형식 예시(값은 환경마다 다름)
+-------------+---------------+
| User | Host |
+-------------+---------------+
| app | 192.168.10.30 |
| mariadb.sys | localhost |
| root | localhost |
+-------------+---------------+
점검 체크리스트
0.0.0.0/*이 아니라 필요한 주소에만 바인드되어 있는가%인 계정이 없는가(있다면 사유가 문서화되어 있는가)| DB | 로그 위치(기본값, 설정에 따라 다름) |
|---|---|
| MariaDB (Rocky) | /var/log/mariadb/mariadb.log |
| MariaDB (Ubuntu) | journalctl -u mariadb 또는 설정된 log_error 경로 |
| PostgreSQL | Rocky: 데이터 디렉터리의 log/, Ubuntu: /var/log/postgresql/ |
| Redis | redis.conf의 logfile 설정 경로 |
MariaDB 인증 실패 형식 예시(값은 환경마다 다름) — 기록 여부는 log_warnings 등 로그 상세도 설정에 따라 다릅니다.
2026-09-26 14:21:07 31 [Warning] Access denied for user 'root'@'198.51.100.23' (using password: YES)
PostgreSQL 인증 실패 형식 예시(값은 환경마다 다름)
2026-09-26 14:22:10.415 KST [2211] FATAL: password authentication failed for user "postgres"
| 관찰 내용 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
| DB 포트로의 연결 | 등록된 앱 서버·백업 서버·DBA 접속 서버 | 등록되지 않은 내부 PC, 외부 IP |
| 인증 실패 | 비밀번호 변경 후 앱 설정 미반영(같은 계정·같은 출발지 반복) | root·admin·sa·postgres 등 기본 계정에 대한 다수 출발지 시도 |
| 대용량 응답 | 정기 백업, 리포트 배치 | 업무 외 시간 대량 조회 후 외부 전송 |
| Redis 명령 | 캐시 조회·저장(GET/SET) | 설정 변경 명령(CONFIG SET 등)이 외부·비인가 출발지에서 발생 |
| DB 서버의 아웃바운드 | 복제·백업 대상으로의 연결 | DB 서버가 인터넷으로 새 연결을 시작 |
# 현재 DB 포트로 연결된 출발지 확인
sudo ss -tn state established '( sport = :3306 )'
# MariaDB 내부에서 접속 세션 확인
sudo mysql -e "SHOW PROCESSLIST;"
# 인증 실패 출발지 집계 (Rocky, 로그 파일 방식일 때)
sudo grep "Access denied" /var/log/mariadb/mariadb.log | grep -oE "@'[^']+'" | sort | uniq -c | sort -rn
ss -tlnp 결과가 실제 상태이며, 포트 번호보다 프로세스 이름에서 출발해야 정확합니다.다음 글: 20. 비표준 포트에서 동작하는 서비스 식별