--- kind: PROJECT_DECISION slug: edge-nginx-moved-into-a-guest-vm title: 엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리 topic: lab-environment-build topicName: 실험대 환경 구성 project: virtualization status: 게시 전 decisionStatus: ADOPTED sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6 source: - final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나 - final/document.md#178-이-부의-출처와-범위 - final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다 --- # 엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리 호스트에서 돌던 엣지 nginx 를 게스트 한 대(.10) 안으로 옮기고, 호스트는 커널 DNAT 로 tailnet 의 443 포트를 그 게스트에 넘기기만 한다. 바꾼 이유는 성능이 아니라 자주 갈아엎는 층의 격리다. 밖에서 게스트로 들어오는 경로가 FORWARD 가 되면서 libvirt 방화벽에 구멍이 새로 필요해졌다. ## 근거 - **호스트 안에서는 404, 밖에서는 connection refused — 앞 체인의 accept 가 libvirt 의 reject 를 막지 못했다** 이 결정이 새로 요구한 것 가운데 3번을 실제로 치른 기록이다. 밖에서 게스트로 들어오는 경로가 FORWARD 가 되면서 libvirt 방화벽의 reject 에 막혔고, §180 은 이곳을 이 구축에서 가장 오래 막힌 곳으로 적었다. - **단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다** 이 구성을 세운 기반 7단계 가이드를 순서대로 따라가면서 나온 결함 여섯이 그 기준의 근거다. 여섯 중 셋이 여기서 옮긴 nginx 와 인증서를 다룬다. - **libvirt 의 firewall_backend 가 iptables 일 때도 guest_input 에 구멍이 필요한가** §180 이 미확인으로 남긴 것을 받는 물음이다. 아래 3번은 이 호스트의 nftables 백엔드에서만 확인했다. - **Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge** 커널 DNAT 뒤에 이어지는 게스트 쪽 경로를 그 글이 설명한다. - **이 호스트의 가상 머신 네트워크는 Bridge 인가 NAT 인가 Routing 인가** §178 이 적은 대로 이 호스트는 이더넷이 없어 libvirt NAT 를 택했다. 그 물음이 실제 구성을 이 호스트에서 확인한다. ## 결정문 엣지 nginx 를 물리 호스트에서 게스트 VM 으로 옮기고, 호스트는 커널 DNAT 로 tailnet 의 443 포트를 그 게스트에 넘기기만 한다. §179 이 전후 경로를 나란히 적었다. 전 : tailnet 의 443 포트에서 호스트 nginx 를 거쳐 게스트 .11/.12 의 Traefik 으로 후 : tailnet 의 443 포트에서 호스트 커널 DNAT 를 거쳐 엣지 nginx(.10) 으로, 거기서 .11/.12 의 Traefik 으로 ## 판단 이유 바꾼 이유로 §179 이 든 것은 성능이 아니라 더러워지는 층의 격리다. nginx 설정과 인증서와 certbot 과 deploy 훅은 자주 갈아엎는 것들이라 호스트에 있으면 초기화가 불가능하다. 엣지에 장애를 일부러 넣어 보는 실험도 호스트에서 하면 SSH 까지 위험해진다. 성능은 근거가 아니다. L7 홉 수는 전후 모두 2홉이고(observed), 늘어난 것은 커널이 하는 L4 전달 한 번뿐이어서 X-Forwarded-* 계약은 그대로 성립한다. 전환 전후를 같은 부하로 잰 측정은 이 저장소에 없으므로, 느려지지 않았다는 말은 이 기록이 하지 않는다. §178 이 적었듯 이 호스트에는 이더넷 없이 WiFi 만 있어 브리지를 못 쓰고 libvirt NAT(virbr0) 와 호스트 진입 구조를 택했다. 견준 것은 호스트에 두기와 게스트로 옮기기 둘이다. 브리지와 NAT 는 고른 것이 아니라 이더넷이 없어 하나만 남았다. ## 영향 감수한 비용을 §179 이 일곱 줄의 표로 적고, 그중 둘을 나머지 다섯과 갈라 놓았다(inferred). 경로가 OUTPUT 에서 FORWARD 로 바뀌면서 생긴 것이 그 둘이다. DNAT(2번) : 전에는 호스트가 443 포트를 직접 들었으니 넘길 일이 없었다. 지금은 호스트에 리스너가 아예 없다 libvirt 방화벽에 구멍(3번) : 호스트에서 게스트로 가는 것은 OUTPUT 경로라 필터를 안 탔다. 밖에서 게스트로 들어오는 것은 FORWARD 다 호스트가 게스트에 접속하는 것과 밖에서 게스트로 들어오는 것은 커널이 보기에 완전히 다른 일이다. 3번을 뚫는 데 이 구축에서 가장 오래 걸렸다. §180 에서 밖에서 친 curl 은 connection refused 로 돌아왔다. libvirt 의 guest_input 체인을 끝내는 reject 규칙의 카운터 4 패킷이 그때 친 curl 횟수와 정확히 일치했다. 구멍이 필요하다는 것을 몰라서 오래 걸린 것은 아니다. DNAT 파일에는 priority filter - 10 으로 먼저 도는 forward 체인과 거기 넣은 ct state new accept 가 이미 있었고, 그것이면 열린다고 보고 세웠다 — iptables 감각으로 쓰면 정확히 여기서 틀린다고 §180 이 적었다. 같은 계열의 가드레일이 하나 더 붙는다. SNAT 금지를 명시(4번) : L4 를 한 번 더 타면서 masquerade 를 붙이고 싶어지는데, 붙이면 엣지가 모든 클라이언트를 192.168.122.1 로 본다 나머지 넷은 배포판이 달라서 생긴 잡무다. nginx 설치(1번) : 호스트에는 이미 있었다. 새 게스트의 cloud-init 은 curl 과 nftables 만 깐다 sites-available 관례(5번) : 호스트는 Arch 라 그 디렉터리가 없어 nginx.conf 에 include 를 직접 넣었다. 게스트는 Debian 이라 기본으로 있다 nginx 버전 차이(6번) : Arch 1.30 vs Debian 12 의 1.22. http2 on; 지시어가 1.25.1 이상이다 certbot 과 인증서와 갱신 훅이 게스트로(7번) : 인증서를 읽는 주체가 nginx 이기 때문이다 얻은 것은 격리다. 자주 갈아엎는 층이 게스트 한 대 안으로 들어가니 그 게스트를 통째로 다시 세울 수 있고, 엣지에 장애를 넣는 실험이 호스트 SSH 를 건드리지 않는다. 그러면서 L7 홉 수는 2홉 그대로라 뒤쪽 Traefik 이 받는 X-Forwarded-* 계약도 바뀌지 않았다. 아직 재지 않은 것이 셋 있다. 전환 전후를 같은 부하로 잰 측정은 이 저장소에 없다. 3번의 구멍은 휘발성이라 libvirt 가 네트워크를 다시 세우면 없어지고, 그래서 DNAT 유닛의 ExecStartPost 에 넣었다. 그 조치가 재기동을 견디는지 실제로 다시 세워 확인한 출력은 없다. libvirt 의 firewall_backend 가 iptables 일 때도 같은 구멍이 필요한지는 §180 이 미확인으로 남겼고, 이 호스트는 nftables 백엔드다.