우연히 샵백에서 노드VPN이 100% 환급 프로모션을 진행한다는 것을 알았다.
예전부터 VPN 서비스를 한 번 사용해보고 싶었기에, 구매를 하고 방치하다가 K3S 클러스터를 다 구축하면서 Pod로 올려보면 좋겠다는 생각이 들었다.
Docker를 이용해 올려보자
서비스를 올리기 위해 여러가지 글을 찾아보다 Gluetun으로 OpenVPN을 사용하면 나중에 NordVPN에서 타 서비스로 변경되도 계속 사용할 수 있어서 이 방식으로 올리기로 했다.
그래서 위 레시피를 따라했는데 AUTH_FAILED 문제로 연결이 잘 안되는 이슈가 있었다..
INFO [openvpn] SIGUSR1[soft,auth-failure] received, process restarting
INFO [openvpn] TCP/UDP: Preserving recently used remote address: [AF_INET]138.199.21.121:1194
INFO [openvpn] UDPv4 link local: (not bound)
INFO [openvpn] UDPv4 link remote: [AF_INET]138.199.21.121:1194
INFO [openvpn] [jp680.nordvpn.com] Peer Connection Initiated with [AF_INET]138.199.21.121:1194
ERROR [openvpn] AUTH: Received control message: AUTH_FAILED
Your credentials might be wrong 🤨
뭐 자격증명이 틀릴 일은 없다 생각했고,
DNS_KEEP_NAMESERVER: "off" # on으로 설정하면 외부 DNS 서버를 사용하지 않고 내부 DNS 모듈을 사용합니다. 보안 이슈 있음
DNS_SERVER: "on" # 여기서 'on'으로 설정하여 내부 DNS 모듈을 활성화합니다.
DNS_OVER_TLS: "off"
이런 연결 설정 문제인가 싶어 설정을 변경하면서 테스트를 하다보니 어느 순간 VPN 연결이 성공했다.
Docker로 테스트를 성공해서 이제 Pod로 올리기 위해 Yaml을 열심히 작성하고 Pod를 올렸는데 또 동일하게 AUTH_FAILED 이 발생하였다.
혹시 몰라 Docker를 다시 올리니 또 연결이 안되서 Up Down을 여러번 반복하다 보니
jp680 → jp542 → jp803
로 서버가 변경되다 어느 순간 연결이 된다는 점을 캐치했다.
연결성공 🙂
INFO [ip getter] Public IP address is 156.146.35.100 (Japan, Tokyo, Tokyo - source: ipinfo+ifconfig.co+ip2location+cloudflare)
INFO [dns] leak check report: 162.158.117.217 (20%), 172.71.4.42 (20%), 162.159.111.43 (10%), 172.64.214.43 (10%), 172.68.41.43 (10%), 172.68.41.44 (10%), 172.70.221.58 (10%), 172.70.221.59 (10%)
왜 인증 실패 ?

사진을 보면 동일한 로그를 출력하다 어느 서버에 연결하는지에 따라 그 이후 메시지가 달라진다.
확실하진 않지만 저 sg632 서버가 불안정 하거나 NordVPN 서버랑 통신을 못해서 인증처리를 하지 못 하는게 아닐까 싶다.
그리고 종종
INFO [firewall] allowing VPN connection...
WARN [vpn] assuming IPv6 is not supported, cannot get OpenVPN link by name: link not found: for name tun0
INFO [openvpn] OpenVPN 2.6.20 x86_64-alpine-linux-musl [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD]
INFO [openvpn] library versions: OpenSSL 3.5.7 9 Jun 2026, LZO 2.10
INFO [openvpn] TCP/UDP: Preserving recently used remote address: [AF_INET]192.166.246.132:1194
INFO [openvpn] UDPv4 link local: (not bound)
INFO [openvpn] UDPv4 link remote: [AF_INET]192.166.246.132:1194
WARN [openvpn] TLS Error: TLS key negotiation failed to occur within 20 seconds (check your network connectivity)
🚒🚒🚒🚒🚒🚨🚨🚨🚨🚨🚨🚒🚒🚒🚒🚒
That error usually happens because either:
1. The VPN server IP address you are trying to connect to is no longer valid 🔌
Check out https://github.com/qdm12/gluetun-wiki/blob/main/setup/servers.md#update-the-vpn-servers-list
2. The VPN server crashed 💥, try changing your VPN servers filtering options such as SERVER_REGIONS
3. Your Internet connection is not working 🤯, ensure it works
4. Something else ➡️ https://github.com/qdm12/gluetun/issues/new/choose
INFO [openvpn] TLS Error: TLS handshake failed
INFO [openvpn] SIGTERM received, sending exit notification to peer
INFO [openvpn] SIGTERM[soft,tls-error] received, process exiting
INFO [vpn] retrying in 15s
위 로그가 나올때도 있다.
The VPN server IP address you are trying to connect to is no longer valid
서버가 유효하지 않기 때문에 연결이 안 되는 거라 다른 서버에 연결하면 연결이 된다.
그러니까 정리하면 실패가 두 종류다.
| 증상 | 서버가 | 정체 |
|---|---|---|
AUTH_FAILED | 응답은 한다 (Peer Connection Initiated까진 뜨는데 인증만 거부) | 목록에 살아있는 서버가 맛이 간 경우 |
TLS key negotiation failed | 아예 응답이 없다 | 목록에서 사라진 유령 서버 |
위의 TLS 에러 로그에 찍힌 192.166.246.132를 NordVPN 현행 목록에서 찾아봤는데 없는 IP였다.
반면 AUTH_FAILED를 낸 서버들(jp680, sg608, sg559)은 전부 현행 목록에 online으로 멀쩡히 있었다.
즉 gluetun 안내문의 두 항목이 각각 따로 놀고 있었던 거다.
- 서버 IP가 유효하지 않음 →
TLS 실패 - 서버가 맛이 감 →
AUTH_FAILED
그래서 해결책은 Pod 재생성을 해야한다
둘 다 "다른 서버로 바꾸면 되는" 문제인데, 문제는 gluetun이 서버를 안 바꿔준다는 거다.
INFO [openvpn] SIGUSR1[soft,auth-failure] received, process restarting
INFO [openvpn] TCP/UDP: Preserving recently used remote address: [AF_INET]138.199.21.121:1194
Preserving recently used remote address
- 직전에 쓰던 서버 주소를 그대로 물고 간다.
- 소프트 재시작이라 gluetun의 서버 선택 로직을 다시 타지 않는다.
- 결국 죽은 서버 하나를 잡으면 프로세스가 아무리 재시작해도 영원히 거기서 못 벗어난다.
서버를 새로 뽑게 하려면 컨테이너 프로세스가 아니라 Pod 자체가 다시 떠야 한다.
- 그래서 Docker Up / Down 하다보면 가끔씩 성공하는...
그래서 사이드카를 붙였다
매번 손으로 내렸다 올릴 순 없으니 자동화를 해야했다.
사이드카 컨테이너에서 Gluetun 로그를 보면서 AUTH_FAILED 카운팅을 하려고 했다.
AUTH_FAIL_COUNT=0
MAX_FAILURES=10
while true; do
CURRENT_LOGS=$(kubectl logs $POD_NAME -c gluetun -n $NAMESPACE --tail=100 2>/dev/null || echo "")
AUTH_FAIL_LINES=$(echo "$CURRENT_LOGS" | grep "AUTH_FAILED" || true)
...
gluetun 로그를 직접 긁어서 AUTH_FAILED를 세고, 10번 넘으면 Pod를 지우는 auth-monitor 사이드카를 만들었다.
그런데 AUTH_FAILED 단어를 제대로 캐치 못 하는지 Pod 재시작을 안 하길래.
국가 체크 + Slack 알림 기반의 vpn-monitor로 갈아탔다.
생각해보면 국가 체크가 특정 시간 동안 안 된다는 것 자체가 VPN 연결을 못 했다는 것이니, 로그를 문자열로 긁는 것보다 이게 더 확실한 신호라고 봤다.
2025-12-27 11:20 | [Proxy] 인증 10번 실패시 pod 삭제 ← 만들고
2025-12-27 12:46 | [Proxy] auth check 제거 ← 지우고
2025-12-27 13:19 | [Proxy] 국가체크 및 슬랙알림전송 ← 갈아타고
2026-01-03 10:51 | [Proxy] vpn 연결 끊기면 pod 삭제 로직 추가 ← 완성
이제 VPN이 죽으면 알아서 Pod가 재생성되도록 수정을 했다.
그리고 6개월간 아무런 문제가 없었다.
...고 믿었다.
6개월 뒤, 크롤링이 몇 개씩 실패하기 시작했다
1월 17일에 사용량 증설 겸 안정성 겸 연결 국가를 japan/singapore 2개로 늘렸다. 그리고 6개월을 잊고 살았다.
7월 16일, 크롤링이 몇 개씩 실패하길래 뭐지 싶어 봤더니 VPN이 하나만 살아있었다.
싱가포르 쪽이 AUTH_FAILED로 죽어 있었는데,
죽은 Pod에도 트래픽이 그대로 가서 ProxyError가 나고 있었던 거다.
라우팅이야 어차피 Service가 알아서 할 테니,
죽은 쪽으로 트래픽을 안 넘기게만 하면 되겠다 싶어 readinessProbe를 추가했다.
- 이때까지만 해도 사이드카가 알아서 Pod를 교체하겠지라 생각했다.
- 내가 원한건 연결이 죽고 다시 붙는 그 사이에 해당 서비스로 요청만 안 가면 된다고 생각을 했다.
readinessProbe:
exec:
command:
- /bin/sh
- -c
- "curl -x http://localhost:8888 -k -s --max-time 10 ${HEALTH_CHECK_URL} | grep -q '\"status\":\"ok\"'"
periodSeconds: 10
failureThreshold: 2
프록시로 실제로 외부에 나갈 수 있을 때만 Ready로 판정해서, VPN이 끊긴 Pod는 엔드포인트에서 자동으로 빠지게 했다.
배포후 35시간 뒤, 둘 다 죽었다
readinessProbe를 배포하면 Pod가 새로 뜬다. 그런데 그 Pod가 하필 죽은 서버를 뽑았다.
readinessProbe 배포 2026-07-16 00:48:08
japan Pod 생성 2026-07-16 00:49:09 ← 61초 뒤
고치겠다고 배포한 지 61초 만에 태어난 Pod가 jp680에 갇혔다. 그리고 35시간을 거기서 보냈다.
그동안 아무 소리도 안 났다. readinessProbe가 제 일을 너무 잘했기 때문이다.
NAME READY STATUS RESTARTS AGE
gluetun-japan-58474b9fd5-j56h4 1/2 CrashLoopBackOff 191 (3m50s ago) 35h
gluetun-singapore-7846d4fb44-fxzpg 2/2 Running 0 35h
새벽에 도는 크롤링은 아무런 이슈가 없었다.
- 죽은 Pod를 엔드포인트에서 빼버리니 트래픽은 멀쩡한 싱가포르로만 갔고, 크롤링도 잘 돌았고, 서비스는 정상으로 보였다.
그러다 인프라를 좀 옮길 일이 있어서 master, worker 몇 대를 drain했다.
proxy Pod도 같이 다시 떴는데, 일본, 싱가포르 둘 다 죽은 서버를 뽑아버렸다.
이제 크롤링이 몇 개씩 실패하는 수준이 아니라 아예 안 돌았다. 그걸 몇 시간을 반복했다.
readinessProbe는 살아있는 쪽이 하나라도 있을 때나 의미가 있지, 양쪽이 동시에 죽으면 그냥 프록시가 없는 거니까.
정작 vpn-monitor가 보내야 할 Slack 알림은 한 통도 안 왔다.
알림이 안 오길래 문제가 없는 줄 알았는데, 알고 보니 알림이 죽어 있었던 거다.
서비스 알림이 오고나서야 이슈를 파악했다.
파헤쳐보니
사이드카 로그가 매번 딱 두 줄이었다.
Waiting for gluetun proxy to be ready...
ERROR: Gluetun proxy is not available after 30 attempts
- 사이드카 스크립트
감시 루프에 들어가기 전에 프록시가 뜰 때까지 60초를 기다리는데,
- VPN이 안 붙었으니 프록시로 나가는 curl이 계속 실패하고
exit 1로 죽는다.
VPN이 안 붙었다는 이유로 VPN 감시자가 죽는 웃픈 상황이다.
Pod를 지워야 할 코드가 6개월간 한 번도 실행된 적이 없었다.
신호를 잘못 골랐다
exit 1은 증상이고, 그 위에 더 근본적인 게 있다. 애초에 어떤 신호를 보고 판단할지를 잘못 골랐다.
국가 체크는 프록시로 나가서 내 IP가 어디로 찍히는지 보는 거다. 그러니까 VPN이 붙어야만 볼 수 있는 신호다. 반면 AUTH_FAILED는 VPN이 안 붙어도 로그에 찍힌다. 안 붙었을 때 보이는 유일한 신호였던 거다.
더 포괄적인 신호로 갈아탄다고 생각했는데, 하필 최악의 상황에서 눈이 먼 신호를 고른 셈이다.
고치고 나서
고친 건 간단하다. 초기 대기 실패 시 exit 1 대신 Pod를 지우게 했다.
if [ $ELAPSED -ge $STARTUP_DEADLINE ]; then
restart_pod "VPN 터널이 ${ELAPSED}초 동안 연결되지 않았습니다" # ← 죽는 대신 Pod를 지운다
fi
6개월 전에 이미 알아낸 결론(재생성만이 탈출구)을 이제야 실제로 실행하는 셈이다.
근데 고치고 나니 그 아래에 숨어있던 두 번째 버그가 드러났다. 6개월간 안 보였던 건 거기까지 도달한 적이 없어서였다.
229 02:09 curl -X POST ... https://hooks.slack.com/... ← 2분 넘게 매달려 있다
Pod를 지우기 전에 Slack 알림을 보내는데, 방화벽이 패킷을 조용히 버리니(REJECT가 아니라 DROP) 응답이 안 온다.
배포하자마자 이 로직이 그대로 검증됐다.
japan은 새 서버를 뽑아 바로 살아났는데, 싱가포르는 죽은 서버만 연달아 뽑았다.
03:17~03:51 sg608 → sg559 → sg633 → ... 약 10회 재생성 (~34분)
03:51 정상 서버 연결, 이후 재시작 0으로 안정
Pod가 ~193초마다 재생성되며 서버를 다시 뽑았고, 34분 만에 자력으로 복구됐다.
japan 156.146.35.100 (Japan, Tokyo) 2/2 restarts=0
singapore 86.48.11.178 (Singapore) 2/2 restarts=0
리전과 무관하게 죽은 서버를 뽑으면 걸리고, 다시 뽑으면 풀린다.
[별첨] 혹시 서버 목록이 오래되서 그런가?
gluetun 안내문에 "서버 IP가 더 이상 유효하지 않을 수 있다"는 항목이 있길래 확인해봤다.
japan : 목록 284개 중 118개가 이미 폐기된 서버 (42%)
singapore : 목록 87개 중 65개가 이미 폐기된 서버 (75%)
그럼 이게 범인일까? NordVPN 현행 목록과 하나하나 대조해봤다.
| 서버 | 현행 목록 | 실제 결과 |
|---|---|---|
| sg608 | ✅ 존재, online, load=2 | AUTH_FAILED (오늘 헛뽑음) |
| jp680 | ✅ 존재, online, load=2 | AUTH_FAILED 6094번 |
목록에 멀쩡히 살아있는 서버가 6094번을 거부했다. 오늘 헛뽑은 sg608, sg559, sg633도 전부 현행 목록에 online으로 존재한다. 적어도 AUTH_FAILED는 목록과 상관이 없었다.
또 앞에서 봤던 TLS key negotiation failed 로그의 192.166.246.132는 현행 목록에 없는 IP였다.
그러니까 목록이 오래된건 TLS 실패의 원인이 맞다. 싱가포르 목록의 75%가 유효하지 않아서, 정상 서버를 뽑는 데 34분이나 걸린 것도 이걸로 설명이 된다.
정리하면 목록 갱신은 절반짜리 처방이다. 유령 서버를 뽑아 TLS가 터지는 건 막아주지만, jp680처럼 멀쩡히 살아있는 서버가 인증을 거부하는 건 못 막는다.
그래서 결론은 Pod를 재생성해서 새로운 서버랑 연결이 되도록 해야한다.
미제
gluetun은 원래 킬스위치 컨테이너라 VPN 터널 밖 아웃바운드를 전부 막는 게 기본값이다. 그리고 사이드카는 gluetun과 네트워크 네임스페이스를 공유한다. 그러니까 Slack행 패킷도 같이 막힌다. Slack이 VPN을 탈 필요가 없는 것과 무관하게 말이다.
즉 VPN이 죽으면 Slack 알림은 구조적으로 못 나간다. 방화벽을 열 수도 없고(그게 gluetun을 쓰는 이유니까). 결국 이런 알림은 Pod 안이 아니라 밖에서(Prometheus 같은 데서) 봐야 한다는 결론인데, 이건 다음 숙제로 남겨뒀다.
그리고 일본/싱가포르가 둘 다 죽으면 지금도 답이 없다. 이번처럼 drain 한 번에 둘 다 죽은 서버를 뽑아버리면, 이젠 재생성으로 알아서 복구되긴 하지만 그동안 크롤링은 못 돈다. 그럼 프록시 실패 시 어떻게 할 건지 방어 로직을 이걸 쓰는 모든 서비스에 제각각 구현해야 하잖아? 그건 좀 아닌 것 같은데, 아직 답을 못 찾았다.
마치며
이번 건은 원인을 몰라서 생긴 문제가 아니었다. 6개월 전에 이미 원인을 정확히 알고 있었고, 심지어 그걸 고치는 코드까지 만들어뒀다. 다만 그게 도는지를 제대로 안 봤을 뿐이다.
