Published on

홈서버 구축기 3 - 외부 접속 완전 정복: 포트포워딩부터 Cloudflare DDNS Gateway API까지

Authors

홈서버를 구축하면서 가장 까다로운 부분은 '외부에서 어떻게 안전하게 내 서버로 들어오게 할 것인가'입니다. 진정한 홈서버의 가치는 '언제 어디서나' 접속 가능하다는 점에 있습니다.

지난 글에서 단일 노드 Kubernetes 클러스터까지 띄웠습니다. 이번 글은 그 클러스터 앞단에 놓이는 네트워크 단계를 다룹니다. 단순한 "따라 하세요" 가이드가 아니라 외부망(인터넷)에서 내부망(우리집 공유기 안)의 서버까지 들어오는 각 단계가 왜 필요한지어떤 원리로 동작하는지를 같이 정리합니다.


1. 외부 접속의 기초: 공인 IP 사설 IP 포트포워딩

1.1 공인 IP와 사설 IP

가장 먼저 이해해야 할 것은 IP 주소의 개념입니다.

인터넷에 가입하면 ISP(KT SKT LG 등)로부터 할당받는 IP는 공인 IP(Public IP) 하나뿐입니다. 그런데 집에서 사용하는 PC 스마트폰 TV 그리고 이번에 구축한 홈서버까지 수많은 기기가 인터넷을 써야 합니다. 공유기는 이 하나의 공인 IP를 사설 IP(Private IP) (예: 172.30.x.x 192.168.x.x)로 쪼개서 내부 기기들에 나눠 줍니다.

외부에서 우리 집으로 들어오려면 공인 IP를 따라 들어와야 하는데 공유기 문 앞까지만 도달할 수 있을 뿐 그 안의 어떤 기기가 '서버'인지 알 수 없습니다. 이를 해결하기 위해 필요한 것이 포트포워딩입니다.

1.2 서버 사설 IP 고정

포트포워딩을 걸기 전에 서버의 사설 IP가 바뀌지 않도록 고정해야 합니다. 공유기는 보통 DHCP로 IP를 유동적으로 할당하기 때문에 재부팅 시 서버에 다른 IP가 떨어질 수 있습니다. 공유기 관리자 페이지에서 서버의 MAC 주소를 특정 IP에 묶어 두면 됩니다.

[!NOTE] 공유기 고정 IP 설정 메뉴 이름은 공유기마다 다른데 '장치 설정' 'DHCP 할당 설정' 'IP/MAC 바인딩' '주소 예약' 같은 이름으로 들어가 있습니다. 서버의 MAC 주소는 리눅스에서 ip link show <인터페이스명>으로 확인할 수 있습니다.

이 단계는 홈서버 구축기 2의 첫 절에서도 짚었습니다. 쿠버네티스 노드 IP 안정성과 포트포워딩 양쪽 모두에 영향을 주는 작업입니다.

1.3 포트포워딩

IP 고정이 끝났다면 길을 터 줄 차례입니다. 포트포워딩은 공유기에게 "외부에서 A번 문(Port)으로 들어오면 내부 특정 IP(서버)의 B번 문으로 연결해 줘" 라는 규칙을 정해 주는 것입니다.

주로 사용하는 포트는 다음과 같습니다.

  • SSH(원격 접속) - 22번 (보안을 위해 외부 포트는 2222 등 다른 번호로 돌리는 것을 추천)
  • HTTP(웹 서버) - 80번
  • HTTPS(보안 웹) - 443번

1.4 NodePort 비대칭 매핑

쿠버네티스 위에 띄운 Gateway는 보통 NodePort 서비스로 노드 외부에 노출됩니다. 그런데 NodePort의 기본 허용 범위는 30000-32767이라서 443 같은 well-known 포트를 직접 NodePort로 잡을 수 없습니다.

예를 들어 우리 클러스터의 nginx-gateway/edge-gw-nginx 서비스는 다음과 같이 노출되어 있습니다.

NAME            TYPE       PORT(S)
edge-gw-nginx   NodePort   443:31040/TCP

컨테이너 내부 포트는 443이지만 노드 외부에서는 31040으로만 들어올 수 있습니다. 이때 공유기에서 외부/내부 포트를 비대칭으로 매핑해 줍니다.

항목
외부(WAN) 포트443
내부 IP192.168.0.100 (서버 사설 IP)
내부 포트31040 (NodePort)
프로토콜TCP

[!WARNING] 외부 포트도 31040으로 잡으면 Cloudflare proxy가 origin으로 443으로 보내기 때문에 연결이 실패합니다. 외부는 반드시 443, 내부는 NodePort 그대로여야 합니다.

1.5 공인 IP 검증과 CGNAT 판별

포트포워딩을 잘 걸어 두었는데도 외부에서 안 들어오는 경우가 있습니다. 가장 먼저 의심할 것은 내가 받은 IP가 진짜 공인 IP가 맞는가입니다. 일부 ISP는 한 공인 IP를 여러 가입자가 나눠 쓰는 CGNAT(Carrier-Grade NAT) 을 적용해 두는데 이러면 포트포워딩 자체가 불가능합니다.

서버에서 여러 소스로 교차검증합니다.

echo "ifconfig.me  : $(curl -s https://ifconfig.me)"
echo "icanhazip    : $(curl -s https://icanhazip.com)"
echo "ipify        : $(curl -s https://api.ipify.org)"
echo "checkip(AWS) : $(curl -s https://checkip.amazonaws.com)"
echo "OpenDNS      : $(dig +short myip.opendns.com @resolver1.opendns.com)"

다섯 군데가 모두 같은 IP를 반환하면 그 값이 ISP가 우리 회선에 매단 외부 IP입니다.

그 다음 공유기 관리 페이지의 WAN IP를 봅니다.

  • 공유기 WAN IP가 위 결과와 같다 → CGNAT 아님 (포트포워딩 가능)
  • 공유기 WAN IP가 100.64.0.0/10 대역(100.64.x.x ~ 100.127.x.x)이거나 10.x.x.x → CGNAT (ISP에 공인 IP 요청 필요)

2. 연결의 시작: Cloudflare DDNS와 ddclient

2.1 왜 DDNS가 필요한가

포트포워딩까지 마쳤으면 외부 접속이 가능해집니다. 그런데 가정용 인터넷 회선은 **공인 IP조차 유동적(Dynamic IP)**이라는 문제가 남아 있습니다. 컴퓨터를 껐다 켜거나 공유기가 재부팅되면 우리 집 주소(IP)가 바뀌어 버립니다.

도메인(예: movingzero.org)은 특정 IP를 가리키는 전화번호부 같은 존재인데 전화번호(IP)가 계속 바뀌면 전화를 걸 수 없습니다.

이 IP 숫자를 외울 필요 없이 변하지 않는 도메인 주소로 접속하게 해 주는 서비스가 DDNS(Dynamic DNS) 입니다. 동작은 다음과 같습니다.

  1. 서버에 설치된 에이전트(ddclient)가 주기적으로 "내 현재 공인 IP"를 확인합니다.
  2. IP가 바뀐 것을 감지하면 Cloudflare API를 호출해 DNS 레코드 수정을 요청합니다.
  3. Cloudflare는 전 세계 DNS 서버에 변경된 IP를 전파합니다.

2.2 ddclient 설치와 설정

리눅스 표준 패키지인 ddclient를 사용합니다.

sudo apt update
sudo apt install -y ddclient

설치가 끝나면 /etc/ddclient.conf를 수정합니다.

[!NOTE] API 토큰은 Cloudflare 대시보드의 My Profile → API Tokens → Create Token에서 Edit zone DNS 템플릿으로 발급합니다. Global API Key는 권한이 너무 넓으니 피합니다.

/etc/ddclient.conf
# 기본 설정
daemon=300                    # 5분마다 체크
syslog=yes
pid=/var/run/ddclient.pid
ssl=yes

# 외부 IP 확인 방법
use=web, web=https://cloudflare.com/cdn-cgi/trace

# Cloudflare 설정
protocol=cloudflare
zone=movingzero.org           # 구매한 도메인
login=token                   # 토큰 방식 사용 명시
password=YOUR_API_TOKEN       # 발급받은 API 토큰

# 업데이트할 서브도메인들
argocd.movingzero.org, homeserver.movingzero.org

설정이 끝나면 서비스를 다시 시작합니다.

sudo systemctl restart ddclient
sudo systemctl enable ddclient

2.3 트러블슈팅: 캐시 문제

ddclient는 불필요한 API 호출을 막기 위해 마지막 성공/실패 기록을 캐싱합니다. 설정 오류로 한 번 실패하면 설정을 고쳐도 "5분 뒤에 다시 해" 같은 메시지만 뜨고 멈춰 있을 수 있습니다. 이때는 캐시를 지우고 강제 실행으로 디버그합니다.

sudo rm -f /var/cache/ddclient/ddclient.cache
sudo ddclient -daemon=0 -debug -verbose -noquiet

3. 보안의 핵심: cert-manager와 DNS-01 챌린지

3.1 DNS-01 챌린지란

HTTPS를 적용하려면 공인 인증서가 필요합니다. Let's Encrypt 같은 인증 기관은 "이 도메인이 진짜 네 거냐"를 묻고 답을 받는 절차를 거치는데 이 절차를 챌린지(Challenge) 라고 합니다. 주로 쓰는 두 방식이 있습니다.

  • HTTP-01 - "네 웹 서버의 특정 경로에 내가 준 파일을 올려 봐." 가장 흔한 방식이지만 포트 80이 외부에 열려 있어야 합니다. 홈서버 환경에선 ISP가 포트 80을 막아 두는 경우가 많아 불편합니다.
  • DNS-01 - "네 도메인 DNS에 내가 준 값을 TXT 레코드로 등록해 봐." API로 자동화할 수 있고 내부망 서버도 발급이 가능하며 와일드카드 인증서 발급까지 지원합니다.

홈서버에서는 DNS-01이 거의 정답입니다. 서브도메인 관리를 편하게 하기 위해 *.movingzero.org 와일드카드 인증서를 DNS-01 방식으로 받습니다.

3.2 ClusterIssuer와 Certificate

Kubernetes의 cert-manager가 이 과정을 대신 수행해 줍니다. 두 개의 리소스가 필요합니다.

  1. ClusterIssuer - 인증서를 발급해 줄 기관(Let's Encrypt)과 대화하는 방법을 정의합니다. Cloudflare API 토큰을 통해 DNS-01 챌린지를 풀게 됩니다.
  2. Certificate - 실제로 발급받을 도메인(*.movingzero.org)을 정의합니다.

[!TIP] 최신 cert-manager에서는 갱신 시마다 개인키(Private Key)를 교체하는 rotationPolicy: Always 설정을 권장합니다.

실제 매니페스트는 홈서버 구축기 2 의 4절에서 다뤘으니 여기서는 개념만 짚고 넘어갑니다.


4. 차세대 라우팅: Gateway API

4.1 Ingress와 Gateway API

기존의 Ingress는 단순했지만 설정이 유연하지 못했고 리소스 종류가 하나라서 인프라 담당과 애플리케이션 담당의 권한이 한 곳에 섞였습니다. Gateway API는 이를 개선한 차세대 표준입니다. 책임을 두 리소스로 쪼갭니다.

  • Gateway - "나는 443 포트를 열고 SSL 인증서를 끼워서 암호화된 요청을 받을게." 진입점을 정의하는 인프라 측 리소스입니다.
  • HTTPRoute - "나는 /api로 들어오는 요청을 api-service로 보낼게." 라우팅 규칙을 정의하는 애플리케이션 측 리소스입니다.

이 둘이 분리돼 있어 관리가 훨씬 명확합니다. Gateway는 인프라 팀이 관리하고 HTTPRoute는 각 서비스 팀이 자기 네임스페이스에서 만들면 됩니다.

4.2 SSL Termination

여기서는 SSL Termination 방식을 사용합니다. 경로별 암호화 상태는 다음과 같습니다.

  1. 사용자 ↔ Gateway: HTTPS(암호화)
  2. Gateway에서 인증서로 암호를 풉니다.
  3. Gateway ↔ 내부 파드: HTTP(평문)

이렇게 하면 내부의 수많은 애플리케이션마다 일일이 인증서를 관리할 필요 없이 Gateway 한 곳에서만 관리하면 됩니다.

Gateway와 HTTPRoute 매니페스트도 홈서버 구축기 2 의 5절에 정리해 두었습니다.


5. 실전 트러블슈팅: ArgoCD 무한 리다이렉트

5.1 문제 상황

ArgoCD는 기본적으로 보안을 위해 "HTTP로 접속하면 HTTPS로 강제 이동(Redirect)" 시키는 기능이 켜져 있습니다. 여기에 Gateway의 SSL Termination이 더해지면 무한 루프가 생깁니다.

  1. 사용자가 HTTPS로 Gateway에 접속.
  2. Gateway가 암호를 풀고 HTTP로 ArgoCD에 전달.
  3. ArgoCD는 "어 HTTP로 왔네 HTTPS로 다시 와" 하고 리다이렉트.
  4. 사용자가 다시 HTTPS로 접속 → Gateway가 풀어서 HTTP로 전달 → ArgoCD가 또 리다이렉트… (무한 루프)

5.2 해결: Insecure 모드

ArgoCD에게 "내 앞에 Gateway가 보안을 챙기고 있으니 너는 HTTP로 통신해도 된다"고 알려 줘야 합니다.

kubectl edit configmap argocd-cmd-params-cm -n argocd

data 섹션에 한 줄을 추가합니다.

data:
  server.insecure: "true"

설정 후 ArgoCD 서버를 재시작합니다.

kubectl rollout restart deploy argocd-server -n argocd

6. 실전 트러블슈팅: Cloudflare 521 (Origin 미도달)

6.1 증상

브라우저로 argocd.movingzero.org에 접속했는데 Cloudflare가 친 Error 521 Web server is down 페이지가 떠 있습니다. 클러스터 내부에서 NodePort로 직접 호출하면 정상 응답이 오는데 외부에서만 안 됩니다.

# 노드 안에서는 정상
curl -k -H "Host: argocd.movingzero.org" https://127.0.0.1:31040/
# HTTP/2 200 ...

# 외부에서는 521
curl -I https://argocd.movingzero.org/
# HTTP/2 521 ...

6.2 521이 의미하는 것

521은 Cloudflare 자체가 만들어 내는 에러입니다. Cloudflare가 우리 origin(공유기의 공인 IP:443)에 TCP 연결을 시도했는데 실패했다는 신호입니다. 즉 클러스터의 Gateway HTTPRoute Pod에는 도달도 못 한 상태고 문제는 외부망 ~ 공유기 ~ 노드 사이 어디엔가 있습니다.

6.3 원인 분류

크게 네 갈래로 좁혀집니다.

  1. 공유기 포트포워딩의 외부 포트가 443이 아님 — 가장 흔한 실수. 1.4절의 비대칭 매핑(외부 443 → 내부 31040)을 다시 확인합니다.
  2. ISP가 가정용 회선의 443 인바운드를 차단 — 일부 통신사가 가정용 상품의 80/443 인바운드를 막아 둡니다.
  3. CGNAT — 1.5절에서 본 경우. 공인 IP가 진짜 공인이 아닙니다.
  4. 호스트 방화벽이 31040을 막음ufw가 켜져 있다면 sudo ufw allow 31040/tcp 가 필요합니다.

6.4 진단 명령

외부망(모바일 데이터 등 우리 집 와이파이가 아닌 회선)에서 한 줄이면 1과 2를 판별할 수 있습니다.

nc -zv <공인IP> 443
  • succeeded → 포워딩과 ISP 모두 OK. 다른 곳이 문제.
  • Connection refused → 공유기 포워딩 미적용 (외부 포트가 443이 아닐 가능성 높음).
  • timed out → ISP가 443을 차단했을 가능성이 높음.

6.5 ISP 443 차단을 만났을 때: Origin Rules로 우회

ISP가 443 인바운드를 막아 두었다면 Cloudflare에서 origin port를 다른 값으로 바꿔서 보내게 할 수 있습니다. Cloudflare가 origin HTTPS로 허용하는 포트는 다음과 같습니다.

443, 2053, 2083, 2087, 2096, 8443

사용자 ↔ Cloudflare 구간은 그대로 443을 쓰고 Cloudflare ↔ origin 구간만 다른 포트로 바꿉니다. 설정 위치는 Cloudflare 대시보드의 Rules → Origin Rules입니다.

단계
Rule namebypass-isp-443-block
If incoming requests matchHostname equals *.movingzero.org
ThenRewrite to → Dynamic
Origin Port8443

이렇게 두면 Cloudflare는 origin에 8443으로 연결합니다. 공유기 포워딩도 그에 맞춰 한 줄을 더 추가합니다.

항목
외부(WAN) 포트8443
내부 IP192.168.0.100
내부 포트31040

7. 보조 노트: 외부에서 공인 IP로 직접 접근하면?

운영 환경에서 도메인 없이 https://<공인IP> 로 접속하면 두 가지 벽에 막힙니다.

  1. TLS 인증서 미스매치 — Let's Encrypt에서 받은 인증서는 *.movingzero.org 전용이라 IP로 접속하면 SNI/CN이 안 맞아 브라우저가 거부합니다.
  2. Gateway hostname 매칭 실패 — Gateway 리스너가 hostname: "*.movingzero.org"로 묶여 있어 Host 헤더가 도메인 패턴에 맞지 않으면 NGINX Gateway Fabric이 라우팅 자체를 하지 않습니다.

테스트나 디버깅용으로 DNS만 우회하고 싶다면 curl --resolve로 도메인 정보는 유지한 채 패킷만 특정 IP로 보낼 수 있습니다.

curl -v --resolve argocd.movingzero.org:443:<공인IP> https://argocd.movingzero.org/

이러면 인증서와 Host 헤더는 정상이고 라우팅 경로만 강제로 IP에 묶입니다. DNS 전파 전이나 Cloudflare 우회 검증 같은 상황에서 유용합니다.


8. 완성된 외부 접속 아키텍처

이제 홈서버는 다음과 같은 흐름으로 동작합니다.

  1. 사용자가 argocd.movingzero.org에 접속.
  2. Cloudflare DNS가 우리 집의 현재 공인 IP로 응답.
  3. 공유기가 포트포워딩을 통해 홈서버 192.168.0.100으로 트래픽 전달.
  4. Gateway가 443 포트에서 요청을 받고 Let's Encrypt 와일드카드 인증서로 TLS 종료.
  5. HTTPRoute 규칙에 따라 내부망의 ArgoCD로 트래픽 전달.
  6. ArgoCD는 평문 HTTP로 화면을 응답.

이 구조는 보안성(SSL) 편의성(DDNS Wildcard) 확장성(Gateway API)을 모두 갖춘 외부 접속 구성입니다. 새 도메인 하나를 더 띄우고 싶을 때 추가로 작업할 것은 ddclient의 도메인 목록 한 줄과 HTTPRoute 한 개뿐입니다.


이 글은 작업 내용을 AI의 도움을 받아 정리했습니다.