VPS 방화벽: VPN 포트만 열고, 그다음 증명하라
포트 하나는 공개로 남아 있어야 하며, 그렇지 않으면 어떤 클라이언트도 접속하지 못한다. 나머지는 거부하고, 포워딩을 입력과 분리하며, 미리 시험한 복구 경로를 남겨 두라.
VPN 포트만 허용하는 방화벽은 하나의 규칙이 아니다. 같은 포트 번호에 얽힌 세 가지 별개의 결정이며, 입력 체인은 로컬 프로세스에 무엇이 도달할 수 있는지 정하고, 포워드 체인은 터널 클라이언트를 대신해 호스트를 통과할 수 있는 것을 정하며, NAT 테이블은 나갈 때 출발지 주소를 다시 쓴다. 하나만 틀려도 결과가 깔끔한 장애인 경우는 드물다. 클라이언트는 받아들이지만 아무것도 불러오지 않는 호스트가 되거나, 누군가 체인 순서를 읽을 때까지 닫힌 것처럼 보이는 호스트가 된다.
닫을 수 없는 유일한 포트
VPN 리스너는 예측할 수 없는 주소에서 연결을 받아들여야 하므로 정의상 공개 서비스다. 설계가 전제하는 유일한 예외가 이것이다. OpenVPN용 UDP 포트 하나, WireGuard용 UDP 포트 하나, SSL VPN용 TCP 포트 하나. 나머지는 모두 터널 뒤에 속한다. 즉 애플리케이션 서비스는 모든 인터페이스가 아니라 터널 주소나 루프백에 바인딩해야 한다.
바인딩은 강제가 아니다. 0.0.0.0 에 바인딩된 프로세스는 규칙이 허용할 때만 도달할 수 있고, 기본 정책은 어떤 규칙과도 일치하지 않은 트래픽을 위한 대비책이지 포트를 지배하는 규칙이 아니다. drop 위에 놓인 accept 는 여전히 accept 이며, 서비스를 공개 주소에서 떼어내는 것은 독립적인 개선이지 체인의 대체물이 아니다.
입력, 포워드, NAT는 서로 다른 질문에 답한다
입력 트래픽은 호스트 자신을 향하므로 VPN 리스너와 관리 경로가 그곳에 산다. 포워드 트래픽은 호스트를 지나가고, 터널 클라이언트는 그것과 net.ipv4.ip_forward=1 을 필요로 한다. 전형적인 반쯤 망가진 설정은 나가는 흐름만 허용하고 돌아오는 방향을 잊어버린다. 그래서 클라이언트는 핸드셰이크를 마치고도 아무것도 불러오지 못한다. 확립 및 관련 트래픽에 대한 conntrack 규칙이 그 방향을 덮는다.
NAT 는 세 번째 질문이다. postrouting 에서 다시 쓰지 않으면 포워드된 패킷은 터널의 사설 출발지로 나가 상위에서 버려진다. 호스트 너머의 어떤 네트워크도 풀을 되돌리는 경로를 모르기 때문이다. 그 규칙의 출발지 프리픽스는 호스트 자신의 주소가 아니라 VPN 풀과 맞아야 하고, 클라이언트 서브넷을 지정하는 포워드 규칙도 마찬가지다.
IPv6 는 두 번째 정책이지 나중에 덧붙이는 생각이 아니다. 엄격한 IPv4 체인과 열린 ip6tables 정책을 가진 호스트는 아무것도 닫지 않은 것이다. 서비스는 다른 주소 체계로 여전히 도달 가능하다. ip6tables 로 규칙 집합을 그대로 복제하거나, 공개 인터페이스에서 IPv6 를 의도적으로 끄고 어느 쪽을 택했는지 적어 두라.
한 번에 읽히는 기준선
drop 에서 시작해 호스트에 필요한 것만 더한다. 먼저 루프백과 응답, 다음 관리 경로, 다음 터널 리스너, 다음 클라이언트를 위한 포워딩 정책이다.
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p udp --dport 1194 -m conntrack --ctstate NEW -j ACCEPT
sysctl -w net.ipv4.ip_forward=1
iptables -A FORWARD -i tun0 -o eth0 -s 10.8.0.0/24 -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE인터페이스 이름, 포트, 풀은 자리 표시자이니, 복사하지 말고 서버에서 읽어라. 결과를 저장하는 것은 기록하는 것만큼 중요하다. 메모리에만 사는 규칙 집합은 다음 재부팅에서 사라지고 호스트를 조용히 다시 열어 버린다. 배포판의 방식으로 영구화하고, 머신이 부팅되지 않을 날을 위해 머신 밖에 사본을 두라.
호스트를 잃지 않고 마지막 포트를 닫기
설계에는 함정이 있다. SSH 가 터널을 통해서만 존재한다면 VPN 서비스 장애가 SSH 를 함께 데려간다. 돌아갈 길은 한 번도 열어 본 적 없는 콘솔뿐일 수 있다. 마지막 공개 포트를 닫기 전에 그 콘솔이 존재하고 실제로 쓸 수 있는지 확인하거나, 터널에 의존하지 않는 좁은 관리 경로 하나를 남겨라. 고정 주소에서의 SSH, 키만 허용, 비밀번호 인증 끔.
변경은 이미 확립된 세션에서 적용하라. 필터링하려는 연결을 통해서가 아니다. 실수가 영구화되지 않고 만료되도록 롤백을 예약하라. 예를 들어 이전 규칙 집합을 복원하는 systemd-run --on-active=10min 작업이다. 첫 세션을 시험하는 동안 두 번째 SSH 세션을 열어 두라. 그다음 롤백 작업이 실제로 발동하는지 확인하라. 시험하지 않은 복구는 복구가 없는 것과 같다.
스크립트가 아니라 경로를 검증하라
로드된 규칙을 읽어라, 그것을 로드했어야 할 파일이 아니라. 승인된 외부 호스트에서 의도한 포트만 응답하는지 확인하라. 터널 안에서는 공개하려던 서비스에 도달할 수 있는지 확인하라. 기본 거부 정책이 터널 트래픽까지 막는다면, 누군가 써 보기 전까지는 작동하는 잠금과 똑같이 보인다.
iptables -S
iptables -t nat -S
ss -lntup
ip -4 addr show허용한 포트, 클라이언트 풀, 터널 인터페이스, 관리 출발지, 콘솔 절차를 설정 옆에 기록하라. 다음에 풀이나 인터페이스 이름을 바꿀 때는 필터와 NAT 규칙 양쪽을 맞춰야 한다. 그렇지 않으면 호스트는 아무도 모르게 반쯤 열린 상태로 돌아간다.
규칙 집합보다 값진 습관이 하나 있다. 변경할 때마다 클라이언트 요청으로 포워드 경로를, 두 번째 세션으로 복구 경로를 증명하라. 쓰지 않은 잠금은 가설일 뿐이다.