← 웹/클라우드/인프라로 돌아가기
웹/클라우드/인프라신윤섭·2026년 10월 26일

IPv4만 막았는데 IPv6로 열려 있다: 주소가 두 벌인 서버의 방화벽

공유

iptables로 막았는데 휴대폰에서는 열린다

VPS에 Node 앱을 올리고 에이전트에게 이렇게 부탁했다고 해보자. "SSH랑 웹 포트만 열고 나머지는 다 막아줘." 돌아온 답은 그럴듯하다.

sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -P INPUT DROP

노트북에서 서버IP:3000으로 접속해보면 막혀 있다. 개발용으로 띄워둔 3000번 포트가 닫힌 걸 확인했으니 안심한다. 그런데 며칠 뒤 와이파이를 끄고 휴대폰 데이터로 같은 도메인에 접속해보니 3000번이 그대로 열려 있다. 휴대폰 데이터망은 IPv6로 붙는 경우가 많고, 이번엔 그 길로 들어갔다.

방화벽이 고장 난 건 아니다. 규칙은 정확히 적힌 대로 일하고 있다. 다만 적힌 규칙이 IPv4 문 하나에만 걸려 있었고, 서버에는 문이 하나 더 있었다. IPv6 문이다.

서버는 주소를 두 벌 가지고 있다

요즘 클라우드 서버는 대부분 IPv4 주소(203.0.113.10 같은 모양)와 IPv6 주소(2001:db8:1234::1 같은 모양)를 함께 받는다. 이렇게 두 주소 체계를 동시에 쓰는 구성을 듀얼 스택이라고 부른다.

IPv6가 서버에 붙는 이유는 단순하다. IPv4 주소가 모자라서 비싸졌다. AWS는 2024년 2월 1일부터 모든 공인 IPv4 주소에 시간당 0.005달러를 받기 시작했다. 한 달이면 3.6달러쯤 된다. Hetzner 클라우드는 IPv4 주소에 월 요금을 따로 매기고 IPv6에는 요금을 받지 않는다. 비용을 아끼려고 IPv6를 켜거나, 아예 처음부터 IPv6가 붙어서 나오는 서버가 늘고 있다. 바이브 코더 입장에서는 대개 내가 고른 적도 없는데 이미 붙어 있다.

여기서 비유를 하나 들어보자. 집 전화와 휴대폰을 둘 다 쓰는 사람이 스팸 전화에 시달리다가 차단 서비스를 신청했다. 그런데 집 전화번호에만 등록했다. 집 전화는 조용해졌지만 휴대폰으로는 여전히 같은 업체가 전화를 건다. 번호가 두 개면 차단 목록도 두 개 필요하다.

IPv4와 IPv6가 딱 이 관계다. 같은 서버, 같은 앱인데 주소가 두 개고, 각 주소로 들어오는 길에 붙는 검문소도 따로 있다. 비유가 어긋나는 지점도 짚어두자. 전화와 달리 서버에서는 한쪽 규칙을 다른 쪽으로 자동으로 복사해주는 도구가 있다. 문제는 그런 도구 없이 IPv4 규칙만 손으로 적었을 때다.

규칙도 두 벌이어야 한다

리눅스에서 iptables는 IPv4 패킷만 본다. IPv6 패킷을 거르는 건 이름이 거의 같은 ip6tables다. 둘은 서로의 규칙을 모른다. 위에서 에이전트가 짜준 규칙에는 iptables만 있었다. ip6tables의 기본 정책은 여전히 모든 걸 받아들이는 ACCEPT다. IPv6로 들어오는 3000번 요청은 아무 검문도 받지 않는다.

<!-- IMAGE PENDING (Gemini 402, 재생성 필요): /images/cloud-infra/2026-10-26-ipv6-dual-stack-firewall-gap-for-vibe-coders-01.png | 같은 서버에 IPv4와 IPv6 두 개의 문이 있고, iptables 규칙은 IPv4 문에만 걸려 있는 구조 -->

다른 도구들은 사정이 조금씩 다르다.

ufw는 두 벌을 알아서 만든다. 우분투의 ufw 매뉴얼은 IPv6를 기본으로 허용한다고 적어두었고, IPv6 방화벽이 동작하려면 /etc/default/ufw에서 IPv6가 켜져 있어야 한다고 말한다. 켜져 있으면 ufw allow 22처럼 주소를 지정하지 않은 규칙은 IPv4와 IPv6 양쪽에 걸린다. sudo ufw status를 쳤을 때 22/tcp (v6) 같은 줄이 함께 보이면 정상이다. 예전 가이드를 따라 IPV6=no로 바꿔둔 서버라면 ufw는 IPv6를 루프백 말고는 받지 않도록 막아준다. 어느 쪽이든 ufw는 문 두 개를 모두 챙긴다. 다만 ufw allow from 203.0.113.7처럼 IPv4 주소를 콕 집은 규칙은 당연히 IPv4에만 해당한다.

nftables는 inet이라는 표 종류를 쓰면 한 표로 IPv4와 IPv6를 같이 거른다. ip 표에 적으면 IPv4만, ip6 표에 적으면 IPv6만이다. 에이전트가 nftables 규칙을 짜줬다면 맨 위 table 줄에 무엇이 적혀 있는지만 보면 된다.

클라우드 방화벽도 두 벌이다. AWS 보안 그룹에서 0.0.0.0/0은 모든 IPv4 주소, ::/0은 모든 IPv6 주소를 뜻하고, 콘솔에는 각각 "Anywhere-IPv4", "Anywhere-IPv6"로 나온다. IPv4 쪽 SSH는 사무실 IP로 좁혀놓고 IPv6 쪽에는 ::/0을 열어둔 설정이 자주 나오는데, 이유 중 하나는 콘솔의 "My IP" 버튼이다. AWS 문서에 따르면 이 버튼은 내 컴퓨터의 공인 IPv4 주소만 채운다. IPv4 줄은 버튼 한 번에 좁혀지고, IPv6 줄은 손으로 고치지 않는 한 그대로 남는다. 서버에 공인 IPv6 주소가 붙어 있다면 그쪽 SSH는 세상 전체에 열려 있는 셈이다.

앱은 이미 IPv6에서 듣고 있다

"우리 앱은 IPv6를 안 쓰는데요"라고 생각할 수 있다. 앱이 안 쓰겠다고 정한 적이 없다면 쓰고 있을 가능성이 크다.

Node.js 문서는 server.listen(3000)처럼 주소를 빼면 IPv6가 가능한 환경에서는 ::, 즉 모든 IPv6 주소에서 연결을 받는다고 적고 있다. 그리고 대부분의 운영체제에서 ::에서 듣는 서버는 0.0.0.0, 즉 모든 IPv4 주소에서도 함께 받는다. 소켓 하나로 두 문을 다 여는 셈이다. Express의 app.listen(3000)도 내부에서 이 함수를 그대로 부르니 사정이 같다. 파이썬의 python -m http.server도 문서에 "기본적으로 모든 인터페이스에 바인딩한다"고 적혀 있다.

서버에서 직접 확인하면 바로 보인다.

sudo ss -tlnp
LISTEN 0 511       *:3000      *:*   users:(("node",pid=812,fd=20))
LISTEN 0 4096  0.0.0.0:22  0.0.0.0:*  users:(("sshd",pid=601,fd=3))
LISTEN 0 4096     [::]:22     [::]:*  users:(("sshd",pid=601,fd=4))

[::]:22는 IPv6 전체에서 듣는다는 뜻이고, *:3000은 IPv4와 IPv6를 하나의 소켓으로 함께 받는다는 뜻이다. 셋 다 바깥에서 닿을 수 있는 상태다. 남은 건 방화벽이 양쪽을 다 막고 있느냐뿐이다.

Docker에는 한 단계가 더 있다. 지난주 글에서 봤듯이 -p 8080:80처럼 주소 없이 포트를 공개하면 Docker는 0.0.0.0과 [::] 양쪽에 연다. 컨테이너가 IPv4 주소만 가지고 있어도 마찬가지다. Docker 포트 공개 문서에 따르면, 기본값인 userland proxy가 켜져 있으면 호스트의 IPv6 주소로 들어온 연결을 받아 컨테이너의 IPv4 주소로 넘겨준다. "컨테이너가 IPv4 전용이니 IPv6로는 못 들어온다"는 생각이 여기서 틀린다.

이 경로에는 부작용도 하나 있다. docker-mailserver 문서는 이렇게 프록시를 거치면 원래 클라이언트의 IPv6 주소가 사라지고 Docker 네트워크의 게이트웨이 주소로 바뀐다고 경고한다. 앱 입장에서는 바깥 손님이 내부 네트워크 주소를 달고 들어온 것처럼 보인다. 그래서 "컨테이너 내부 대역에서 온 요청은 믿는다"는 설정이 있으면, 인터넷에서 온 요청이 그 믿음을 그대로 물려받는다. 메일 서버라면 아무나 메일을 보낼 수 있는 오픈 릴레이가 될 수 있다. 접속 IP를 믿는 문제는 X-Forwarded-For 글에서 다룬 이야기와 뿌리가 같다.

참고로 Docker 27.0부터는 IPv6를 켠 bridge 네트워크에 ip6tables 규칙 관리가 기본으로 들어갔다. IPv6에서도 공개한 포트만 바깥에서 닿게 하려는 변화다. 반대로 말하면 공개한 포트는 IPv6에서도 공개된다.

<!-- IMAGE PENDING (Gemini 402, 재생성 필요): /images/cloud-infra/2026-10-26-ipv6-dual-stack-firewall-gap-for-vibe-coders-02.png | Node, 파이썬, Docker가 주소를 지정하지 않았을 때 IPv4와 IPv6 양쪽에서 연결을 받는 기본 동작 -->

"IPv6는 너무 넓어서 아무도 못 찾는다"

IPv6 문이 열려 있어도 괜찮다는 쪽에서 흔히 드는 근거가 주소 공간의 크기다. 틀린 말은 아니다. IPv4 인터넷 전체는 약 43억 개 주소다. 오픈소스 스캐너 masscan은 기계 한 대로 인터넷 전체를 5분 안에 훑을 수 있다고 소개한다. IPv6는 서버 한 대에 흔히 배정하는 /64 대역 하나에만 약 1844경 개 주소가 들어 있다. 하나하나 두드려서 찾는 건 사실상 불가능하다.

그런데 공격자는 하나하나 두드리지 않는다. 주소를 알아낼 방법이 따로 있다.

가장 쉬운 건 DNS다. 도메인에 AAAA 레코드(도메인 이름을 IPv6 주소로 연결하는 레코드)를 등록했다면 서버 IPv6 주소는 공개 정보다. AAAA를 안 걸었더라도 서버가 바깥으로 연결을 하는 순간 상대방은 내 IPv6 주소를 본다. 패키지 업데이트, 외부 API 호출, 시간 동기화가 다 그렇다.

이 방법이 실제로 쓰인 적이 있다. 2016년 초, 공개 시간 서버 모음인 pool.ntp.org의 IPv6 풀에 스캐너와 연결된 것으로 보이는 서버들이 들어가 있다는 보고가 올라왔다. 이 서버에 시간을 맞추러 온 기기들이 곧이어 포트 스캔을 받았다. 당시 Shodan과의 연관성이 거론됐지만 확정되지는 않았다. SANS 인터넷 스톰 센터의 요하네스 울리히는 이 일을 분석하면서, IPv6에서는 주소를 일일이 훑는 것보다 기기가 먼저 연락해오게 만드는 쪽이 훨씬 현실적인 방법이라고 정리했다.

결국 IPv6의 넓은 주소 공간은 "아직 아무도 내 주소를 모를 때"만 가리개 역할을 한다. 서버를 운영하는 이상 그 시간은 길지 않다.

에이전트가 짠 방화벽에서 생기는 일

에이전트에게 방화벽을 맡기면 처음 예시처럼 iptables만 쓰는 답이 자주 나온다. 인터넷에 있는 방화벽 예제 상당수가 IPv4 시절에 쓰였고, "포트 막아줘"라는 요청에는 IP 버전 이야기가 없다. 에이전트는 시킨 일을 했다. 빠진 건 이번에도 질문 쪽이다.

Anthropic이 Claude Code용으로 공개한 레퍼런스 개발 컨테이너도 좋은 예다. 이 컨테이너에 들어 있는 init-firewall.sh는 에이전트가 허용 목록에 있는 곳으로만 나가도록 막는 스크립트다. iptables로 INPUT, FORWARD, OUTPUT 기본 정책을 DROP으로 두고, 허용할 주소만 골라 연다. 스크립트 안에 ip6tables는 없다. 2026년 2월에는 IPv6 네트워크에서 rootless podman으로 이 컨테이너를 띄우자 차단돼야 할 example.com에 접속이 됐다는 이슈(#24952)가 올라왔다. 이 이슈는 한 달 뒤 활동이 없다는 이유로 봇이 자동으로 닫았고, 함께 올라온 수정 PR도 반영되지 않았다. 2026년 10월 현재 main 브랜치의 스크립트에도 ip6tables는 없다.

공정하게 덧붙이면, Docker의 기본 bridge 네트워크는 IPv6가 꺼져 있어서 일반적인 Docker 환경에서는 컨테이너가 IPv6로 나갈 길 자체가 없다. 그래서 대부분의 사용자는 이 틈을 겪지 않는다. 다만 IPv6가 켜진 네트워크로 옮기는 순간 "막혀 있다"는 전제가 조용히 깨진다. 들어오는 쪽이든 나가는 쪽이든 구조는 같다. IPv4 규칙은 IPv6 패킷을 보지 못한다.

그래서 이 규칙은 프로젝트 지침 파일(Claude Code의 CLAUDE.md, Codex의 AGENTS.md)에 적어두는 편이 낫다.

- 방화벽 규칙은 IPv4와 IPv6를 모두 다룬다. iptables를 쓰면 ip6tables도 같이 쓰고, nftables는 inet 표를 쓴다.
- 서버 앱은 바인딩 주소를 명시한다. 외부에 공개하지 않을 포트는 127.0.0.1(필요하면 ::1)에만 연다.

확인하고 막는 순서

먼저 지금 상태를 본다. 서버 안에서는 두 가지다.

sudo ss -tlnp            # [::] 또는 * 로 듣는 포트가 있는지
sudo ip6tables -S        # IPv6 규칙이 실제로 있는지 (nftables면 sudo nft list ruleset)

ip6tables -S 결과가 -P INPUT ACCEPT 한 줄 정도로 끝나면 IPv6 쪽은 아무것도 막고 있지 않은 것이다. ufw를 쓴다면 sudo ufw status에 (v6) 줄이 보이는지 확인한다.

그다음은 바깥에서 두드려본다. 서버 IPv6 주소는 ip -6 addr로 확인할 수 있다. 여기서 함정이 하나 있다. 내 노트북이 연결된 와이파이에 IPv6가 없으면 테스트 자체가 안 된다. IPv6가 되는 다른 서버에서 해보는 게 가장 확실하다. 휴대폰 데이터 핫스팟에 노트북을 붙여보는 것도 방법이다.

curl -6 -m 5 "http://[2001:db8:1234::1]:3000"   # IPv6 주소는 대괄호로 감싼다
nc -6 -zv 2001:db8:1234::1 5432

응답이 오면 열려 있는 것이다. 막는 방법은 손이 덜 가는 것부터 적는다.

공개할 필요가 없는 포트는 바인딩부터 좁힌다. server.listen(3000, '127.0.0.1'), Docker라면 -p 127.0.0.1:5432:5432. 서버 바깥에서 들어올 문이 처음부터 없으니 IPv4든 IPv6든 상관이 없다. 앞단의 Caddy나 nginx만 바깥 요청을 받고, 앱은 서버 안에서만 듣게 하는 구조가 가장 단순하다.

서버 방화벽은 두 벌을 한 번에 다루는 도구로 쓴다. ufw라면 IPv6 설정이 켜져 있는지 확인하고 주소 없는 규칙을 쓴다. 직접 규칙을 짠다면 nftables의 inet 표에 쓰거나, iptables와 ip6tables에 같은 규칙을 넣는다. ip6tables로 막을 때는 ICMPv6를 통째로 막지 않도록 주의한다. IPv6는 이웃 장비 찾기와 경로 크기 확인을 ICMPv6에 의존해서, 이걸 다 막으면 IPv6 연결 자체가 이상하게 끊긴다.

클라우드 방화벽은 IPv6 줄을 따로 읽는다. AWS 보안 그룹, 클라우드 사업자 방화벽에서 0.0.0.0/0과 ::/0을 나란히 놓고 비교한다. 22번 포트처럼 IPv4는 좁혀놨는데 IPv6만 ::/0인 줄이 있으면 바로 고친다.

IPv6를 아예 안 쓴다면 끄는 것도 방법이다. 서버에서 IPv6 주소를 떼거나 클라우드 콘솔에서 IPv6를 할당하지 않으면 문 자체가 없어진다. 다만 IPv4 과금을 피하려고 IPv6 전용으로 가는 흐름도 있으니, 끄기보다 양쪽을 같이 막는 습관이 길게 보면 낫다.

<!-- IMAGE PENDING (Gemini 402, 재생성 필요): /images/cloud-infra/2026-10-26-ipv6-dual-stack-firewall-gap-for-vibe-coders-03.png | IPv6 노출을 확인하는 명령과 바인딩, 서버 방화벽, 클라우드 방화벽 세 겹의 막는 자리 -->

문은 두 개, 확인도 두 번

듀얼 스택 서버는 주소가 두 벌이고, 그 앞의 검문소도 두 벌이다. iptables 규칙은 IPv6 패킷을 모르고, 보안 그룹의 0.0.0.0/0 줄은 ::/0 줄과 상관이 없다. 반면 앱은 주소를 지정하지 않으면 양쪽 문에서 다 손님을 받는다. 방화벽을 IPv4 기준으로만 짰다면 막는 쪽은 한 벌인데 여는 쪽은 두 벌이 된다.

IPv4 주소 값이 오른 뒤로 IPv6가 기본으로 붙어 나오는 서버는 이미 흔하다. 에이전트에게서 "방화벽 설정을 마쳤습니다"라는 답을 들으면 IPv6 쪽도 막았는지 한 번 더 묻고, 확인은 IPv6가 되는 바깥 네트워크에서 curl -6 한 줄로 하면 된다.

YS

신윤섭

데이너스 대표 | AI 교육 & AX 컨설팅

126개의 AI/AX 교육 과정을 설계하고, 90개 교육·협업 기관에서 강의했습니다. 강남세브란스, 삼성전자, 현대자동차 등 다양한 조직의 AI 역량 강화를 지원하고 있습니다.

AI 교육이 필요하신가요?

조직에 맞는 맞춤형 AI/AX 교육 프로그램을 설계해드립니다. 커리큘럼 상담부터 시작해보세요.

같은 주제의 다른 글