Client VPN은 클라이언트에 주소를 주고 핸드셰이크도 끝나지만 연결은 아무 데도 가지 않습니다. 인증서나 클라이언트 버전을 건드리기 전에 엔드포인트 자체의 라우트 테이블을 보세요. AWS Client VPN 엔드포인트는 라우트 중 하나가 목적지 프리픽스를 포함할 때만 패킷을 전달합니다. 아무것도 일치하지 않으면 패킷은 엔드포인트에서 버려지고 대상 호스트는 SYN을 한 번도 보지 못합니다.

이것은 포워딩 결정이지 인증 결정이 아닙니다. 터널이 올라오고 권한 부여 규칙이 평가된 뒤에 일어나므로, 유효한 인증서와 유효한 그룹이 있어도 죽은 연결이 될 수 있습니다.

엔드포인트는 라우트 테이블이 포함하는 것만 전달한다

각 라우트는 목적지 CIDR과 트래픽을 받아야 할 서브넷입니다. 엔드포인트는 클라이언트의 목적지를 그 목록과 비교해 일치하는 항목을 고르고 패킷을 연결된 서브넷으로 보냅니다. 항목이 없으면 다음 홉도 없다.

라우트가 무엇이 아닌지 보세요. VPC 라우트 테이블 항목이 아니고, 무엇도 권한을 부여하지 않습니다. 권한 부여 규칙은 그룹이 목적지에 도달할 수 있는지 정하고, 라우트는 엔드포인트에 패킷을 보낼 곳이 있는지 정합니다. 둘 다 일치해야 하고 실패 증상도 다르므로, 어느 쪽이 고장 났는지 추측하면 오후를 낭비합니다.

전형적인 불일치는 피어링된 VPC입니다. 호스트가 10.20.4.0/24에 있는데 누군가 10.20.0.0/24 경로를 추가해서 주소가 모든 항목 밖으로 떨어집니다. 클라이언트 측 캡처에는 응답 없는 SYN이 반복되고, 대상 로그에는 아무것도 남지 않습니다.

인증서보다 라우트 목록을 먼저 읽는다

엔드포인트의 라우트는 별도 객체이므로 VPC 테이블을 읽는 대신 직접 조회하세요.

aws ec2 describe-client-vpn-routes --client-vpn-endpoint-id cvpn-endpoint-0123456789abcdef0 --query 'Routes[].{Dest:DestinationCidr,Target:TargetSubnet,Origin:Origin}'

실패하는 클라이언트에서 정확한 주소와 프리픽스 길이를 가져온 뒤 문자 단위로 포함 여부를 확인하세요. 최장 프리픽스 일치가 적용되므로, 더 좁은 라우트가 다른 곳을 가리키면 더 넓은 라우트로는 해결되지 않습니다.

for_each로 목적지마다 리소스 하나

손으로 쓴 라우트 블록은 누군가 콘솔에서 프리픽스를 추가하는 순간 어긋납니다. 목적지를 맵에 두고 키마다 리소스를 하나씩 만들면 추가와 삭제가 분리됩니다.

variable "client_vpn_routes" {
  type = map(string)
  default = {
    "10.20.0.0/16"  = "subnet-0aa11bb22cc33dd44"
    "172.31.0.0/20" = "subnet-0aa11bb22cc33dd44"
  }
}

resource "aws_ec2_client_vpn_route" "this" {
  for_each               = var.client_vpn_routes
  client_vpn_endpoint_id = var.client_vpn_endpoint_id
  destination_cidr_block = each.key
  target_vpc_subnet_id   = each.value
  depends_on             = [aws_ec2_client_vpn_network_association.this]
}

네트워크 연결에 대한 의존성을 유지하세요. 라우트에는 연결된 서브넷이 필요하므로 둘 다 만드는 apply는 연결을 먼저 정렬해야 하고, 그 참조가 없으면 Terraform이 라우트를 먼저 시도할 수 있습니다. 그다음 plan을 읽으세요. 라우트 추가 수는 추가한 프리픽스와 일치해야 합니다. plan이 엔드포인트도 교체한다면 엔드포인트 리소스가 잘못 작성된 것이고, 적용하면 연결된 모든 클라이언트가 끊깁니다.

직접 만들지 않은 엔드포인트 인수하기

대부분의 팀은 콘솔에서 만든 엔드포인트를 물려받기 때문에 라우트 코드가 없습니다. 라우트를 Terraform에 넣기 위해 두 번째 엔드포인트를 세우지 마세요.

대신 import하거나 id를 변수로 넘기고 물려받은 객체는 관리하지 않는 상태로 두세요. import에는 공급자가 리소스 유형별로 문서화한 식별자 형식이 필요하며, 이는 엔드포인트, 목적지 프리픽스, 대상 서브넷을 인코딩합니다. 기억으로 조합하지 말고 리소스 문서에서 그 문자열을 복사한 뒤 plan을 실행하고, 새로 추가하기 전에 변경이 없기를 기대하세요.

패킷이 지나가는 순서대로 추적한다

라우트가 있는데도 연결이 실패하면 상위 계층에서 추측하지 말고 경로를 순서대로 따라가세요.

  1. 라우트: 프리픽스가 목적지를 포함하고 엔드포인트에 연결된 서브넷을 가리킨다.
  2. 권한 부여: 권한 부여 규칙이 사용자 그룹에 그 목적지를 허용한다. 라우트만으로는 결코 접근이 허용되지 않는다.
  3. 반환 경로: 대상 서브넷의 라우트 테이블이 응답을 엔드포인트 쪽으로 돌려보내고, 보안 그룹이 클라이언트 주소 풀에서 오는 포트를 허용한다.
  4. 애플리케이션: 클라이언트에서 실제 TCP 연결을 열고, 대상 로그에 풀에서 온 예상 출발지 주소가 보이는지 확인한다.

apply마다 프리픽스 하나를 바꾸고, 그 뒤 라우트 목록을 다시 읽고, 목적지와 권한을 부여하는 규칙, 반환 경로를 함께 기록하세요. 거부된 연결을 다음에 디버깅할 사람은 인증서 이론이 아니라 그 목록에서 시작해야 합니다.

관련 글