기술개발/Nginx

nginx-proxy, LETSENCRYPT 설정 후 인증서 발급 한도 초과 에러 해결

한승욱 2022. 5. 9. 16:09
반응형

배경:

현재 본인은 사이드 프로젝트를 배포하는 과정에서, nginx-proxy와 LETSENCRYPT를 이용해서 테스트 배포 환경을 구성해 놓았다.

내가 원하는 구성은 서브 도메인을 기반으로 각 도커 컨테이너 서비스에 할당을 해주고 연결을 해주는 방법이었다.

간단하게 해당 과정을 설명해보기로 한다.

 

예를 들어 내가 seungwook.com 이라는 도메인을 소유했다는 가정하에, 해당 도메인과 wildcard domain, 즉 *.seungwook.com 의 모든 서브도메인을 내 서버 주소로 A 레코드를 설정해준다.

위 과정을 진행하면 해당 도메인과 그 어떤 서브 도메인을 주소창에 치게 되면 반드시 내 서버로 라우팅되게 된다.

그러면 서버에서는 해당 도메인을 기반으로 예를 들어, api.seungwook.com 으로 요청이 오면 api 컨테이너를, admin.seungwook.com 의 요청이 오면 어드민 서비스 컨테이너와 연결해줄 수 있게 하면 된다.

이러한 기술을 reverse-proxy(리버스 프록시)라고 하는데 이 지점에서 대표적으로 쓰이는 것이 nginx이다.

nginx는 실제 정적 파일을 올리는 웹서버용으로도, 또 프록시 서버로도 굉장히 많이 쓰인다.

(물론 요새는 클라우드 자체의 상품을 쓰는 것이 보다 편한 추세인 것 같다...)

 

위 과정이 성공적으로 진행된다면 각 컨테이너 별로 포트를 외부로 열 필요도, 서버 자체에서 포트포워딩도 해줄 필요도 없다.

모든 요청을 80번 or 443 포트를 할당한 nginx가 받은 뒤, 해당 도메인을 기반으로 연결된 컨테이너가 있다면 자동으로 연결해주기에 각종 서비스를 한 서버에서 돌리기 매우 유리하다.

 

이러한 과정을 하나하나 내가 nginx 파일을 작성하고 설정해도 좋겠지만...

Don't reinvent the wheel 을 기억하자.

이미 천재들?이 미리 만들어 놓은 도커 이미지가 존재했다.

이름하여 nginx-proxy라는 이름의 서비스인데, 간단히 설명하자면 같은 도커 네트워크에 연결된 컨테이너를 훑어보고 그 중 포트가 expose 된 것, VIRTUAL_HOSE 환경 변수를 가진 것을 찾아서 연결해준다.

자세한 설명은 아래 공식 레포를 확인하기 바란다.

예전에는 jwilder/nginx-proxy 였던 것으로 기억하는데 nginx-proxy/nginx-proxy로 레포가 이동된 것 같다.

(dockerhub에서는 둘 다 동일 이미지로 존재함)

 

또한 ssl 설정을 위해 사설 인증서 발급 서비스인 LETSENCRYPT도 자동화해주는 서비스가 존재했다.

자세한 설명은 역시 아래 공식 레포를 확인하기 바란다.

예전에는 jrcs/letsencrypt-nginx-proxy-companion 였던 것으로 기억하는데 nginx-proxy/acme-companion로 레포가 이동된 것 같다.

 

참고:

https://github.com/nginx-proxy/nginx-proxy
https://github.com/nginx-proxy/acme-companion

 


문제:

version: "3.9"
services:
  nginx-proxy:
    container_name: nginx-proxy
    image: jwilder/nginx-proxy
    ports:
      - 80:80
      - 443:443
    volumes:
      - ../nginx/certs:/etc/nginx/certs        
      - ../nginx/log:/var/log/nginx 
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - conf:/etc/nginx/conf.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
    labels:
      - com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy
    restart: always

  letsencrypt-nginx-proxy:
    container_name: letsencrypt-nginx-proxy
    image: jrcs/letsencrypt-nginx-proxy-companion
    depends_on:
      - nginx-proxy
    volumes:
      - ../nginx/certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: always
  
  user-frontend:
    environment:
      - VIRTUAL_HOST=dev.${HOST}
      - LETSENCRYPT_HOST=dev.${HOST}
    expose:
      - 3000

  admin-frontend:
    environment:
      - VIRTUAL_HOST=admin.${HOST}
      - LETSENCRYPT_HOST=admin.${HOST}
    expose:
      - 3000

  express:
    environment:
    - VIRTUAL_HOST=api.${HOST}
    - LETSENCRYPT_HOST=api.${HOST}
    expose:
      - ${PORT}
  
  adminer:
    environment:
    - VIRTUAL_HOST=db.admin.${HOST}
    - LETSENCRYPT_HOST=db.admin.${HOST}
    expose:
      - 8080

volumes:
  conf:
  vhost:
  html:

예전에 작성했었던 경험을 토대로 docker-compose.yml 파일을 위와 같이 작성했다.

각 서비스는 overlay한 docker-compose.yml 에서 따로 정의하고 있기에 왜 image 같은게 정의 되어 있지 않지? 라는 질문은 생략해주시면 감사할 것 같다.

어쨌든 letsencrypt 서비스에서는 기본적으로 CA 인증서 발급을 위해 내 컨테이너 안의 디렉토리에 특정 키파일을 생성하고 인증하는 과정을 거치기에 관련해서 볼륨 마운트가 필요하다.

컨테이너 간 공통적인 볼륨이되 내 로컬에 마운트가 필요어 없는 것은 하단에 따로 volumes 로 빼주어서 용이하게 하였다.

또한 ssl 인증서를 적용할 각 서비스에 LETSENCRYPT_HOST 라는 환경변수를 넣어주었다.

 

이렇게 작성 후 docker-compose up 을 했을 때는 정상적으로 문제 없이 동작을 하였다.

  • nginx-proxy를 기반으로 서브 도메인 기반 reverse-proxy 기능
  • 각 서비스에 ssl 인증서 발급

 

하지만 문제는 며칠 뒤 정도에 일어났다.

어느 순간 부터 ssl 인증서가 제대로 안 먹여지던 것이었다.

에러로그를 살펴보니 아래와 같았다.

nginx-proxy-letsencrypt | [Mon Feb  1 12:30:50 UTC 2021] Create new order error. Le_OrderFinalize not found. {
nginx-proxy-letsencrypt |   "type": "urn:ietf:params:acme:error:rateLimited",
nginx-proxy-letsencrypt |   "detail": "Error creating new order :: too many certificates already issued for exact set of domains: mydomain.com: see https://letsencrypt.org/docs/rate-limits/",
nginx-proxy-letsencrypt |   "status": 429
nginx-proxy-letsencrypt | }
nginx-proxy-letsencrypt | [Mon Feb  1 12:30:50 UTC 2021] Please check log file for more details: /dev/null
nginx-proxy-letsencrypt | Sleep for 3600s

too many certificates...

난 분명이 만료 기한이 다 되지 않는 한 인증서를 매번 발급할 생각이 없었는데...

서비스가 수정되고, 다시 docker-compose down & docker-compose up 을 해주는 과정에서 매번 인증서를 새롭게 발급하던 것이었다.

 

letsencrypt는 인증서 발급에 있어서 rate limit이라는 것이 있다.

동일한 도메인에 대한 인증정보 그룹(domain set)은 일주일에 최대 5개까지 발급이 가능한데 이 제한에 걸려서 이러한 문제가 발생한 것이다.

 

해결 과정:

시도 1) acme.sh 추가

https://github.com/nginx-proxy/acme-companion/issues/216
 
https://github.com/nginx-proxy/acme-companion/discussions/914

https://github.com/nginx-proxy/acme-companion/issues/510

 

공식 레포에서 나와 비슷한 문제를 겪었던 issue를 찾아보면서 해결 과정을 습득했다.

The nginxproxy/acme-companion container requires a volume mounted to /etc/acme.sh for certificate persistance since version 2.0.0.

1차적으로 시도한 부분은 acme.sh을 마운트해주어서 기존에 인증서가 있다면 해당 인증서를 가지고 올 수 있게 설정해주었는데, 이미 한도가 넘어서인지 해당 방법으로는 해결이 안되고 동일한 에러가 발생했다.

 

시도 2) 써드파티 활용

zero ssl이라는 써드파티를 사용해서 해결하는 방법인데, rate limit이 없기에 매우 유용할 것이라고 생각했다.

위 공식 README가 조금은 불친절해 보이기는 하지만 위에서 나온대로 그대로 설정하니 문제가 없었다.

수정한 yaml 파일은 아래와 같다.

version: "3.9"
services:
  nginx-proxy:
    container_name: nginx-proxy
    image: nginxproxy/nginx-proxy
    ports:
      - 80:80
      - 443:443
    volumes:
      - certs:/etc/nginx/certs:ro      
      - ../nginx/log:/var/log/nginx 
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - conf:/etc/nginx/conf.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
    labels:
      - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx-proxy"
    restart: always

  letsencrypt-nginx-proxy:
    container_name: letsencrypt-nginx-proxy
    image: nginxproxy/acme-companion
    depends_on:
      - nginx-proxy
    volumes:
      - certs:/etc/nginx/certs:rw
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: always
    environment:
      - NGINX_PROXY_CONTAINER=nginx-proxy
      - ACME_CA_URI=https://acme.zerossl.com/v2/DV90
      - ACME_EAB_KID="내 EAB_KID"
      - ACME_EAB_HMAC_KEY="내 HMAC_KEY"
  
  user-frontend:
    environment:
      - VIRTUAL_HOST=dev.${HOST}
      - LETSENCRYPT_HOST=dev.${HOST}
    expose:
      - 3000

  admin-frontend:
    environment:
      - VIRTUAL_HOST=admin.${HOST}
      - LETSENCRYPT_HOST=admin.${HOST}
    expose:
      - 3000

  express:
    environment:
    - VIRTUAL_HOST=api.${HOST}
    - LETSENCRYPT_HOST=api.${HOST}
    expose:
      - ${PORT}
  
  adminer:
    environment:
    - VIRTUAL_HOST=db.admin.${HOST}
    - LETSENCRYPT_HOST=db.admin.${HOST}
    expose:
      - 8080

volumes:
  certs:
  conf:
  vhost:
  html:
  acme:

 

사전에 해야할 일이 있다.

https://zerossl.com/

여기서 회원가입을 우선적으로 진행해야한다.

회원가입 및 로그인 후에 https://app.zerossl.com/developer 로 접속해서

1) ZeroSSL API Key

2) EAB Credentials for ACME Clients

둘 중 하나를 활용하면 된다.

본인은 2번을 활용했고, 해당 부분을 README를 참고하여 letsencrypt-nginx-proxy 컨테이너에

- ACME_CA_URI=https://acme.zerossl.com/v2/DV90

- ACME_EAB_KID="내 EAB_KID"

- ACME_EAB_HMAC_KEY="내 HMAC_KEY"

이와 같이 환경변수를 추가해주었다.

 

결과:

내부적으로 매번 재발급을 하는지, 기존에 발급된 것을 가져다 쓰는지는 정확하게 파악이 안되지만, rate limit 문제가 해결되었기에 정상적으로 동작하게 되었다.

 

참고하면 좋은 자료:

 

반응형