RunFridge's Blog

라즈베리파이 + WireGuard로 집 IP 숨기고 홈랩 만들기

개요

작은 웹 서비스 몇 개를 띄워두려고 EC2 인스턴스(t3.small, 2GB RAM)를 항상 켜두고 있었다. 트래픽은 거의 없는데 매달 14달러 남짓한 요금은 꼬박꼬박 나갔다.

집에 24시간 켜져 있는 작은 장비 하나면 충분하지 않을까? 조건은 세 가지였다.

  • 항상 켜져 있을 것: 노트북처럼 덮으면 꺼지는 장비는 안 된다.
  • 전기를 적게 먹을 것: EC2 요금 아끼려다 전기 요금이 더 나오면 곤란하다.
  • 집 IP를 공개하지 않을 것: 집 공인 IP가 DNS에 박히는 건 영 찜찜하다.

세 번째 조건 때문에 꽤 돌아갔다. 이 글은 그 시행착오의 기록이다.

장비

  • 보드: Raspberry Pi 4 Model B (4GB RAM)
  • 저장소: 64GB microSD
  • OS: Alpine Linux (aarch64)

Raspberry Pi 4 Model B 4GB RAM 박스. 붉은 상자에 보드의 흰색 선화가 그려져 있다.

OS는 Alpine을 골랐다. 부팅 직후 메모리를 수십 MB밖에 안 쓰고, apk로 패키지 관리가 단순하고, 서비스 관리도 OpenRC라 손에 익었다. 4GB면 nginx와 웹 서비스 몇 개, 데이터베이스 하나 정도는 넉넉하게 돌아간다.

$ apk add wireguard-tools nginx
$ rc-update add nginx default

1차 시도: 공유기 포트포워딩

가장 먼저 떠올린 건 정석대로 하는 방법이다. Pi에 nginx를 올리고, LG U+ 기본 공유기에서 80, 443 포트를 Pi로 포워딩하고, Route53에 집 공인 IP를 A 레코드로 등록한다.

1차 시도: 인터넷에서 공유기 포트포워딩을 거쳐 라즈베리파이 nginx로 바로 들어오는 구성. 집 공인 IP가 DNS에 그대로 노출된다.

설정 자체는 10분이면 끝난다. 그런데 dig runfridge.dev만 쳐보면 우리 집 공인 IP가 그대로 나온다. 누구든 그 IP로 포트 스캔을 걸 수 있고, 공유기 펌웨어의 보안은 내가 통제할 수 있는 영역이 아니다.

집 IP를 공개하지 않겠다는 세 번째 조건에 정면으로 걸린다. 바로 접었다.

2차 시도: Pi를 WireGuard 서버로

그렇다면 집으로 들어오는 문을 하나로 줄여보자. Pi에 WireGuard 서버를 올리고, 폰과 노트북, 그리고 아주 작은 EC2 인스턴스(t4g.nano)를 피어로 붙였다. 인터넷에서 오는 요청은 EC2의 nginx가 받아서 터널을 통해 Pi로 넘긴다.

2차 시도: 라즈베리파이가 WireGuard 서버, 폰과 노트북과 EC2가 피어. EC2 nginx가 터널로 라즈베리파이에 요청을 넘기고, crontab이 집 IP 변경을 감지해 Route53을 갱신한다.

문제는 피어들이 Pi를 찾아가려면 집 공인 IP를 알아야 한다는 점이다. 그런데 ISP 정책상 집 IP는 언제든 바뀔 수 있다. 그래서 crontab으로 ifconfig.me에 공인 IP를 물어보고, 캐시해둔 값과 다르면 Route53의 vpn.runfridge.dev A 레코드를 갱신하는 DDNS 스크립트를 돌렸다.

#!/bin/sh
# ~/bin/ddns.sh : 공인 IP가 바뀌면 Route53 A 레코드 갱신
CACHE=/var/cache/ddns.ip
CURRENT=$(curl -s https://ifconfig.me)

# 바뀐 게 없으면 아무것도 하지 않는다
[ "$CURRENT" = "$(cat "$CACHE" 2>/dev/null)" ] && exit 0

aws route53 change-resource-record-sets \
  --hosted-zone-id ZONE_ID \
  --change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{
    \"Name\":\"vpn.runfridge.dev\",\"Type\":\"A\",\"TTL\":60,
    \"ResourceRecords\":[{\"Value\":\"$CURRENT\"}]}}]}"

echo "$CURRENT" > "$CACHE"
# 5분마다 확인
*/5 * * * * ~/bin/ddns.sh

돌아가긴 했다. 그런데 두 가지가 마음에 안 들었다.

  • 귀찮다: IP가 바뀌는 순간과 cron이 도는 순간 사이에는 터널이 끊긴다. Pi에 AWS 자격 증명도 놓아야 한다.
  • 여전히 노출된다: dig vpn.runfridge.dev를 치면 집 IP가 나온다. 문을 하나로 줄였을 뿐, 주소는 그대로 공개된 셈이다.

결국 1차 시도와 같은 문제를 조금 더 복잡하게 만든 것에 불과했다 😂

최종: EC2를 WireGuard 서버로

여기서 방향을 뒤집었다. 집이 서버가 아니라 클라이언트가 되면 된다.

WireGuard 서버를 EC2(t4g.nano)에 올리고, Pi는 피어로서 EC2에 접속만 한다. 폰과 노트북도 마찬가지로 EC2에 붙는다. 이렇게 하면:

  • Pi는 아웃바운드 UDP 연결만 하므로 집 공유기에 포트포워딩이 필요 없다.
  • 집 IP가 바뀌어도 Pi가 알아서 다시 붙는다. DDNS도, cron도, Pi 위의 AWS 자격 증명도 필요 없다.
  • DNS에는 EC2의 IP만 올라간다.

최종 구성: 인터넷 요청은 EC2 nginx로만 들어오고, EC2는 WireGuard 서버로서 라즈베리파이와 폰, 노트북을 피어로 받는다. nginx가 터널 안 주소로 라즈베리파이 서비스에 요청을 넘긴다. 집 IP는 DNS 어디에도 없다.

Route53

서브도메인별로 레코드를 만드는 대신 와일드카드 하나로 끝냈다. 새 서비스를 붙일 때 DNS는 건드릴 일이 없다.

*.runfridge.dev.   A   <EC2 공인 IP>   # wg.runfridge.dev 같은 이름도 전부 EC2로 간다

WireGuard

EC2 쪽이 서버다. 터널 대역은 10.0.0.0/24, EC2가 10.0.0.1, Pi가 10.0.0.2이다.

# EC2: /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <EC2 개인키>

[Peer]
# Raspberry Pi
PublicKey = <Pi 공개키>
AllowedIPs = 10.0.0.2/32

[Peer]
# 폰, 노트북도 같은 식으로 추가
PublicKey = <폰 공개키>
AllowedIPs = 10.0.0.3/32
# Pi: /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.2/24
PrivateKey = <Pi 개인키>

[Peer]
PublicKey = <EC2 공개키>
Endpoint = wg.runfridge.dev:51820
AllowedIPs = 10.0.0.0/24
# NAT 뒤에 있으니 주기적으로 신호를 보내 터널을 유지한다
PersistentKeepalive = 25

⚠️ 주의: PersistentKeepalive를 빼먹으면 공유기가 NAT 테이블을 정리하는 순간 EC2에서 Pi로 가는 길이 막힌다.

nginx

EC2의 nginx는 그냥 멍청한 리버스 프록시다. 서브도메인을 보고 터널 안 Pi의 포트로 넘길 뿐이다.

# EC2: /etc/nginx/conf.d/app.conf
server {
    listen 443 ssl;
    server_name app.runfridge.dev;
    # ssl_certificate ... (certbot)

    location / {
        proxy_pass http://10.0.0.2:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

서비스를 하나 더 붙이고 싶으면 server 블록 하나와 Pi의 포트 하나면 끝이다.

노출면 줄이기

EC2가 유일하게 밖에 드러난 장비이니 여기만 잘 잠그면 된다.

  • 인바운드: 80, 443 (nginx), 51820/UDP (WireGuard). 그 외 전부 차단.
  • SSH 비활성화: 22번 포트는 열지 않는다. 접속은 AWS Systems Manager Session Manager로만 한다. 키 파일 관리도, 무차별 대입 로그도 사라진다.
# 로컬에서 EC2 셸 열기 (SSH 없이)
$ aws ssm start-session --target i-0123456789abcdef0

정리하면 요청의 흐름은 이렇다.

인터넷 → EC2 nginx (443) → WireGuard 터널 (UDP) → Pi (10.0.0.2:포트)

이제 집 IP는 DNS에도, 설정 파일에도, 어디에도 없다.

마치며

세 번의 시도에서 배운 것을 정리하면 이렇다.

  • 터널 방향을 뒤집으면 동적 IP 문제가 사라진다. 집이 클라이언트가 되는 순간 DDNS는 필요 없어진다.
  • 노출면은 하나로 좁히는 게 편하다. 공유기, Pi, DNS를 각각 걱정하는 대신 EC2 하나만 잠그면 된다.
  • t4g.nano면 충분하다. 패킷을 넘기기만 하는 nginx와 WireGuard에는 그 이상이 필요 없다. 무거운 건 전부 Pi가 한다. 예약 인스턴스(Reserved Instance)로 돌리니 S3, VPC, KMS 같은 부수 비용까지 다 합쳐도 AWS 청구서가 월 4달러를 넘지 않는다. t3.small 시절의 14달러에서 3분의 1 이하로 줄었다.

집에 안 쓰는 라즈베리파이가 굴러다닌다면, 작은 EC2 하나와 짝지어 홈랩을 만들어보자!