포트포워딩 차단 후 Home Assistant 원격 접속 복구: Cloudflare Tunnel과 Tailscale 서브넷 라우터 구성
· home-assistant, cloudflare, tailscale
배경
- 홈서버(Ubuntu, Docker)에서 Nginx Proxy Manager(NPM)를 운영했다.
- NPM에 HA 항목(
http://<HA-내부IP>:8123, Let’s Encrypt 인증서, Public)이 등록돼 있었다. - 연이은 보안 이슈로 공유기 포트포워딩(80/443)을 전부 차단했다.
- 포트 차단으로 HA 외부 접속이 끊겼다.
- HA는 같은 서버 위 KVM VM(
haos, HAOS)으로 실행 중이며, 호스트에 VM과 통신하기 위한macvlan-ha인터페이스가 있다. - 평소 외부에서는 공유기 WireGuard VPN으로 접속하고, SSH 앱(JuiceSSH)으로 서버들을 IP로 연결해 사용했다.
결론부터
- 터널과 Tailscale 중 무엇이냐: 두 방식 모두 포트포워딩 없이 동작한다. 터널은 서버에서 Cloudflare로 나가는 연결만 만든다. Tailscale은 양쪽이 NAT를 통과해 직접 연결한다.
- 먼저 한 것: 이미 cloudflared 컨테이너가 실행 중이었다. 대시보드의 호스트네임 규칙(
<HA-도메인>→http://<HA-내부IP>:8123)만 맞추면 HA 외부 접속을 복구했다. - 최종 방향: 모든 서비스를 혼자 쓰므로 공개할 필요가 없다. HA, Nextcloud 등은 VPN(WireGuard/Tailscale)으로만 접속하고, 터널은 공개가 꼭 필요한 서비스에만 사용한다.
- Tailscale을 서브넷 라우터로 구성: 서버 한 대가
<LAN 대역>/24를 광고하면 폰에서 Tailscale만 켜도 기존 LAN IP 그대로 HA와 다른 서버에 접속된다. JuiceSSH 설정을 바꿀 필요가 없다. - 버린 선택지: 모든 서비스를 터널로 공개하는 방식. 터널은 앱 취약점을 막아 주지 못하고, Cloudflare 무료 플랜 경유 시 요청당 약 100MB 제한(Nextcloud 클라이언트는 청크 업로드를 쓰므로 영향은 달라질 수 있음, 미확인)과 Cloudflare Access를 적용했을 때 동기화 클라이언트가 차단되는 문제가 있다.
1. 터널 상태 확인과 토큰 교체
docker-compose.yml에cloudflared-tunnel컨테이너가 정의돼 있었다. 토큰 방식(--token)이라 터널 설정은 compose가 아니라 Cloudflare 대시보드에서 관리한다.- 토큰이 compose 파일 안에 평문으로 포함돼 있었고, 대화에도 노출되었다. 누구나 해당 터널을 실행할 수 있으므로 대시보드에서 토큰을 교체(roll)하고
.env파일로 분리했다. - 새 토큰은 대시보드 → 터널 상세 → “복제본 추가”에 표시되는 설치 명령에서 확인했다(명령은 실행하지 않고 토큰 문자열만 복사).
- 컨테이너를 다시 올릴 때 이름 충돌이 발생했다.
Error response from daemon: Conflict. The container name "/cloudflared-tunnel" is already in use by container "<컨테이너ID>".
docker compose down으로는 해당 컨테이너가 삭제되지 않았다(다른 프로젝트로 만든 것으로 추정).docker rm -f <컨테이너ID>후docker compose up -d로 해결했다.- 이후 대시보드에는 활성 복제본이 2개로 보였다. 하나는 3일 된 옛 연결이고, 하나는 방금 뜬 새 연결이었다.
docker ps -a에는 cloudflared 컨테이너가 하나뿐이라 옛 복제본은 대시보드에 남은 이전 연결로 판단했다. 이후 사라졌는지는 미확인.
2. trusted_proxies 걱정과 실제 상태
- 처음에는 HA에
trusted_proxies를 추가해야 400 오류를 피한다고 안내받았다. HAOS가 VM이라 어디에서 수정하는지가 문제였다(File editor 애드온 또는 SSH 애드온으로/config/configuration.yaml편집,ha core check→ha core restart). - 확인 결과 이미 아래와 같이 설정돼 있었다. 수정할 필요가 없었다.
http:
use_x_forwarded_for: true
trusted_proxies:
- <LAN 대역>/24
- 172.16.0.0/12
- 127.0.0.1
- cloudflared 컨테이너(도커 브리지)가 호스트를 거쳐 HA에 연결되면 출발지가 호스트의
macvlan-ha주소로 보인다(ip route get <HA-내부IP>로 확인). 이 대역은 이미 허용 범위 안이다. - HA 설정에 문제는 없었고, 접속 불가는 다른 원인에서 비롯된 것이었다.
3. 접속 불가의 진단
서버에서 구간별로 확인했다.
| 구간 | 결과 |
|---|---|
https://<HA-도메인> (Cloudflare 경유) |
200 |
호스트 → <HA-내부IP>:8123 |
200 |
| cloudflared 컨테이너 네트워크 → HA | 200 |
| 터널 로그의 에러/경고 | 없음 |
- 터널 로그를 보면 토큰 교체 전후 몇 분 사이에 인그레스 설정이 여러 번 갱신되었다. 중간에 규칙이 없어서 404만 응답했으며, 이후
<HA-도메인>→http://<HA-내부IP>:8123규칙이 다시 들어갔다. 이전에 있던 블로그용 규칙(http://<도커-게이트웨이>:8088)는 현재 없다. - 폰에서 통신사망으로 접속하면 정상이었다. 내부망에서만 안 되던 것은 크롬 DNS 캐시(예전 공인 IP)가 원인일 가능성이 있다. 데스크톱 크롬을 새로고침하면 접속된 점과 일치한다. 확정은 못 했다(미확인).
- 내부 DNS(AdGuard Home)에 해당 도메인 리라이트가 있는지는 미확인.
4. 어떻게 공개/비공개를 나눌까
- 터널의 한계: 포트를 열지 않고 집 IP를 숨기고 DDoS를 차단하지만, 공개한 앱의 취약점은 그대로 남는다.
- 서비스별 정리: 혼자 쓰는 것(HA, Nextcloud, NPM 관리 UI)은 VPN 전용, 남과 공유하거나 공개 링크가 필요한 것은 터널(+가능하면 Cloudflare Access)으로 제공한다.
- NPM과 Let’s Encrypt: 터널로 공개하는 서비스에는 인증서가 필요 없다(Cloudflare가 처리). 내부/VPN에서 도메인 + HTTPS로 사용하려면 NPM이 여전히 유용하다. 이때 80/443을 열지 않으려면 HTTP 챌린지 대신 Cloudflare DNS 챌린지로 인증서를 발급해야 한다.
- HA를
http://<IP>:8123으로 사용하면 불편했던 이유: 폰 컴패니언 앱의 일부 센서·권한, 브라우저의 마이크·알림·클립보드·PWA 등이 HTTPS에서만 동작한다. 내부 도메인 + DNS 챌린지 인증서가 해결책이다. 이 부분은 아직 구성하지 않았다(새 세션에서 진행 예정).
5. WireGuard와 Tailscale, 서브넷 라우터
- 공유기 WireGuard는 외부에서 접속하려면 UDP 포트 하나가 열려 있어야 한다. 올바른 키로 서명된 패킷이 아니면 응답하지 않으므로 웹 서비스를 여는 것보다 노출되는 공격면이 훨씬 작다.
- Tailscale은 포트포워딩이 필요 없다. 서버와 폰이 모두 바깥쪽으로 먼저 연결해 NAT를 통과한다.
- 평소 WireGuard가 잘 쓰이고 있으므로 교체는 필수는 아니다. Tailscale은 WireGuard가 막히는 환경(CGNAT, 이중 NAT 등)의 예비로도 유용하다.
- 서브넷 라우터: Tailscale이 설치된 기기 한 대가 LAN 대역 전체로 가는 문 역할을 한다. 설치하지 않은 기기(HA VM, 다른 서버)도 Tailscale만 켜면 LAN IP로 접속한다. JuiceSSH의 기존 IP 설정도 그대로 사용할 수 있다.
- 이 서버는 Tailscale이 이미 연결돼 있었고(
tailscale status에 기기 목록 확인), IPv4 포워딩도 켜져 있었다(net.ipv4.ip_forward = 1).
sudo tailscale set --advertise-routes=<LAN 대역>/24
헷갈렸던 점들
1. 터널로 “접속 불가”인데 모든 구간이 정상
- 증상: 내부망에서는 도메인이 열리지 않았지만, 서버에서 curl하면 모두 200.
- 원인: 인그레스 설정이 갱신되는 짧은 시간의 공백과 브라우저 DNS 캐시로 추정(확정 못 함).
- 해결: 통신사망(모바일 데이터)에서 접속해 터널이 정상임을 확인했다.
2. 서브넷 라우터를 켰는데 폰에서 HA에 접속 안 됨
- 증상: 폰(Tailscale, 통신사망)은 서버와
direct로 연결되고 응답도 46 ms였지만http://<HA-내부IP>:8123은 열리지 않았다. 폰 앱의 “Use Tailscale subnets”는 켜져 있었다. - 점검: 서버 쪽은 모두 정상(
ts-forward규칙·마스커레이드 존재, HA 응답 0.5 ms, ufw 비활성).tailscale status --json에서PrimaryRoutes가None이고 허용 IP에 LAN 대역이 없었다. - 원인: 관리 콘솔의
Subnets라벨은 경로를 광고만 해도 붙는다. 승인은⋯→ Edit route settings에서 체크박스를 켜야 한다. 라벨에!표시가 있었고 “1 route is advertised but not approved”라는 안내가 있었다. - 해결: 체크박스를 켜서 승인했으며, 이후 폰(통신사망)에서 HA VM(
<HA-내부IP>)에 접속되었다.
정리
- 포트포워딩을 차단해도 터널(나가는 연결)이나 Tailscale(NAT 통과)로 원격 접속을 유지할 수 있다.
- 터널은 정말 필요한 서비스만 공개하고, 혼자 쓰는 서비스는 VPN 전용이 가장 안전하다.
- 접속 불가를 진단할 때는 구간별(외부 도메인 → 호스트 → 컨테이너 → 서비스)로 상태 코드를 확인하면 원인 파악이 쉽다. 같은 도메인도 네트워크마다 결과가 다를 수 있으니 통신사망에서 반드시 재확인한다.
- Tailscale 서브넷 라우터는 광고 후 관리 콘솔에서 승인까지 해야 동작한다. 라벨의 경고 표시가 신호다.
- 토큰은 compose에 평문으로 두지 말고
.env로 분리하고, 노출되었으면 교체한다. - 남은 작업: NPM의 HA 항목 정리, 터널에서 HA 호스트네임과 DNS 레코드 삭제, 내부 도메인 + DNS 챌린지 인증서로 HA HTTPS 구성(새 세션).