Caddy에서 국가별 IP 접근통제 구현 (리버스 프록시)

Caddy는 리버스 프록시다. Nginx의 대안으로 Go 언어로 개발되었으며 별도 설정없이 Caddy의 백엔드에 연결된 웹사이트의 SSL 인증서를 자동으로 관리해주기도 한다. 또한 설정 파일인 Caddyfile을 통해 다양한 보안적인 요소를 제공하기도 한다. 그래서 필자가 구축해 사용하고 있는 블로그 방문자 로그 분석 웹사이트, 비밀번호 관리를 위한 Vaultwarden, 여러 커뮤니티 게시판의 글을 모아보는 웹사이트, 메모 사이트 등 10여개의 웹사이트가 Caddy의 백엔드에 연결되어 있다.

문제는 보안이다.

이 모든 사이트들이 Caddy를 통해 인터넷 어디에서든 접근 가능하다 보니 매우 다양한 해킹 시도가 발생한다. 그래서 Caddy에서 최소한의 보안 설정과 IP 접근통제를 구현하기로 했다. 이 때 IP 접근통제를 위해 사용되는 Caddy의 플러그인이 있었다. 바로 Maxmind다. 깃허브 계정 porech에 공개되어 있는 Maxmind Geo IP 기반 접근통제를 구현 가능하게 해주는 플러그인이다.

필자는 Caddy를 Portainer에서 도커 컨테이너로 배포하여 사용하고 있는데 Caddy의 설정파일인 Caddyfile을 웹에서 편집하고 로딩할 수 있는 Caddy-GUI 컨테이너를 코딩하여 Caddy Manager로 배포하고 있다. 다음 글을 참고하자.


Maxmind 플러그인을 포함하여 Caddy 명령어를 빌드하는 Dockerfile 작성

먼저 caddy-maxmind-geolocation 플러그인을 포함하는 Caddy 명령어를 빌드하는 Dockerfile을 작성해야 한다.

Maxmind 플러그인을 포함하는 커스텀 Caddy 명령어 빌드
Maxmind 플러그인을 포함하는 커스텀 Caddy 명령어 빌드

4행에서 Caddy의 공식 이미지 중 플러그인을 빌드하여 caddy에 포함할 수 있는 개발도구(go, xcaddy 등)가 설치된 Caddy 이미지를 builder 라는 이름으로 지정하여 사용한다. 그리고 9행의 Run 단계에서 builder 이미지 내 xcaddy 명령을 실행하여 github에 공개되어 있는 maxmind 플러그인 소스를 가져다가 mixmind가 포함된 커스텀 caddy 명령어를 빌드한다.

13행에서 다시 caddy의 공식이미지(개발도구 미포함)를 사용하며

18행에서 builder 이미지에서 커스텀 caddy 명령어를 공식 caddy 이미지에 덮어쓴다.

이 Dockerfile을 작성하여 caddy-maxmind 폴더에 저장한다.

커스텀 caddy 이미지를 빌드하는 Github Action 파일 작성

이제 앞에서 작성한 커스텀 Caddy 이미지를 실제로 빌드하고 Github의 Registry (GHCR)에 Push하는 Action을 다음과 같이 작성해야 한다. 이 파일은 .github\workflows 폴더에 저장해야 한다. 이 폴더는 Github의 Action 폴더에 동일하게 Push되고 Github의 Actions 메뉴에 가면 워프플로(Workflow)에 워크플로 이름으로 등록되어 있는 것을 확인할 수 있다.

Maxmind 플러그인 포함된 커스텀 캐디 이미지 빌드 및 GHCR에 푸시하기
Maxmind 플러그인 포함된 커스텀 캐디 이미지 빌드 및 GHCR에 푸시하기

Action에서 실행할 수 있는 워크플로 파일인 docker-caddy-maxmind.yml 파일의 전체 코드는 다음과 같다. 주석문을 달아두었으니 참고하면 쉽게 이해할 수 있다.

# 워크플로의 이름
name: Build Caddy-MaxMind Image

# 이 액션은 수동으로 실행함을 정의
# 사용자가 직접 "Run workflow"를 눌러 실행함
on:
  workflow_dispatch:

# GitHub 패키지(GHCR : GitHub Container Registry)에 이미지를 업로드할 수 
# 있는 쓰기 권한을 부여
permissions:
  packages: write

jobs:
  build:
    # 이미지의 OS는 ubuntu 최신 버전
    runs-on: ubuntu-latest

    steps:
    # 현재 리포지토리에 있는 소스 코드(Dockerfile 등)를 가상 환경으로 가져옴
    - name: Checkout
      uses: actions/checkout@v3
  
    # 빌드를 지원하기 위한 가상화 에뮬레이터를 설치
    - name: Set up QEMU
      uses: docker/setup-qemu-action@v2

    # 멀티 플랫폼 빌드 등 확장된 기능을 제공하는 Docker Buildx 도구를 설정
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    # caddy-maxmind/Dockerfile를 사용해 Caddy 커스텀 이미지를 빌드한 다음 
    # GHCR에 Push하기 위한 GHCR에 로그인 정보 설정
    - name: Log in to GitHub Container Registry
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.repository_owner }}   # Github 리포지토리 오너의 계정명
        password: ${{ secrets.GITHUB_TOKEN }}     # Github PAT 토큰. 리포지토리 관련 권한이 설정되어야 함

    # 리포지토리 이름에 대문자가 섞여 있으면 빌드 에러가 날 수 있어 
    # 소문자로 변환해 저장
    - name: Set lower case repository name
      run: echo "REPO_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV

    # 실제 빌드 및 푸시 단계
    - name: Build and push caddy-maxmind image
      uses: docker/build-push-action@v4
      with:
        # caddy maxmind 플러그인이 포함된 커스텀 Caddy 이미지 빌드를 위한
        # Dockerfile 경로와 빌드할 플랫폼과 Push여부, tag 설정
        context: ./caddy-maxmind
        file: ./caddy-maxmind/Dockerfile
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ghcr.io/taeho9/caddymanager/caddy-maxmind:latest

코드를 보면 “실제 빌드 및 푸시단계”에서 앞에서 작성한 커스텀 Caddy 이미지를 빌드하는 Dockerfile인 ./caddy-maxmind/Dockerfile 을 사용해 커스텀 Caddy 이미지를 빌드하는 것을 알 수 있다.


커스텀 Caddy 이미지를 빌드하는 워크플로 실행

이 코드들을 작성 후 Github에 Push하고 main 브랜치에 머지한 다음 리포지토리의 Actions 메뉴에 가면 다음과 같이 등록된 워크플로가 보이게 된다.

깃허브 액션에 등록된 워크플로
깃허브 액션에 등록된 워크플로

앞의 docker-caddy-maxmind.yml 파일에서 지정한 워크플로의 이름인 “Build Caddy-MaxMind Image” 이 워크플로에 보인다. 이 워크플로를 선택하면 오른쪽 창에 “Run workflow” 등 워크플로를 실행할 수 있는 버튼이 보인다. 이 버튼을 누르면 워크플로에서 정의된 대로 Maxmind 플러그인을 포함하는 커스텀 Caddy 이미지를 빌드하여 GHCR에 패키지로 푸시하게 된다.

그런데 이 시간은 괘 오래걸릴 수 있다. 특히 ARM CPU용 이미지를 생성할 경우 멈춘건가??? 싶을만큼 오래 걸린다.

이제 다음은 빌드된 커스텀 Caddy 이미지를 포테이너를 통해 배포할 수 있도록 docker-compose.yml 파일을 작성한다.

커스텀 Caddy 이미지를 배포하는 docker-compose.yml
커스텀 Caddy 이미지를 배포하는 docker-compose.yml

필자의 경우 앞의 포스트에서 작성했던 docker-compose.yml을 수정해주었다. 6행의 caddy 이미지 빌드에 사용되는 이미지의 주소가 GHCR로 변경되어 있음을 알 수 있다. Actions에서 실행한 워크플로에서 빌드하고 푸시한 바로 그 커스텀 Caddy 이미지다.

아래의 caddy-gui 도 GHCR에 푸시되어 있고 그 이미지를 가져와 배포한다.

정상적으로 배포되면 다음과 같이 Caddyfile에 Geo IP 기반의 정책으로 한국(KR)에서만 접근할 수 있도록 통제할 수 있다.

Caddyfile에 Geo IP 기반 접근통제 적용하기

Geo IP 기반의 통제를 위해서는 주기적으로 국가별 IP 주소를 담고 있는 DB파일을 다운로드 받아 호스트의 /data/caddy/data 폴더에 업로드 해야 한다.

Maxmind 홈페이지에 무료 계정을 만들고 GeoLite2-Country.mmdb 파일을 다운로드 받으면 된다. 그 다음에 Caddyfile을 다음과 같이 수정해준다. 아래쪽에 GeoIP 기반 차단로직 처럼 한국(KR)만 접속을 허용하도록 설정하면 된다.

(app_common) {
    encode gzip
    header {
        Strict-Transport-Security "max-age=31536000;"

        # HTTPS 강제 (HSTS) - SEO에 긍정적
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

        # User-Agent의 문자열 포함여부로 차단 (정규식 활용 - 대소문자 무시(?i))
        @bad_bots header_regexp User-Agent (?i)(sqlmap|burp|nikto|nmap|python-requests|go-http)
        abort @bad_bots "Access Denied" 403

        # 클릭재킹 방지 - 영향 없음
        X-Frame-Options "SAMEORIGIN"

        # MIME 스니핑 방지 - 영향 없음
        X-Content-Type-Options "nosniff"

        # Referrer 정책 - 분석 도구(GA 등) 사용 시 필요
        # 'strict-origin-when-cross-origin'은 보안과 데이터 분석 사이의 균형이 좋습니다.
        Referrer-Policy "strict-origin-when-cross-origin"

        # CSP (예시: 구글 서비스 허용 설정)
        # 실제 환경에 맞춰 조정이 필요하지만, 기본적으로 self와 google 관련 도메인을 허용합니다.
        Content-Security-Policy "
           default-src 'self';
           script-src 'self' 'unsafe-inline'
              https://www.google-analytics.com
              https://pagead2.googlesyndication.com 
              https://adservice.google.com 
              https://tpc.googlesyndication.com;
           style-src 'self' 'unsafe-inline'
              https://fonts.googleapis.com;
           img-src 'self' 
              https://pagead2.googlesyndication.com 
              https://*.doubleclick.net;
        "
    }

    # GeoIP 기반 차단로직 시작
    @geoblock {
       not maxmind_geolocation {
           # 컨테이너 내부 경로 기준 (호스트의 /data/caddy/data/GeoLite2-Country.mmdb)
           db_path "/data/GeoLite2-Country.mmdb"
           allow_countries KR
       }
    }
    abort @geoblock
}
# 백엔드의 서비스 주소
caddys.********.pe.kr {
    route {
       # 앞의 공통코드 Import
       import app_common
       basic_auth {
           taeho $2a$14$AKZtgrIZ***********8jyi0UBfrLwlZ6J6 
       }
       reverse_proxy caddy-gui:80
    }
}
wplog.********.pe.kr {
    route {
       # 앞의 공통코드 Import
       import app_common
       reverse_proxy wordpress-log-nginx:80
    }
}

#caddy #reverse-proxy #maxmind #geo-ip

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다