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

- Name
- 이동영
- Github
- @Github
홈서버를 구축하면서 가장 까다로운 부분은 '외부에서 어떻게 안전하게 내 서버로 들어오게 할 것인가'입니다. 진정한 홈서버의 가치는 '언제 어디서나' 접속 가능하다는 점에 있습니다.
지난 글에서 단일 노드 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 |
| 내부 IP | 192.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) 입니다. 동작은 다음과 같습니다.
- 서버에 설치된 에이전트(
ddclient)가 주기적으로 "내 현재 공인 IP"를 확인합니다. - IP가 바뀐 것을 감지하면 Cloudflare API를 호출해 DNS 레코드 수정을 요청합니다.
- 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는 권한이 너무 넓으니 피합니다.
# 기본 설정
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가 이 과정을 대신 수행해 줍니다. 두 개의 리소스가 필요합니다.
- ClusterIssuer - 인증서를 발급해 줄 기관(Let's Encrypt)과 대화하는 방법을 정의합니다. Cloudflare API 토큰을 통해 DNS-01 챌린지를 풀게 됩니다.
- 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 방식을 사용합니다. 경로별 암호화 상태는 다음과 같습니다.
- 사용자 ↔ Gateway: HTTPS(암호화)
- Gateway에서 인증서로 암호를 풉니다.
- Gateway ↔ 내부 파드: HTTP(평문)
이렇게 하면 내부의 수많은 애플리케이션마다 일일이 인증서를 관리할 필요 없이 Gateway 한 곳에서만 관리하면 됩니다.
Gateway와 HTTPRoute 매니페스트도 홈서버 구축기 2 의 5절에 정리해 두었습니다.
5. 실전 트러블슈팅: ArgoCD 무한 리다이렉트
5.1 문제 상황
ArgoCD는 기본적으로 보안을 위해 "HTTP로 접속하면 HTTPS로 강제 이동(Redirect)" 시키는 기능이 켜져 있습니다. 여기에 Gateway의 SSL Termination이 더해지면 무한 루프가 생깁니다.
- 사용자가 HTTPS로 Gateway에 접속.
- Gateway가 암호를 풀고 HTTP로 ArgoCD에 전달.
- ArgoCD는 "어 HTTP로 왔네 HTTPS로 다시 와" 하고 리다이렉트.
- 사용자가 다시 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 원인 분류
크게 네 갈래로 좁혀집니다.
- 공유기 포트포워딩의 외부 포트가 443이 아님 — 가장 흔한 실수. 1.4절의 비대칭 매핑(외부 443 → 내부 31040)을 다시 확인합니다.
- ISP가 가정용 회선의 443 인바운드를 차단 — 일부 통신사가 가정용 상품의 80/443 인바운드를 막아 둡니다.
- CGNAT — 1.5절에서 본 경우. 공인 IP가 진짜 공인이 아닙니다.
- 호스트 방화벽이 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 name | bypass-isp-443-block |
| If incoming requests match | Hostname equals *.movingzero.org |
| Then | Rewrite to → Dynamic |
| Origin Port | 8443 |
이렇게 두면 Cloudflare는 origin에 8443으로 연결합니다. 공유기 포워딩도 그에 맞춰 한 줄을 더 추가합니다.
| 항목 | 값 |
|---|---|
| 외부(WAN) 포트 | 8443 |
| 내부 IP | 192.168.0.100 |
| 내부 포트 | 31040 |
7. 보조 노트: 외부에서 공인 IP로 직접 접근하면?
운영 환경에서 도메인 없이 https://<공인IP> 로 접속하면 두 가지 벽에 막힙니다.
- TLS 인증서 미스매치 — Let's Encrypt에서 받은 인증서는
*.movingzero.org전용이라 IP로 접속하면 SNI/CN이 안 맞아 브라우저가 거부합니다. - 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. 완성된 외부 접속 아키텍처
이제 홈서버는 다음과 같은 흐름으로 동작합니다.
- 사용자가
argocd.movingzero.org에 접속. - Cloudflare DNS가 우리 집의 현재 공인 IP로 응답.
- 공유기가 포트포워딩을 통해 홈서버
192.168.0.100으로 트래픽 전달. - Gateway가 443 포트에서 요청을 받고 Let's Encrypt 와일드카드 인증서로 TLS 종료.
- HTTPRoute 규칙에 따라 내부망의 ArgoCD로 트래픽 전달.
- ArgoCD는 평문 HTTP로 화면을 응답.
이 구조는 보안성(SSL) 편의성(DDNS Wildcard) 확장성(Gateway API)을 모두 갖춘 외부 접속 구성입니다. 새 도메인 하나를 더 띄우고 싶을 때 추가로 작업할 것은 ddclient의 도메인 목록 한 줄과 HTTPRoute 한 개뿐입니다.
이 글은 작업 내용을 AI의 도움을 받아 정리했습니다.