Caddy와 도커를 사용하여 제대로 reverse proxy하는 방법
도커 컨테이너 안에서 어떻게 Caddy를 제대로 설정하는지에 관한 튜토리얼입니다.
준비사항
- 도커 서비스가 활성화되어 있는 서버. 이 서버 (호스트네임) 이름은
timmy
라고 하겠습니다. - 인터넷에 노출시키고 싶은 서비스들
- 이 서비스들을 담고 있는 커스텀 도커 네트워크 (여기에서
proxynetwork
이란 이름을 갖고 있는 것으로 하겠습니다) - (선택) 인터넷에 노출시키고 싶지 않은 서비스들
- (선택) 위의 서비스들에 접속하는데 사용할 VPN 서비스 (예로 Wireguard)
왜 reverse proxy를 사용하죠?
왜냐하면 service.timmy.example.com
를 치는 것이 192.168.1.200:4343
또는 timmy:4343
를 치는 것보다 훨씬 낫기 때문입니다.
이는 접속할 서비스가 2개나 3개보다 더 많을 때 더욱 그렇습니다. 포트 번호들을 다 기억하는 것은 힘들거든요!
1부 - 외부 레이어
일단, Caddy를 host
네트워크 옵션을 가지고 설치합니다. (왜 그런지는 나중에 설명할게요.) 그런 다음, 설정에서 필요에 따라 도메인들을 매핑합니다:
# 로깅 설정
(logs) {
log {
output file /data/logs/access.log
}
}
# 검색 엔진들에게 서비스들을 숨기기
(no_robots_txt) {
respond /robots.txt 200 {
body "User-agent: *
Disallow: /
"
close
}
}
# 메인 페이지
timmy.example.com {
# 서비스에 reverse proxy를 하거나...
reverse_proxy 192.168.1.200:8080
# 다음과 같이 응답할 수 있습니다 (다음 줄을 코멘트 해제하세요)
#respond "Hello World!"
import no_robots_txt
import logs
}
# 다른 서비스들
*.timmy.example.com {
import logs
@plex host plex.timmy.example.com
handle @plex {
# `172.18.0.1`가 도커 호스트 IP입니다
reverse_proxy 172.18.0.1:32400
import no_robots_txt
}
}
꽤 일반적인 것들입니다. 컨테이너가 실행되면, Caddy가 Let’s Encrypt를 사용하여 SSL 인증서를 만드는 것과 같은 일들을 수행하는데, 여기에서 timmy.example.com
가 서버의 공개 IP로 설정되어 있다고 하고, *.timmy.example.com
는 timmy.example.com
에 가르키는 CNAME이여야 합니다.
만약 이게 원했던 것의 전부라면 여기에서 멈추셔도 좋습니다! 하지만 만약 VPN을 통해 접속하고 싶었던 내부 서비스들이 있다면 어떻게 해야 할까요? 그럼 더 진행하겠습니다.
2부 - 내부 서비스들
이 부분이 꽤 복잡한 부분입니다. 왜냐하면 VPN에 연결된 사람들만이 내부 서비스에 접속할 수 있도록 제한하고 싶기 때문이죠.
그럼 내부 Caddy 서버부터 먼저 설정하겠습니다. 다음과 같은 Caddyfile
을 만듭니다:
# 로깅 설정
(logs) {
log {
output file /data/logs/access.log
}
}
:80 {
@main host timmy.private.example.com
handle @main {
reverse_proxy foo:8080
}
@jenkins host jenkins.timmy.private.example.com
handle @jenkins {
reverse_proxy jenkins:8080
}
}
이렇게 설정하려면, timmy.private.example.com
는 VPN 네트워크 안의 비공개 IP를 가리켜야 하고, *.timmy.private.example.com
는 timmy.private.example.com
의 CNAME이여야 합니다. 이들을 공개 DNS에 올려도 괜찮은데, 내부 주소라 어차피 외부에서 접속할 수 없기 때문입니다.
이제 여기에서 들 수 있는 의문이, 왜 서버 블록에 timmy.private.example.com
또는 *.timmy.private.example.com
대신 :80
을 사용하는지 궁금할 수도 있습니다. 이는 왜냐하면 이 Caddy 서버는 내부 Caddy 서버로써 다른 Caddy 서버에서 reverse-proxy를 받는 서버이기에, 이 내부 Caddy 서버가 자동으로 SSL 인증서를 만드는 등의 일을 하지 않길 원하기 때문입니다. 오히려, 이런 reverse proxy 셋업에서 여러 레이어의 HTTPS를 설정해두는 것은 주로 나쁜 아이디어™이기에, 기본 HTTP 포트인 80에서 듣는 이유입니다.
그리고, 여기에서 서비스들을 지칭할 때 jenkins
와 같이 호스트네임을 직접 사용하는 것을 눈치채셨을 수도 있습니다. 이는 위에서 말했던 커스텀 도커 네트워크가 사용되었을 때 작동하기 때문에, 서비스(예를 들어, Jenkins)와 내부 Caddy 서버 모두 같은 네트워크 상에 있어야 합니다! 이 경우에는, 내부 Caddy 서버를 proxynetwork
에 만든 후, 포트 80을 호스트 머신의 680 포트로 전달하겠습니다.
이제 해야 할 부분은 두 서버들을 연결하는 것입니다!
3부 - 두 서버 연결하기
외부 Caddy 서버에서 다음을 Caddyfile
에 추가합니다:
# 내부 Caddy로 reverse-proxy
timmy.private.example.com {
import logs
reverse_proxy 172.18.0.1:680
}
*.timmy.private.example.com {
import logs
reverse_proxy 172.18.0.1:680
}
이제 내부 서비스들에 접속할 수 있습니다. 완성…?
아직은 아닙니다!
별로 안전하지 않기에…
들 수 있는 생각이, timmy.private.example.com
가 VPN 네트워크 안 비공개 IP 주소로 가르키기에, 인터넷 상에서 이 서비스들에 접속할 수 없다고 생각하실 수도 있습니다.
하지만 host
키워드가 보이시나요? Caddy는 클라이언트가 무슨 호스트에 방문하는지 확인한 다음 적절한 블록으로 전달합니다. 그럼 Caddy가 확인하는 부분이 뭘까요? 바로 모든 HTTP/S 요청에 들어가는 Host
헤더입니다.
문제가 보이시나요? HTTP/S 요청들은 클라이언트들이 만드는데, 내용물을 마음대로 정할 수 있기에 문제가 됩니다.
간단한 시험을 해보세요: VPN 네트워크에서 연결 해제한 후, 운영체제의 hosts
파일을 연 후 timmy.private.example.com
를 서버의 공개 IP로 가리키는 줄을 추가하세요. 그런 다음 timmy.private.example.com
에 방문합니다. 무엇이 보이시나요?
이 체크가 hosts
파일과 같이 간단한 방법으로 우회 가능하기에, 안전하지 않고 조치가 필요합니다.
4부 - 외부 레이어의 보안 강화
다시 외부 Caddy 서버 레이어에서, 다음을 추가합니다:
(block_nonvpn) {
@notVPN not remote_ip 10.10.0.0/24
handle @notVPN {
respond "접근 금지" 403
}
}
이때 10.10.0.0/24
를 VPN 네트워크의 실제 IP 레인지로 치환해야 합니다.
여기에서 공개 IP가 비공개 서버 블록에 접근을 시도하는 것을 알아챘을때 곧바로 연결을 끊어버리는 방법을 사용하고 싶으실 수도 있습니다:
(block_nonvpn) {
@notVPN not remote_ip 10.10.0.0/24
handle @notVPN {
abort
}
}
하지만, 이 방법은 권장되지 않는데, fail2ban
등을 설정하여 연결 기록에서 403 응답이 많은 공격자들을 찾고, 네트워크에서 차단하여 로드를 감소시킬 수 있기 때문입니다.
물론, 이게 동작하려면 외부 Caddy 레이어가 host
네트워크 모드여야 합니다. 제가 진행한 몇몇 실험에서, Caddy를 다른 네트워크 모드로 설정하게 된다면 모든 클라이언트들이 172.18.0.1
에서 오는 것으로 인식해버리게 되는데, 이는 도커 머신 호스트를 가리키는, 도커 네크워크 레이어의 내부 IP 주소입니다.
그런 다음, 이 스니펫을 비공개 서비스들에 다음과 같이 추가합니다:
# 내부 Caddy로 reverse-proxy
timmy.private.example.com {
import logs
import block_nonvpn
reverse_proxy 172.18.0.1:680
}
*.timmy.private.example.com {
import logs
import block_nonvpn
reverse_proxy 172.18.0.1:680
}
이제 다시 한번 hosts
파일 시험을 해보면, 더 이상 작동하지 않는 것을 확인하실 수 있습니다!
결론
도커 서비스들에 reverse proxy하기 위해 Caddy를 설정하는데 도움이 되었으면 합니다!