Infoteam Blog
소개지원 바로가기

WireGuard로 기숙사에 홈서버 띄우기

안녕하세요, 김도현입니다.
토이 프로젝트를 진행하는 과정에서, 내부망 안에 있는 홈서버를 외부에서 사용해야 하는 상황이 생겼습니다.
이를 해결하며 사용한 WireGuard 가 무엇이고, 어떻게 동작하며, 어떤 점을 조심해야 하는지까지 이야기해보려 합니다.

WireGuard가 필요했던 이유

이 프로젝트에는 무거운 연산을 담당하는 AI 워커가 있습니다.
이 작업은 CPU로 돌리면 수 분씩 걸릴 만큼 무거워서 GPU가 거의 필수인데, 클라우드에서 GPU 인스턴스를 상시 켜두는 건 비용이 만만치 않습니다.
그래서 기숙사에 있는 GPU가 달린 데스크탑을 홈서버로 띄워서 AI 워커를 돌리기로 했습니다.
백엔드(NestJS)는 클라우드에, AI 워커는 데스크탑에 두고, 백엔드가 요청을 이 서버로 보내는 구조를 그렸죠.
[클라우드 백엔드] ---- 작업 요청 ---> [AI 워커(GPU)]
그런데 여기서 문제가 있습니다.
지스트 내부망은 방화벽 뒤에 있어서, 외부에서 들어오는 연결(inbound)을 받을 수 없습니다.
즉 클라우드 백엔드가 내부망의 AI 워커에게 직접 요청을 보낼 방법이 없는 것이죠.
포트포워딩이나 인바운드 개방은 개인이 건드릴 수 있는 영역도 아니고, 내부망 정책상 애초에 허용되지도 않습니다.
따라서 밖으로 나가는(outbound) 연결만으로 두 서버를 하나의 사설 네트워크처럼 묶어주는 방법이 필요했습니다.

WireGuard?

WireGuard는 리눅스 커널에 내장된 모던 VPN 프로토콜입니다.
기존 VPN(OpenVPN, IPsec 등)이 설정이 복잡하고 무거웠던 것과 달리, WireGuard는 다음과 같은 특징을 가집니다.
  • 간결함: 코드베이스가 수천 줄 수준으로 작아 감사(audit)가 쉽고 버그 표면이 작습니다.
  • 빠름: 커널에서 동작하고 최신 암호(Curve25519, ChaCha20 등)를 사용해 성능이 뛰어납니다.
  • 키 기반 인증: 아이디/비밀번호가 아니라 SSH처럼 공개키/개인키 쌍으로 서로를 인증합니다.
핵심은 WireGuard가 클라이언트-서버라는 개념 대신 peer 라는 대등한 단위로 동작한다는 점입니다.
각 peer는 자기만의 키 쌍을 갖고, 상대 peer의 공개키를 등록해 서로 신뢰합니다.

어떻게 동작할까?

WireGuard의 동작은 크게 세 가지 개념으로 이해할 수 있습니다.

키 쌍 (Public / Private Key)

각 peer는 등록 시 무작위로 개인키와 공개키를 만듭니다.
개인키는 자기 기기에만 보관하고, 공개키는 상대 peer에게 알려줍니다.
그러면 각 peer는 자기 개인키 + 상대 공개키를 조합해, 둘만 계산해낼 수 있는 동일한 세션 키를 각자 따로 만들어냅니다. (Diffie–Hellman 키 교환)
실제 데이터는 이 세션 키로 암호화되기 때문에, 비밀번호는 물론 세션 키조차 네트워크로 주고받지 않습니다.
개인키만 새어나가지 않으면 중간에서 패킷을 가로채도 세션 키를 만들 수 없어 내용을 알 수 없습니다.

AllowedIPs (누구의 트래픽을 이 터널로 보낼까)

WireGuard는 각 peer에 대해 AllowedIPs 라는 사설 IP 대역을 지정합니다.
예를 들어 클라우드 백엔드를 10.0.0.1, 데스크탑 AI 워커를 10.0.0.2 로 묶어두면,
백엔드가 10.0.0.2 로 보낸 패킷은 자동으로 WireGuard 터널을 통해 AI 워커에게 전달됩니다.
즉, 물리적으로는 인터넷 너머에 떨어져 있어도 논리적으로는 같은 사설망처럼 다룰 수 있습니다.

Handshake (UDP)

WireGuard는 UDP(기본 51820번 포트) 위에서 동작합니다.
연결을 시작하는 peer가 상대에게 handshake 패킷을 보내면, 서로의 공개키로 세션 키를 교환하고 암호화된 터널이 열립니다.
연결이 성립하고 나면 그 위로는 그냥 평범한 IP 패킷이 오갑니다.

방화벽은 어떻게 넘을까?

여기가 우리 문제의 핵심입니다.
데스크탑 AI 워커는 인바운드는 못 받지만 아웃바운드(밖으로 나가는 연결)는 가능합니다.
그래서 발상을 뒤집습니다.
클라우드 백엔드가 데스크탑 워커에 접속하는 게 아니라, 데스크탑 워커가 공인 IP를 가진 클라우드 쪽으로 먼저 WireGuard 터널을 겁니다.
여기서 "어차피 방화벽이 막는데, 방향만 바꾼다고 뭐가 달라지지?" 라는 의문이 들 수 있습니다.
핵심은 방화벽이 패킷을 '방향'만 보고 막는 게 아니라, 오가는 '연결(connection)'을 통째로 기억하며 판단한다는 점입니다. (이를 stateful, 연결 추적이라고 합니다.)
방화벽의 규칙은 대략 이렇습니다.
  • 밖에서 안으로 새로 시작되는 연결 → 차단
  • 안에서 밖으로 내가 먼저 시작한 연결과, 그에 대한 응답 → 허용
사실 이건 당연한 동작입니다.
우리가 브라우저로 구글에 접속할 때, 요청은 밖으로 나가지만 구글의 응답 패킷은 밖에서 안으로 들어옵니다.
이 응답까지 막아버리면 서비스 사용 자체가 불가능하겠죠.
그래서 방화벽은 "내가 먼저 시작한 연결의 답장"만큼은 안으로 들여보내 줍니다.
전화에 비유하면 이렇습니다.
  • 데스크탑은 밖에서 걸려오는 전화는 못 받습니다. (inbound 차단)
  • 하지만 내가 먼저 전화를 걸면, 그 통화가 연결된 동안에는 양쪽이 자유롭게 대화할 수 있습니다.
WireGuard도 똑같습니다.
데스크탑 워커가 클라우드로 먼저 "전화를 걸어" 터널을 열면, 방화벽은 이 outbound 연결을 허용 목록에 올려두고 되돌아오는 트래픽을 위한 길을 열어 둡니다.
그리고 WireGuard 터널은 한 번 열리면 그 위로 양방향 통신이 되기 때문에, 이제 클라우드 백엔드가 보내는 요청도 이미 열려있는 이 연결의 '답장'인 것처럼 방화벽을 통과해 데스크탑 워커에 도달합니다.
즉, 대화를 먼저 트는 건 안쪽(데스크탑)이지만, 통로가 뚫린 뒤로는 바깥(백엔드)에서도 얼마든지 먼저 말을 걸 수 있게 되는 것이죠.
여기서 중요한 설정이 PersistentKeepalive 입니다.
앞의 전화 비유를 이어가면, 통화가 너무 오래 조용하면 교환기가 "끊어졌나 보다" 하고 회선을 정리하는 것과 같습니다.
방화벽도 일정 시간 트래픽이 없으면 아까 열어둔 연결을 잊어버리고, 그 순간 백엔드가 보낸 요청은 다시 막혀버립니다.
그래서 PersistentKeepalive = 25 처럼 설정해두면 25초마다 작은 패킷을 흘려보내 "나 아직 통화 중이야" 하고 연결을 계속 살려둡니다.
덕분에 백엔드가 언제 요청을 보내도 터널이 살아있어 워커에게 도달합니다.

토이 프로젝트에서는 이렇게 연결했다

전체 그림은 이렇습니다.
  1. 클라우드 백엔드 서버(공인 IP)에 WireGuard peer를 띄우고 10.0.0.1 을 할당합니다.
  1. 데스크탑 AI 워커에 WireGuard peer를 띄우고 10.0.0.2 를 할당한 뒤, Endpoint 를 클라우드 서버로 지정합니다.
  1. 데스크탑 워커가 밖으로 터널을 열고 PersistentKeepalive 로 유지하면, 백엔드는 http://10.0.0.2:8000 처럼 워커를 바로 옆 서버처럼 호출할 수 있습니다.
데스크탑 워커 쪽 설정은 대략 이런 모습입니다.
[Interface] PrivateKey = <데스크탑 워커 개인키> Address = 10.0.0.2/24 [Peer] PublicKey = <클라우드 백엔드 공개키> Endpoint = example.com:51820 # 공인 IP를 가진 클라우드 AllowedIPs = 10.0.0.1/32 PersistentKeepalive = 25 # 방화벽 구멍 유지
sequenceDiagram participant API as 클라우드 백엔드 (10.0.0.1) participant WG as WireGuard 터널 participant AI as 데스크탑 AI 워커 (10.0.0.2) AI ->> WG: outbound handshake (UDP 51820) WG ->> API: 터널 수립 Note over API,AI: 이후 양방향 사설망처럼 동작 (PersistentKeepalive로 유지) API ->> WG: http://10.0.0.2:8000 로 작업 요청 WG ->> AI: 터널 통해 전달 AI ->> AI: AI 모델 실행 AI ->> WG: 결과 반환 WG ->> API: 백엔드로 전달
방화벽 입장에서는 데스크탑 워커가 밖으로 연 평범한 UDP 연결 하나로만 보입니다.
하지만 그 위에서는 두 서버가 마치 같은 사설망에 있는 것처럼 자유롭게 통신하고 있는 것이죠.
WireGuard는 처음부터 이런 용도로 설계된 정식 VPN이라 빠르고 안정적이며 암호화까지 보장됩니다.

간단하게 시도해보기: Tailscale

여기까지 직접 해보면, 키를 만들고 config를 짜고 공인 IP를 가진 클라우드 서버까지 마련하는 게 은근히 번거롭습니다.
이 과정을 간단하게 처리해주는 도구가 Tailscale입니다.
Tailscale은 WireGuard를 그대로 엔진으로 쓰면서, 귀찮은 부분을 대신 맡아줍니다.
  • 키 교환·관리: 계정으로 로그인만 하면 각 기기의 키 등록과 공유를 알아서 해줍니다.
  • NAT 통과: 중계 서버(DERP)와 NAT 홀펀칭을 자동으로 시도해, 공인 IP 서버를 직접 두지 않아도 방화벽 뒤 기기끼리 연결됩니다.
  • 주소 할당: 각 기기에 고정 사설 IP를 자동으로 부여해, 위에서 손으로 정한 10.0.0.x 를 신경 쓸 필요가 없습니다.
즉, "데스크탑에 Tailscale 깔고, 클라우드에도 깔고, 같은 계정으로 로그인" 만으로 앞에서 한 일이 거의 끝납니다.
하지만 편해진 만큼, 원리를 알고 나면 마냥 안심할 수만은 없는 지점도 있습니다.
Tailscale은 두 기기를 직접(UDP 홀펀칭) 연결하려 하지만, 직접 연결이 안 되면 DERP라는 Tailscale의 중계 서버를 거쳐 트래픽을 흘려보냅니다.
이때도 패킷은 두 기기의 키로 끝단 암호화(E2E)되어 있어 중계 서버가 내용을 열어보진 못하지만, 그렇다고 "완전히 안전하다"고 단정하긴 어렵습니다.
결국 Tailscale을 쓴다는 건 내 사설망의 키 배포·접근 제어·연결 중개를 남의 서비스에 맡긴다는 뜻입니다.
그 중앙 서버나 계정이 뚫리거나, 연결 메타데이터가 노출되거나, 서비스 정책·장애에 내 네트워크가 휘둘리는 등 - 꼭 위에서 말한 경로가 아니더라도 내가 미처 생각 못 한 곳에서 위험이 생길 수 있습니다.
그래서 다루는 데이터가 민감하고 통제권을 완전히 쥐고 싶다면, 앞서처럼 WireGuard를 직접 구성하는 것을 고려할 수 있습니다.

그래도 조심할 것

WireGuard는 편리하지만, 결국 이 터널은 내부망 안으로 들어오는 문을 하나 뚫는 행위이기도 합니다.
  • 개인키 관리: 개인키가 유출되면 그 peer 행세를 하며 내부망에 접근할 수 있습니다. 키 파일 권한(600)과 보관에 주의해야 합니다.
  • AllowedIPs 최소화: 필요한 IP/포트만 열어야 합니다. 넓게 열면 터널이 내부망 전체로의 통로가 될 수 있습니다.
내가 편하려고 뚫은 터널은 관리가 소홀하면 그대로 공격자의 통로가 될 수 있습니다.
그래서 VPN 하나를 띄우더라도 키·접근 범위 등을 확인해야 합니다.

인포팀에서 함께 일하고 싶다면?

지원 바로가기