이전 이야기

조금 오래된 이야기긴 하지만, Cloudflare의 든든한 방화벽을 거치지 않고 원본 IP로 멋대로 들어온 불청객을 쫓아내는 방안에 대해서 정리한 적이 있었습니다. 이번 글에서는 하는 일은 비슷하지만 좀 더 공식적인 수단에 가까운 방법으로 대응하는 방법을 알아봅니다.

클라이언트 인증서를 활용한 확실한 접속 루트 관리

인증서를 설치하고 어쩌구저쩌구…… 그러한 작업은 사실 서버의 이야기에 한정된 것처럼 느껴집니다.

애초에 인증서로 우리가 서버를 인증하는 이유는, example.com 같은 곳이 과연 우리가 아는 바로 그곳이 맞는지, 알기 위해서 우리가 클라이언트 입장에서 서버를 시험해보는 것입니다. 손톱 먹은 들쥐 이야기에서 등장한 것처럼, 들쥐(해커)가 진짜(사이트) 행세를 할지도 모르니까 우리끼리만 알아볼 수 있는 암호를 교환함으로써 진짜 사이트를 판별해내는데 목적이 있는 것입니다.

하지만 정반대로 클라이언트도 인증서를 가질 수 있고, 서버가 클라이언트를 인증할 수도 있어요! 서버가 부정한 클라이언트를 골라내기 위해 접속자를 시험할 수 있다니…… 발상이 반대라서 놀랍지만, 보안이 중요한 곳이라면 당연히 필요한 기능임에 틀림이 없지요.

그리고 이 기술은 Cloudflare 같은 WAF(웹 애플리케이션 방화벽) 서비스에서 빛을 발하게 됩니다. 어차피 접속자는 Cloudflare를 통해서 들어오는 것을 전제로 하고 있으니, 이곳을 거치지 않은 예기치 않은 접속을 차단해내는 방법으로 인증서를 쓰는 건 매우 바람직해요.

설정하기

Cloudflare 대시보드에서 사이트 설정 → SSL/TLS → 원본 서버(Origin Server) → 인증된 원본 끌어오기(Authenticated Origin Pulls) 설정을 켜줍니다.

참고로 Cloudflare 사이트가 최근에 한국어를 지원하게 리뉴얼 되어 매우 편리해졌습니다.

인증된 원본 끌어오기 옵션

이 옵션을 켜고 바로 아래쪽 Help를 눌러보면 매우 간결한 설명이 되어 있어요. 윗쪽을 건너뛰고 아랫쪽 설명으로 이동합니다.

origin-pull-ca.pem 다운로드 링크

다운로드 링크가 보이는데 이 주소를 복사하여 curl 같은 방법으로 서버에서 직접 다운로드 받습니다. /etc/nginx/의 적당한 디렉토리가 좋겠어요. cloudflare.crt로 이름을 바꿔줍시다.

$ cd /etc/nginx/

$ mkdir certs

$ cd certs

$ sudo -i

# curl https://support.cloudflare.com/hc/en-us/article_attachments/360044928032/origin-pull-ca.pem -o cloudflare.crt

이제 nginx config에 들어가서 server {} 절에 아래 같은 스크립트를 복사합니다.

ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
ssl_verify_client on;

이렇게 넣으면 설정 파일은 대략 이런 구성이 됩니다.

http {

   server {
      location {
         ...
      }

      ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
      ssl_verify_client on;
   }

}

옵션 값에 대한 상세 설명

Cloudflare에서 저 옵션을 켜주면, 수 분 이내로 모든 요청에 고유의 인증서를 달아서 서버에 요청을 보내게 됩니다. 만약 Cloudflare를 거치지 않고 우회하거나 본 IP로 직접 접속한다면, 이런 인증서 없이 보내게 됩니다.

클라이언트의 특정 인증서가 같이 전송되는지 확인하고, Nginx는 인증된 사용자 여부를 가려내게 됩니다. 그리고 그 다음 행동은 위에 ssl_verify_client가 핵심이 됩니다.

여기 옵션은 4가지 중 하나를 가질 수 있습니다.

  • on: ssl_client_certificate로 지정한 클라이언트 인증서가 없는 클라이언트를 모두 거절합니다.
  • off: 클라이언트 인증서 판별을 끄고 모두 허용합니다.
  • optional: 인증서 확인은 합니다. 하지만 거절하지는 않고, 변수 $ssl_client_verify로서 갖고만 있습니다.
  • optional_no_ca: 인증서 확인을 하되 유효한지 nginx가 따지지 않습니다. 변수 $ssl_client_cert를 외부 인증서 판별 시스템으로 보내서 유효한지 확인할 수 있습니다.

$ssl_client_verify

우리는 on 옵션을 주어서 인증서가 올바르지 않으면 모두 거절하는 방식을 택했지만, optional의 경우 보다 다양하게 활용할 수 있습니다. 이 값은 다음과 같습니다.

  • SUCCESS: 클라이언트 인증서가 유효합니다.
  • FAILED:reason: 클라이언트 인증서가 존재하지만, 유효성 검증에 실패했고, 그 이유가 나와 있습니다.
  • NONE: 요청에 클라이언트 인증서가 없습니다.

이렇게 되면 이 요청이 Cloudflare에서 온 요청인지 판별하여 아니면 무조건 거절하는게 아니라, 특정 조건에만 location 절에서 허용하거나 거절할 수 있습니다.

어떻게 되나요?

Cloudflare를 경유하여 올바르게 요청이 들어오면

무사히 결과물이 나타나고, 평소처럼 페이지를 볼 수 있습니다. HTTP 200 OK!

Cloudflare를 우회하여 실제 IP로 요청이 들어오면

클라이언트 인증서를 확인할 수 없기 때문에, Nginx는 HTTP 400 BAD REQUEST 응답을 하게 됩니다. 잘못된 요청!

주의사항

WordPress는 wp-cron.php 같은 스크립트가 상시 작동합니다. 서버 스스로에게 리퀘스트를 보내서 루프백 IP(가령 127.0.0.1)로 요청을 보내기도 합니다.

이 때 위에 추가한 옵션 탓에 서버 자기자신의 요청을 거절할 수 있습니다. 이렇게 되면 워드프레스가 자동 업데이트를 수행하거나 API REST 요청을 자기 자신에게 보내지 못하게 되고, 관리자 메뉴의 일부 기능이 작동하지 않게 됩니다.

이 현상의 해결을 위해서는 ssl_verify_client 값을 optional로 지정하고, location 절에서 워드프레스에 필요한 부분만 제외하는 방법도 고려해봄직 하지만….. 조금 다른 방법이 있을 것 같아서 좀 더 연구해보려고 합니다.

2년차 개발자 이진백입니다. 현재 일본에서 웹 프로그래머로 근무중입니다. 주로 Java, Vue (Nuxt), TypeScript 언어를 활용하고 있습니다. 취미로 C#과 Windows Universal Store App을 오래 만져왔고, 개인 서버 운영에도 관심이 많습니다. 최신 이슈를 알기 쉽게 전달해드리도록, 더욱 더 노력하겠습니다.