약 5분

서버가 뚫렸을 때 SSL 인증서 개인키 교체와 폐기 순서

이 글의 순서 6개 절
  1. 키가 나갔다고 볼 때
  2. 순서
  3. certbot으로 받은 인증서
  4. 유료 인증서
  5. CT 로그와 CAA
  6. 참조

원격 코드 실행 취약점은 패치로 막는다. 하지만 패치 전에 이미 공격자 코드가 돌았다면 패치는 그다음 일만 막는다. 그 서버에 있던 인증서 개인키는 따로 처리해야 한다. 키가 나간 인증서는 만료일까지 유효하다. 키를 가진 쪽은 DNS를 바꿔치거나 통신 경로에 끼어들 수 있을 때 우리 서버 행세를 한다.

키가 나갔다고 볼 때

침해 흔적이 있고 다음 중 하나에 해당하면 키를 바꾼다.

  • 공격자 코드가 키를 들고 있는 프로세스 안에서 돌았다. Apache mod_php가 그렇다. PHP가 Apache 워커 안에서 돌고, 그 워커는 TLS 핸드셰이크를 하려고 키를 메모리에 들고 있다. Node가 직접 HTTPS를 받는 구성도 같다.
  • 공격자가 얻은 권한으로 키 파일을 읽을 수 있었다.
  • root 권한까지 갔는지 확인할 수 없다.

두 번째는 웹 사용자로 직접 열어 보면 안다. 키 내용은 출력하지 않는다.

sudo -u www-data head -c0 /etc/letsencrypt/live/example.com/privkey.pem \
  && echo "읽힌다" || echo "못 읽는다"

Debian과 Ubuntu는 www-data, RHEL 계열은 apache나 nginx로 바꿔 넣는다. certbot은 개인키를 root만 읽게 만든다. 키를 다른 곳에 복사해 두었거나 권한을 풀어 두었다면 그 파일로 시험한다.

와일드카드 인증서처럼 같은 키를 여러 서버에 깔았다면 한 대만 뚫려도 전부 바꿔야 한다. 로드밸런서와 CDN 원본에 올린 사본도 포함이다.

순서

  1. 새 키로 새 인증서를 받는다.
  2. 서버에 걸고 밖에서 새 인증서가 나오는지 본다.
  3. 이전 인증서를 폐기한다. 사유는 keyCompromise다.
  4. 침해 기간에 모르는 인증서가 나갔는지 CT 로그에서 찾는다.

폐기를 먼저 하면 폐기 정보를 확인하는 클라이언트는 새 인증서가 걸릴 때까지 접속에 실패한다. 반대로 폐기를 미루면 나간 키가 그만큼 더 유효하다. 새 인증서를 걸고 확인한 직후에 폐기한다.

certbot으로 받은 인증서

certbot은 갱신할 때마다 새 키를 만드는 것이 기본이다. 키를 재사용하도록 설정했는지 먼저 본다.

sudo grep -H reuse_key /etc/letsencrypt/renewal/*.conf

아무것도 안 나오면 강제 갱신만으로 새 키가 나온다. reuse_key = True가 보이면 --new-key(certbot 1.26.0 이상)를 붙인다.

sudo certbot renew --cert-name example.com --force-renewal   # reuse_key면 --new-key 추가
sudo systemctl reload nginx

밖에서 보이는 인증서의 일련번호가 새 파일과 같은지 맞춰 본다.

sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -serial
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -serial

이전 인증서와 키는 archive 폴더에 번호가 하나 작은 파일로 남아 있다. live의 링크가 cert4.pem을 가리키면 폐기할 것은 cert3.pem과 privkey3.pem이다.

sudo ls -l /etc/letsencrypt/live/example.com/
sudo certbot revoke \
  --cert-path /etc/letsencrypt/archive/example.com/cert3.pem \
  --key-path /etc/letsencrypt/archive/example.com/privkey3.pem \
  --reason keyCompromise --no-delete-after-revoke

--key-path를 주면 계정 키 대신 인증서의 개인키로 폐기 요청에 서명한다. Let’s Encrypt는 인증서 키로 서명된 폐기 요청을 받으면 그 키를 막고 같은 키를 쓰는 다른 인증서도 폐기한다. 새 인증서를 같은 키로 받았다면 그것도 같이 폐기된다. 그래서 새 키가 먼저다.

--no-delete-after-revoke를 빼면 certbot이 폐기 뒤에 인증서를 지울지 묻는다. 여기서 지우겠다고 답하면 그 이름으로 관리하는 인증서 전체, 방금 받은 새 인증서까지 지워질 수 있다.

유료 인증서

대부분의 CA는 유효기간 안에서 새 CSR로 다시 발급(재발급, rekey)해 준다. 새 키와 CSR을 만들어 CA 관리 화면이나 판매처에 낸다.

openssl req -new -newkey rsa:2048 -nodes \
  -keyout example.com.key -out example.com.csr -subj "/CN=example.com"

새 인증서를 건 뒤 이전 인증서 폐기를 요청하고 사유를 키 유출(key compromise)로 적는다. CA/B Forum Baseline Requirements 4.9.1.1은 가입자가 폐기를 요청하거나 CA가 키 유출 증거를 얻으면 24시간 안에 폐기하도록 정한다.

CT 로그와 CAA

웹 루트에 파일을 쓸 수 있었던 공격자는 HTTP-01 검증을 통과할 수 있다. 우리 도메인 인증서를 자기 키로 따로 받았을 수 있다는 뜻이다. 침해 기간에 우리가 받지 않은 발급이 있는지 CT 로그에서 찾는다. 앞으로는 CAA 레코드에 accounturi를 걸어 두면 우리 ACME 계정 말고는 발급받지 못한다(CAA 레코드 설정).

root까지 넘어갔다면 키만 바꿔서 끝나지 않는다. ACME 계정 키(/etc/letsencrypt/accounts)와 서버에 있던 다른 비밀값도 나갔다고 보고 서버를 새로 만든다.

참조