VPN 클라이언트가 터널에서 서로에게 도달하지 못하게 하세요
서비스에 도달하는 것은 경로이고, 다른 클라이언트에 도달하는 것은 정책입니다. 피어 트래픽을 결정하는 전달 지점을 찾은 다음 클라이언트에서 거부를 증명하세요.
두 클라이언트가 모두 점프 호스트를 열 수 있어도 서로를 열아야 할 일은 없습니다. 서비스에 도달하는 것은 전달 결정이고, 피어에 도달하는 것은 정책 결정이며, 대부분의 VPN 스택은 이 둘을 별도 객체에 보관합니다. 첫 번째를 위해 구성한 어떤 것도 두 번째를 강제하지 않습니다. 따라서 작업은 규칙을 추가하는 것이 아니라, 이 서버에서 피어 트래픽을 결정하는 객체를 찾아 거기에 거부를 쓰고 클라이언트에서 증명하는 것입니다.
공유 서비스로 가는 경로는 피어 간의 허가가 아닙니다
클라이언트 서브넷을 광고하면 피어에게 다른 클라이언트 주소를 향해 패킷을 회선에 어떻게 올릴지 알려 줍니다. 그 패킷이 전달될지는 나중에 그것을 전달하는 것에 의해 결정됩니다. Linux 게이트웨이에서는 FORWARD 체인이고, 사용자 공간 데몬 안에서는 터널 자체의 세션 테이블입니다. 경로는 도달 가능성에 답하고 허가는 건드리지 않습니다.
그래서 경로만 읽는 격리 점검은 아무것도 증명하지 못합니다. /24를 모든 클라이언트에 밀어 넣어도 피어 간 패킷은 모두 전달 훅에서 버려질 수 있습니다. 위험한 경우는 반대입니다. 명시적 규칙이 없고 기본값이 허용적이며, 아무도 시험하지 않았기 때문에만 존재하는 격리입니다.
터미널을 열기 전에 요구 사항을 한 줄로 쓰세요. 서비스 블록과 점프 호스트에는 도달하고, 클라이언트 블록 안의 다른 무엇에도 도달하지 않아야 합니다.
누가 실제로 피어 패킷을 전달하는지 알아내세요
서버의 두 가지 동작은 클라이언트 쪽에서 같아 보이고 서로 다른 규칙을 요구합니다. 커널이 패킷을 터널 인터페이스 밖으로 다시 전달하거나, 데몬이 세션 사이에서 복사하고 커널은 그것을 보지 못합니다.
WireGuard는 첫 번째 그룹입니다. 피어 간 스위치가 없으므로 10.8.0.4에서 10.8.0.5로 가는 패킷은 wg0에 도착해 전달 훅에 닿고, AllowedIPs가 두 주소를 모두 덮으면 다시 wg0 밖으로 라우팅됩니다. OpenVPN은 서버 구성에 client-to-client가 설정되면 두 번째 그룹입니다. 그 지시문이 사용자 공간에서 세션 사이로 패킷을 넘기기 때문입니다. 무엇이든 쓰기 전에 로드된 구성에서 그 지시문을 확인하세요. 트래픽을 결코 보지 못하는 커널 규칙은 작동하는 규칙과 구별할 수 없고, 그래서 배포가 격리 없이 끝납니다.
로드되었다고 가정하는 파일이 아니라 유효 구성을 읽으세요. 배포판 패키지는 자체 포함을 가져오기 때문입니다. 데몬이 전달한다면 거부는 데몬 자체의 클라이언트별 제어에 속하고, 호스트 방화벽은 호스트로 향한 트래픽만 필터링합니다. 커널이 전달한다면 전달 훅만이 중요한 곳입니다.
라우팅 및 변환 배포에는 점검이 하나 더 필요합니다. 먼 클라이언트가 보는 원본이 기대한 것과 다를 수 있습니다. 그러면 클라이언트 서브넷과 일치하는 규칙은 아무것도 일치하지 않습니다. 일치를 쓰기 전에 변환에서 살아남는 주소를 기록하고, NAT 주소를 추측하기보다 인터페이스 쌍을 일치시키는 편을 택하세요.
각 주소에 스스로 폐기할 수 있는 신원을 주세요
두 장치가 자격 증명 하나를 공유하는 순간 격리는 무너집니다. 그 신원에 묶인 규칙은 둘을 분리할 수 없고, 분실한 노트북을 폐기하면 그룹 전체가 폐기됩니다. 장치별 인증서나 피어별 키는 폐기를 국소적으로, 정책을 읽기 쉽게 유지합니다. 각 주소가 정확히 한 사람이나 기계에 대응하기 때문입니다. 설계가 그렇게 할 수 없다면 규칙을 세밀하다고 설명하는 대신 런북에 그렇게 적으세요.
주소 할당은 정책과 일치해야 합니다. 서비스용 블록과 클라이언트용 블록을 예약하고, 클라이언트 주소는 먼저 온 사람이 가져가는 범위가 아니라 통제하는 풀에서 나눠 주세요. 서버가 변환하거나 브리지한다면 매핑 표를 구성 옆에 두세요. 다음 독자가 규칙을 다시 이끌어내는 데 필요한 것이 바로 그 표입니다.
거부를 패킷이 터널을 가로지르는 곳에 두세요
WireGuard 트래픽을 전달하는 게이트웨이에서는 두 규칙이면 충분합니다. 서비스 블록을 허용하고 클라이언트 블록의 나머지를 버립니다. 인터페이스 쌍은 주소만큼 중요하므로 목적지만 일치시키면 LAN에서 오는 트래픽도 잡기 때문에 허용이 먼저 평가되도록 그 순서로 로드하세요.
nftables에서는 이 쌍이 기존 inet filter 테이블로 들어가며 허용이 먼저, 버리기가 다음입니다. 두 일치 모두 주소와 함께 인터페이스 쌍을 고정합니다.
nft add rule inet filter forward iifname "wg0" oifname "wg0" ip saddr 10.8.0.0/24 ip daddr 10.8.2.0/24 accept
nft add rule inet filter forward iifname "wg0" oifname "wg0" ip saddr 10.8.0.0/24 ip daddr 10.8.0.0/24 drop
로드한 뒤 종료 상태를 믿지 말고 체인을 다시 읽어 두 규칙이 의도한 순서에 있는지 확인하세요. IPv6 풀에는 일치하는 ip6 규칙을 추가하세요. 그렇지 않으면 이중 스택 터널에서 v4 쌍은 장식일 뿐입니다. 서버에 이미 허용적인 전달 정책이 있다면 체인을 통째로 바꾸지 말고 버리기를 덧붙이세요.
OpenVPN에서 client-to-client가 켜져 있으면 이 전달 규칙은 피어 트래픽과 결코 일치하지 않으며 거부는 데몬 자체의 제어에 있어야 합니다. 호스트로 향한 트래픽에는 커널 규칙을 유지하되, 격리 제어로 계산하지 마세요.
거부를 서버가 아니라 클라이언트에서 증명하세요
게이트웨이에서 시험하는 것은 아무것도 증명하지 못합니다. 로컬에서 생성된 트래픽은 출력 경로를 타고 전달 훅을 결코 지나지 않기 때문입니다. 실제 클라이언트 두 대를 쓰고 허용 사례를 먼저 실행해 끊어진 터널이 작동하는 정책으로 오해되지 않게 하세요.
허용된 클라이언트에서 서비스가 자기 포트에서 응답하는지 확인하고, 같은 클라이언트에서 피어 주소를 시도해 거부가 아니라 조용한 버리기를 기대하세요.
ping -c 3 -W 2 10.8.0.5
nc -z -w 2 10.8.0.5 22
nft list chain inet filter forward
iptables -vnL FORWARD
ping -6 -c 3 -W 2 fd00:8::5
버려진 패킷과 오프라인 피어는 둘 다 시간 초과처럼 보이므로 결론을 내리기 전에 카운터를 확인하세요. 피어가 분명히 살아 있는 동안 버리기 규칙의 카운터가 증가하는지 확인하세요. IPv6에서도 반복하세요. ip6 규칙이 없으면 v4 거부가 효과적으로 보입니다. 수락 및 거부 결과를 날짜와 함께 런북에 기록하세요. 다음 이전이 규칙을 다시 이끌어낼 것이기 때문입니다.