방화벽을 왜 공부하게 됐나
첫 서비스를 배포하고 나서 3일 뒤, AWS 대시보드를 보니 알 수 없는 IP에서 SSH 접속을 시도한 로그가 수백 개 찍혀 있었다. 소름이 돋았다. "내 서버가 지금 공격받고 있구나." 서둘러 Security Group 설정을 뒤져봤는데, SSH 포트 22번이 0.0.0.0/0 (전 세계 모든 IP 허용)으로 열려 있었다. 황급히 내 집 IP로만 제한했고, 그제야 공격 로그가 멈췄다.
그때 깨달았다. 코드를 아무리 잘 짜도, 방화벽이 뚫려 있으면 무용지물이라는 걸. 그 뒤로 나는 방화벽을 "까칠한 문지기"로 받아들였다. 귀찮게 구는 게 아니라, 내 서버를 지켜주는 존재였다.
처음엔 이게 헷갈렸다
방화벽 공부를 시작하니 용어가 쏟아졌다. Inbound, Outbound, Stateful, Stateless, Security Group, NACL, iptables, ufw, WAF... 하나하나가 다 다른 건지, 비슷한 건지 도통 모르겠더라. 특히 AWS를 쓰면 Security Group과 NACL이라는 게 둘 다 있는데, "방화벽이 왜 두 개야?"라는 의문에 빠졌다.
더 혼란스러웠던 건, 방화벽 규칙을 설정했는데도 접속이 안 되는 경우였다. "분명 80번 포트 열었는데 왜 안 돼?"를 외치며 한 시간을 헤맸는데, 알고 보니 서버 안쪽에 ufw라는 또 다른 방화벽이 있어서 거기서 막고 있었다. 방화벽이 여러 겹이라는 걸 그때 처음 알았다.
결국 이거였다 - 클럽 입구의 바운서(Bouncer)
방화벽은 클럽 입구에 서 있는 덩치 큰 문지기(Bouncer)다. 나는 이 비유로 모든 게 정리됐다. 아무나 들여보내지 않는다. 명단에 있거나(WhiteList), 복장 규정을 지킨 사람만 통과시킨다. 이름표(IP 주소)를 확인하고, 어느 입구로 들어오는지(포트 번호), 어디서 왔는지(소스 IP), 심지어 가방 안에 뭐가 들었는지(패킷 내용)까지 검사한다.
인터넷 세상은 위험하다. 문을 활짝 열어두면 러시아 해커, 중국 봇, 옆집 철수의 디도스 공격이 24시간 쏟아진다. 그래서 우리는 방화벽이라는 문지기를 세워두고 규칙을 정해준다. "22번 포트(SSH)는 우리 사무실 IP에만 열어줘", "80번(HTTP)과 443번(HTTPS)은 누구든 들어와도 돼", "그 외에는 전부 거절해" 같은 식이다.
방화벽의 기본 철학: Deny by Default
방화벽의 가장 중요한 철학은 "기본적으로 모두 거부(Deny by Default)"다. 클럽 문지기가 "일단 다 못 들어와. 명단에 있는 사람만 통과"라고 하는 것처럼, 방화벽도 "일단 다 막아. 내가 허용한 것만 들어와"라고 동작한다. 이게 보안의 핵심이다. 허용 목록(Whitelist)을 만들어두고, 그 외에는 전부 차단하는 방식이다.
반대로 "일단 다 허용하고, 위험한 것만 차단"하는 방식(Blacklist)도 있지만, 이건 위험하다. 새로운 공격 방법이 매일 나오는데, 그걸 일일이 차단 목록에 추가하기엔 너무 느리다. 그래서 현대 방화벽은 대부분 Whitelist 방식을 쓴다.
기본 원칙 - Inbound는 막고, Outbound는 푼다
방화벽 설정의 가장 기본적인 패턴(Default Policy)은 이렇다.
Inbound (들어오는 것) - Deny All이 기본
외부에서 내 서버로 들어오는 트래픽이다. 기본적으로 모두 차단(Deny All)한다. 그리고 "웹사이트 보여줘야 하니까 80(HTTP), 443(HTTPS)번 문만 열어줘"라고 예외를 둔다.
왜 기본적으로 막냐고? 내가 요청하지도 않았는데 갑자기 들어오는 패킷은 대부분 공격이기 때문이다. 포트 스캐닝(Port Scanning)이라고, 해커들이 "이 서버 어느 포트가 열려 있나?" 하고 1번부터 65535번까지 다 두드려보는 공격이 있다. 만약 필요 없는 포트까지 열어뒀다면, 해커는 그 포트로 침투를 시도한다.
실제로 내가 겪은 사례가 있다. MySQL을 설치했는데, 기본 포트 3306이 0.0.0.0/0으로 열려 있었다. 그날 밤 중국 IP에서 수천 번의 로그인 시도가 있었고, 비밀번호를 추측하려는 Brute Force 공격이었다. 다행히 비밀번호가 복잡해서 뚫리진 않았지만, Security Group에서 3306을 막자 공격이 즉시 멈췄다.
Outbound (나가는 것) - Allow All이 기본
내 서버에서 외부로 나가는 트래픽이다. 기본적으로 모두 허용(Allow All)한다. 내 서버가 apt update를 하거나 외부 API를 호출해야 하기 때문이다.
예를 들어, Node.js 서버가 npm install을 하려면 npm 레지스트리(registry.npmjs.org)에 접속해야 한다. Python 서버가 pip install을 하려면 PyPI(pypi.org)에 접속해야 한다. 만약 Outbound를 막으면, 서버가 외부와 통신할 수 없어서 패키지 설치도, API 호출도 불가능해진다.
하지만 보안을 극도로 중요시하는 환경(금융, 국방 등)에서는 Outbound도 화이트리스트 방식으로 관리한다. "이 서버는 오직 이 IP의 이 포트로만 나갈 수 있다"고 제한하는 식이다. 그러면 혹시 서버가 해킹당해도, 해커가 외부로 데이터를 빼내기 어렵게 만들 수 있다.
방화벽의 진화 - 3세대로 정리해봤다
방화벽은 세대별로 진화해왔다. 나는 이걸 "문지기가 점점 똑똑해지는 과정"으로 이해했다.
1세대 - 패킷 필터링 (Packet Filtering)
가장 단순한 형태다. 네트워크 패킷의 헤더(주소표)만 본다. 패킷 내용은 안 본다.
검사 항목:
- 출발지 IP (Source IP)
- 목적지 IP (Destination IP)
- 출발지 포트 (Source Port)
- 목적지 포트 (Destination Port)
- 프로토콜 (TCP, UDP, ICMP 등)
예를 들어, "너 22번 포트(SSH)로 들어오네? 출발지가 우리 사무실 IP야? 아니네? 꺼져."라고 판단한다. 간단하고 빠르다. CPU를 거의 안 먹는다. 그래서 지금도 많이 쓰인다.
한계: 패킷 내용을 안 보기 때문에, 80번 포트로 들어오는 해킹 공격은 못 막는다. 예를 들어, 정상적인 HTTP 요청인 척하면서 SQL Injection 코드를 숨겨 보내면, 패킷 필터링 방화벽은 "80번 포트니까 OK"라고 그냥 통과시킨다.
대표 예시:
iptables(Linux)- Cisco ACL (Access Control List)
- 공유기 방화벽 (대부분 이 방식)
실제로 iptables 명령어를 보면 이렇다:
# 22번 포트(SSH)는 192.168.1.100에서만 허용
iptables -A INPUT -p tcp --dport 22 -s 192.168.1.100 -j ACCEPT
# 그 외 22번 포트는 전부 거부
iptables -A INPUT -p tcp --dport 22 -j DROP
# 80번(HTTP), 443번(HTTPS) 포트는 누구나 허용
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# 나머지는 전부 거부
iptables -P INPUT DROP
이게 1세대 방화벽의 전형이다. IP와 포트만 보고 "통과" 또는 "차단"을 결정한다.
2세대 - 상태 기반 검사 (Stateful Inspection)
조금 더 똑똑해졌다. "연결 상태(Connection State)"를 기억한다. 이게 무슨 말이냐면, "아까 내 서버가 먼저 요청 보낸 거에 대한 응답이네? 그럼 들어와."라고 판단할 수 있다는 뜻이다.
핵심 개념: TCP의 3-Way Handshake 추적
TCP 연결은 3단계로 이뤄진다:
- SYN (클라이언트 → 서버): "연결하자"
- SYN-ACK (서버 → 클라이언트): "OK, 연결하자"
- ACK (클라이언트 → 서버): "연결됐다"
Stateful 방화벽은 이 과정을 추적한다. 만약 1번(SYN) 없이 갑자기 3번(ACK) 패킷이 날아오면, "너 누구야? 우리 연결한 적 없는데?"라고 차단한다. 이걸 "연결 테이블(Connection Table)"에 기록하면서 관리한다.
예시: 웹 브라우징 시나리오
- 내가
naver.com에 접속하려고 Outbound 요청을 보낸다. - Stateful 방화벽은 "아, 이 연결은 내가 먼저 시작한 거네"라고 기억한다.
naver.com서버가 응답을 보내면(Inbound), 방화벽은 "이건 아까 내가 요청한 거에 대한 답변이니까 통과"시킨다.- 만약 요청하지도 않았는데 갑자기 외부에서 패킷이 들어오면, "요청 기록이 없네? 차단"한다.
이 방식의 장점은, Inbound 규칙을 일일이 열어주지 않아도 된다는 것이다. Outbound 요청에 대한 응답은 자동으로 허용되기 때문이다. 이게 바로 AWS Security Group이 Stateful한 이유다.
한계: 여전히 패킷 내용은 안 본다. 80번 포트로 정상 HTTP 요청인 척하면서 XSS(Cross-Site Scripting) 공격 코드를 숨겨 보내면, Stateful 방화벽도 "정상 연결이네"라고 통과시킨다.
대표 예시:
- AWS Security Group
- Azure Network Security Group
- 최신
iptables(conntrack 모듈 사용) - 대부분의 기업용 방화벽 (Palo Alto, Fortinet 등)
3세대 - 애플리케이션 방화벽 (WAF - Web Application Firewall)
이제 패킷을 뜯어서 내용을 본다. 택배 상자를 열어서 안에 뭐가 들었는지 확인하는 문지기가 된 것이다.
검사 항목:
- HTTP 요청 본문 (Request Body)
- URL 파라미터
- 쿠키
- 헤더
"80번 포트로 들어와서 합격인 줄 알았더니, 내용물에 DROP TABLE users 같은 SQL 인젝션 공격 코드가 있네? 체포해."라고 판단한다. 가장 강력하지만 비싸고, CPU를 많이 먹는다.
실제 차단 예시:
POST /login HTTP/1.1
Host: myapp.com
Content-Type: application/json
{
"username": "admin' OR '1'='1",
"password": "anything"
}
위 요청은 SQL Injection 공격이다. WAF는 username 필드에 ' (작은따옴표)와 OR 같은 SQL 문법이 들어간 걸 감지하고, "이거 공격이네"라고 차단한다.
또 다른 예시:
GET /search?q=<script>alert('XSS')</script> HTTP/1.1
이건 XSS 공격이다. WAF는 URL에 <script> 태그가 들어간 걸 보고 차단한다.
WAF의 종류:
- Cloud WAF: AWS WAF, Cloudflare WAF
- 장점: 설정 간단, 글로벌 CDN과 연동
- 단점: 비용이 많이 든다 (트래픽당 과금)
- Self-hosted WAF: ModSecurity (Apache/Nginx 모듈)
- 장점: 무료, 직접 룰 커스터마이징 가능
- 단점: 설정 어렵고, 유지보수 부담
한계: 모든 패킷을 뜯어보니 성능 부담이 크다. 특히 HTTPS는 암호화되어 있어서, WAF가 SSL 인증서를 갖고 있어야 복호화해서 볼 수 있다. 이걸 "SSL Termination"이라고 한다. 또한, 오탐(False Positive)이 잦다. 정상 요청인데 공격으로 오인해서 차단하는 경우가 있다.
실제 - 리눅스 서버에 방화벽 설정하기
이론만 알아선 소용없다. 실제로 서버에 방화벽을 설정해본다.
ufw (Uncomplicated Firewall): 초보자용 방화벽
Ubuntu에서 가장 많이 쓰는 방화벽이다. iptables를 쉽게 쓸 수 있게 감싼 래퍼(wrapper)다.
# ufw 설치 (Ubuntu는 기본 설치되어 있음)
sudo apt install ufw
# 현재 상태 확인
sudo ufw status
# 방화벽 활성화 전, 기본 정책 설정
sudo ufw default deny incoming # 들어오는 건 기본 차단
sudo ufw default allow outgoing # 나가는 건 기본 허용
# SSH 포트 22번 열기 (이거 안 하면 SSH 접속 끊김!)
sudo ufw allow 22/tcp
# HTTP(80), HTTPS(443) 열기
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# 특정 IP에만 SSH 허용 (내 집 IP가 203.0.113.50이라면)
sudo ufw allow from 203.0.113.50 to any port 22
# 방화벽 활성화
sudo ufw enable
# 상태 확인 (번호와 함께 보기)
sudo ufw status numbered
# 특정 규칙 삭제 (예 - 3번 규칙 삭제)
sudo ufw delete 3
# 방화벽 비활성화
sudo ufw disable
실제 겪은 실수:
한번은 SSH로 접속한 상태에서 ufw enable을 했는데, 22번 포트를 미리 열어두지 않아서 SSH 연결이 끊겼다. 다행히 AWS 콘솔의 Session Manager로 접속해서 ufw disable로 해결했다. 꼭 SSH 포트를 먼저 열고 ufw enable하라.
iptables: 고급 사용자용 방화벽
iptables는 Linux 커널에 직접 방화벽 규칙을 설정하는 저수준 도구다. 강력하지만 복잡하다.
# 현재 규칙 확인
sudo iptables -L -v -n
# 기본 정책 설정
sudo iptables -P INPUT DROP # 들어오는 건 기본 차단
sudo iptables -P FORWARD DROP # 포워딩은 차단
sudo iptables -P OUTPUT ACCEPT # 나가는 건 허용
# 로컬 루프백 허용 (localhost 통신 허용)
sudo iptables -A INPUT -i lo -j ACCEPT
# 이미 연결된(established) 것과 관련된(related) 패킷 허용
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# SSH 포트 22번 허용
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# HTTP(80), HTTPS(443) 허용
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Ping (ICMP) 허용
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# 특정 IP만 MySQL 접속 허용 (192.168.1.100만 3306 허용)
sudo iptables -A INPUT -p tcp -s 192.168.1.100 --dport 3306 -j ACCEPT
# 규칙 저장 (재부팅 후에도 유지)
sudo apt install iptables-persistent
sudo netfilter-persistent save
iptables의 체인(Chain) 개념:
- INPUT: 서버로 들어오는 패킷
- OUTPUT: 서버에서 나가는 패킷
- FORWARD: 서버를 거쳐가는 패킷 (라우터처럼 동작할 때)
iptables의 타겟(Target):
- ACCEPT: 허용
- DROP: 조용히 차단 (응답 없음)
- REJECT: 차단하고 "Port Unreachable" 응답 보냄
- LOG: 로그 남김 (차단은 안 함)
AWS 클라우드 환경: Security Group vs NACL
클라우드(AWS)를 쓰면 방화벽이 두 개라 헷갈린다. 나는 이걸 "건물 입구 검문소(NACL)와 사무실 문(Security Group)"으로 정리했다.
Security Group (보안 그룹) - EC2 바로 앞의 문지기
서버(EC2) 바로 앞에 있는 문지기다. Stateful하다. 들어오는 거 허용하면, 나가는 건 자동으로 허용된다. 주로 이걸로 관리한다.
특징:
- Stateful (연결 상태 추적)
- 허용(Allow) 규칙만 설정 가능 (거부 규칙 없음)
- 여러 EC2에 동시에 적용 가능
- 실시간 적용 (재부팅 불필요)
설정 예시:
| Type | Protocol | Port Range | Source | Description |
|---|---|---|---|---|
| HTTP | TCP | 80 | 0.0.0.0/0 | Public web traffic |
| HTTPS | TCP | 443 | 0.0.0.0/0 | Public web traffic |
| SSH | TCP | 22 | 203.0.113.50/32 | My office IP |
| MySQL | TCP | 3306 | sg-12345678 | From web server SG |
마지막 줄을 주목하라. Source에 IP 대신 다른 Security Group ID(sg-12345678)를 넣을 수 있다. "이 보안 그룹이 적용된 서버들에서만 접속 허용"이라는 뜻이다. 웹 서버에서 DB 서버로의 접속을 허용할 때 유용하다.
팁:
- SSH는 절대로
0.0.0.0/0으로 열지 마라. 내 IP로만 제한하거나, VPN을 통해서만 접속하게 하라. - DB 포트(3306, 5432 등)는 같은 VPC 내부에서만 열어라. 외부에 열면 Brute Force 공격 타겟이 된다.
- Security Group 이름을 명확하게 짓자.
web-server-sg,db-server-sg같은 식으로.
Network ACL (NACL): 서브넷 입구의 검문소
서브넷(마을 입구)에 있는 검문소다. Stateless하다. 들어오는 문, 나가는 문 따로따로 열어줘야 한다.
특징:
- Stateless (연결 상태 추적 안 함)
- 허용(Allow)과 거부(Deny) 규칙 둘 다 설정 가능
- 규칙 번호 순서대로 평가 (낮은 번호 우선)
- 서브넷 전체에 적용됨
설정 예시 (Inbound):
| Rule # | Type | Protocol | Port Range | Source | Allow/Deny |
|---|---|---|---|---|---|
| 100 | HTTP | TCP | 80 | 0.0.0.0/0 | Allow |
| 200 | HTTPS | TCP | 443 | 0.0.0.0/0 | Allow |
| 300 | SSH | TCP | 22 | 203.0.113.0/24 | Allow |
| * | All | All | All | 0.0.0.0/0 | Deny |
주의사항: Ephemeral Ports
NACL은 Stateless라서, 클라이언트가 서버에 요청을 보낼 때 사용하는 임시 포트(Ephemeral Ports)도 열어줘야 한다. 보통 32768-65535 범위다.
예를 들어, 내가 80번 포트로 요청을 보내면, 서버는 내 임시 포트(예: 54321)로 응답을 보낸다. Security Group은 Stateful이라 알아서 허용하지만, NACL은 Stateless라 명시적으로 열어줘야 한다.
Outbound NACL 규칙:
Rule 100: TCP 32768-65535 → 0.0.0.0/0 Allow
이게 헷갈려서, 대부분 NACL은 기본 설정(Allow All)으로 두고, Security Group으로만 관리한다.
NACL을 쓰는 경우:
- 특정 국가 IP를 통째로 차단할 때
- 알려진 악성 IP를 서브넷 레벨에서 차단할 때
- DDoS 공격 IP를 빠르게 차단할 때
네트워크 계층 모델로 이해하는 방화벽 위치
방화벽이 네트워크 스택의 어느 계층에서 동작하는지 정리해봤다.
| 방화벽 종류 | OSI Layer | 검사 항목 |
|---|---|---|
| 패킷 필터링 (iptables) | Layer 3-4 | IP, Port |
| Stateful (AWS SG) | Layer 3-4 | IP, Port, Connection State |
| WAF (AWS WAF) | Layer 7 | HTTP Header, Body, Cookie |
| Next-Gen Firewall (NGFW) | Layer 7 | Application, User Identity, Content |
Layer가 높을수록 더 많은 정보를 볼 수 있지만, 성능 부담이 커진다.
실제 - nginx를 Application-Level Firewall로 쓰기
nginx는 웹 서버이면서 동시에 리버스 프록시(Reverse Proxy)로도 쓰인다. 여기에 간단한 방화벽 기능을 추가할 수 있다.
# 특정 IP만 허용 (나머지는 차단)
location /admin {
allow 203.0.113.50; # 내 사무실 IP
allow 192.168.1.0/24; # 내부 네트워크
deny all; # 나머지 전부 차단
proxy_pass http://backend;
}
# 특정 User-Agent 차단 (봇 트래픽 차단)
if ($http_user_agent ~* (bot|crawler|spider)) {
return 403;
}
# Rate Limiting (DDoS 방어)
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /api {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://backend;
}
}
# 특정 국가 IP 차단 (GeoIP 모듈 사용)
geo $block_country {
default 0;
CN 1; # 중국
RU 1; # 러시아
}
server {
if ($block_country) {
return 403;
}
}
이런 식으로 nginx를 Application-Level Firewall처럼 쓸 수 있다. 특히 Rate Limiting은 DDoS 공격을 막는 데 효과적이다.
네트워크 세그멘테이션 - DMZ 개념
기업 네트워크에서는 방화벽을 여러 겹으로 둔다. 이걸 네트워크 세그멘테이션(Network Segmentation)이라고 한다.
DMZ (Demilitarized Zone, 비무장지대): 외부 인터넷과 내부 네트워크 사이에 있는 중간 지대다. 웹 서버처럼 외부에 노출되어야 하는 서버는 DMZ에 두고, DB 서버 같은 민감한 서버는 내부 네트워크(Private Zone)에 둔다.
인터넷
│
[Firewall 1: Edge Firewall]
│
DMZ (Web Server, API Server)
│
[Firewall 2: Internal Firewall]
│
Private Zone (DB Server, Admin Server)
만약 DMZ의 웹 서버가 해킹당해도, Firewall 2가 막고 있어서 DB 서버까지 침투하기 어렵다. 이게 Defense in Depth(심층 방어) 전략이다.
Zero Trust vs Perimeter-Based Security
전통적인 보안 모델은 Perimeter-Based Security(경계 기반 보안)였다. "내부 네트워크는 안전하고, 외부는 위험하다"고 가정하고, 경계에 방화벽을 세우는 방식이다.
하지만 요즘은 Zero Trust Security(제로 트러스트 보안)로 바뀌고 있다. "내부든 외부든 아무도 믿지 마. 모든 요청을 검증해."라는 철학이다.
Zero Trust의 핵심 원칙:
- Verify Explicitly: 모든 요청은 인증하고 권한 확인
- Least Privilege Access: 최소한의 권한만 부여
- Assume Breach: "이미 침투당했다"고 가정하고 방어
예를 들어, 옛날에는 "VPN으로 내부 네트워크에 들어오면 모든 리소스 접근 가능"이었다면, 지금은 "VPN 들어와도, 매번 MFA(Multi-Factor Authentication)로 인증하고, 필요한 서버에만 접근 가능"이다.
흔한 실수 - 이것만은 피하자
방화벽 설정하면서 내가 겪었던 실수들을 정리해봤다.
1. SSH 포트를 0.0.0.0/0으로 열기
가장 흔한 실수다. SSH(포트 22)를 전 세계에 열어두면, 봇들이 24시간 Brute Force 공격을 시도한다. /var/log/auth.log를 보면 중국, 러시아, 브라질 IP에서 수천 번의 로그인 시도 로그가 찍혀 있을 것이다.
해결책:
- 내 IP로만 제한:
allow from 203.0.113.50 - VPN을 통해서만 접속
- SSH 포트를 22에서 다른 번호로 변경 (Security by Obscurity)
- SSH Key 인증만 허용 (비밀번호 인증 끄기)
2. 모든 포트 열어두기
개발할 때 귀찮다고 "일단 다 열어두고 나중에 막지 뭐"라고 생각하면 위험하다. 나중엔 잊어버린다.
해결책:
- 필요한 포트만 열기
- 주기적으로
nmap으로 스캔해서 불필요한 포트 확인 - Infrastructure as Code(Terraform 등)로 방화벽 규칙 관리
3. NACL 설정 잘못해서 통신 끊기
NACL은 Stateless라서 Inbound/Outbound 둘 다 설정해야 한다. 한쪽만 설정하면 통신이 안 된다.
해결책:
- NACL은 가급적 기본 설정(Allow All)으로 두고, Security Group으로 관리
- 꼭 써야 한다면, Ephemeral Ports(32768-65535)를 Outbound에 열어둬라
4. 방화벽 규칙 순서 무시
iptables나 NACL은 규칙을 순서대로 평가한다. 먼저 매칭되는 규칙이 적용되므로, 순서가 중요하다.
# 잘못된 예 - 먼저 모두 차단하면 아래 규칙이 무시됨
iptables -A INPUT -j DROP
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # 이건 절대 실행 안 됨
# 올바른 예 - 허용 규칙을 먼저 쓰고, 마지막에 차단
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -j DROP
Docker와 방화벽의 충돌
Docker를 쓰면 방화벽 설정이 꼬이는 경우가 있다. Docker는 자체적으로 iptables 규칙을 추가하기 때문이다.
문제 상황:
ufw로 포트를 막아뒀는데, Docker 컨테이너가 그 포트를 열면 외부에서 접속이 된다. ufw가 무시당하는 것처럼 보인다.
원인:
Docker는 iptables의 FORWARD 체인에 규칙을 추가한다. ufw는 INPUT 체인만 관리하므로, Docker의 규칙이 우선 적용된다.
해결책 1: Docker의 iptables 조작 끄기
// /etc/docker/daemon.json
{
"iptables": false
}
이렇게 하면 Docker가 iptables를 건드리지 않는다. 하지만 컨테이너 간 네트워킹이 안 될 수 있다.
해결책 2: ufw 규칙을 FORWARD 체인에도 추가
# /etc/ufw/after.rules 파일 수정
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN
COMMIT
Docker가 사용하는 DOCKER-USER 체인에 ufw 규칙을 적용한다.
해결책 3: Docker 포트를 localhost에만 바인딩
# 잘못된 예 - 모든 인터페이스에 바인딩
docker run -p 3306:3306 mysql
# 올바른 예 - localhost에만 바인딩
docker run -p 127.0.0.1:3306:3306 mysql
이렇게 하면 localhost에서만 접속 가능하고, 외부에서는 접속 불가능하다.
실제 디버깅 - "왜 접속이 안 되지?" 체크리스트
개발하다가 "DB 접속이 안 돼요", "SSH가 안 먹혀요" 하면 99%는 방화벽 문제다. 당황하지 말고 이 체크리스트를 따라가보자.
1. 로컬 PC 방화벽 확인
Windows:
- 제어판 → Windows Defender 방화벽 → 고급 설정 → 아웃바운드 규칙 확인
Mac:
# 방화벽 상태 확인
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
Linux:
sudo ufw status
sudo iptables -L -v -n
2. 네트워크 경로 확인 (traceroute)
# Linux/Mac
traceroute example.com
# Windows
tracert example.com
어느 지점에서 막히는지 확인할 수 있다.
3. 포트가 실제로 열려 있는지 확인 (telnet/nc)
# telnet으로 확인
telnet example.com 80
# netcat으로 확인
nc -zv example.com 80
# nmap으로 확인 (여러 포트 스캔)
nmap -p 22,80,443 example.com
"Connection refused"가 나오면 서버가 그 포트를 닫은 것이고, "No route to host"나 타임아웃이 나오면 방화벽이 막은 것이다.
4. 클라우드 보안 그룹 확인 (AWS Security Group)
- AWS 콘솔 → EC2 → Security Groups
- Inbound 규칙에 내가 필요한 포트가 열려 있는가?
- Source에 내 IP가 포함되어 있는가? (0.0.0.0/0인가, 아니면 특정 IP만인가?)
5. 서버 내부 방화벽 확인 (ufw/iptables)
# SSH로 서버 접속 후
sudo ufw status verbose
sudo iptables -L -v -n | grep <포트번호>
# 특정 포트가 LISTEN 중인지 확인
sudo netstat -tlnp | grep <포트번호>
# 또는
sudo ss -tlnp | grep <포트번호>
6. 애플리케이션이 정상 동작 중인지 확인
# 프로세스가 살아있는지
ps aux | grep <프로세스명>
# 로그 확인
sudo journalctl -u <서비스명> -n 50
# Docker 컨테이너라면
docker ps
docker logs <컨테이너명>
7. SELinux/AppArmor 확인 (Linux 보안 모듈)
때로는 방화벽이 아니라 SELinux나 AppArmor 같은 보안 모듈이 막는 경우도 있다.
# SELinux 상태 확인 (CentOS/RHEL)
getenforce
# 임시로 끄기 (재부팅 시 다시 켜짐)
sudo setenforce 0
# AppArmor 상태 확인 (Ubuntu)
sudo aa-status
마치며 - 보안은 귀찮지만, 뚫리는 것보단 낫다
처음 방화벽을 공부할 땐 "왜 이렇게 복잡해?"라고 짜증났다. 포트 하나 열려면 ufw도 설정하고, Security Group도 설정하고, NACL도 확인하고... 단계가 너무 많았다. 하지만 실제로 서버가 공격받는 걸 보고 나니, 이 모든 게 필요한 방어막이라는 걸 깨달았다.
방화벽은 "까칠한 문지기"지만, 내 서버를 지켜주는 고마운 존재다. 귀찮더라도 제대로 설정해두면, 밤에 편하게 잘 수 있다. "내 서버가 지금 공격받고 있는 건 아닐까?" 하는 불안감 없이.
결국 방화벽은 "기본적으로 모두 거부(Deny by Default)"라는 철학으로 귀결된다. 필요한 것만 열고, 나머지는 닫아두자. 그게 가장 안전한 방법이다.