약 8분

인증서를 갱신했더니 mTLS가 unsuitable certificate purpose로 실패할 때

이 글의 순서 6개 절
  1. 인증서의 용도 칸
  2. 왜 빠지나
  3. 무엇이 깨지나
  4. 해당되는지 확인
  5. 옮겨 갈 곳
  6. 참조

인증서를 갱신했는데 웹사이트는 멀쩡하고 서버끼리 붙는 연결만 끊긴다. 상대편 로그에는 이런 줄이 남는다.

verify error:num=26:unsuitable certificate purpose
SSL routines:tls_process_client_certificate:certificate verify failed

새 인증서에 “TLS 클라이언트 인증” 용도가 없어서다. 공인 CA들이 2025년 10월부터 서버 인증서에서 이 용도를 빼고 있다. 인증서를 서버 신분증으로만 쓰는 웹사이트는 영향이 없다. 같은 인증서를 상대 서버에 내밀어 자기를 증명하던 연결(mTLS)이 깨진다.

인증서의 용도 칸

인증서에는 어디에 써도 되는지 적는 확장 필드가 있다. Extended Key Usage(EKU)다.

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -ext extendedKeyUsage

예전 공인 인증서는 대개 두 가지가 다 들어 있었다.

X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication

지금 발급되는 인증서는 한 줄이다. 2026-10-20에 letsencrypt.org가 내준 인증서도, 그 중간 인증서(YE2)도 이렇다.

X509v3 Extended Key Usage:
    TLS Web Server Authentication

TLS Web Server Authentication(serverAuth)은 이 인증서로 서버임을 증명해도 된다는 표시다. TLS Web Client Authentication(clientAuth)은 접속하는 쪽이 자기를 증명하는 데 써도 된다는 표시다.

검증하는 쪽은 상대가 내민 인증서의 용도를 본다. OpenSSL은 클라이언트 인증서를 검증할 때 clientAuth가 있는지 확인하고, serverAuth만 있으면 오류 26(X509_V_ERR_INVALID_PURPOSE)으로 거부한다. EKU 확장이 아예 없는 인증서는 용도 제한이 없는 것으로 보고 통과시킨다. 그래서 EKU를 안 넣는 사설 인증서는 문제가 없고, 용도를 serverAuth로 좁힌 공인 인증서만 걸린다.

왜 빠지나

Chrome 루트 프로그램이 정책 1.3.2에서 Chrome 루트 스토어를 서버 인증 전용으로 정했다. 2026-06-15부터 서버 인증과 다른 용도를 섞은 인증서 계층을 퇴출하고, 2027-03-15 이후 발급되는 인증서는 EKU에 serverAuth만 담아야 한다. 공인 CA는 대부분 이 기한보다 먼저 clientAuth를 뺐다.

CA 기본 발급에서 빠진 날 완전히 끝나는 날
Let’s Encrypt 2026-02-11 (classic 프로필) 2026-07-08 (tlsclient 프로필 종료)
DigiCert 2025-10-01 2027-03-01
Sectigo 2025-10-07 2027-02-10
Google Trust Services 2025-11-10 주간 (조건부 예외) 2026-04-13 주간

Let’s Encrypt에서는 이제 clientAuth가 든 인증서를 받을 방법이 없다. DigiCert는 신청할 때 고르면 아직 두 용도를 다 넣어 주지만 2027-03-01까지다. 지금 인증서에 clientAuth가 있어도 다음 갱신에서 빠진다고 보고 준비한다.

무엇이 깨지나

공인 인증서 하나로 서버 역할과 클라이언트 역할을 같이 하던 곳이다.

XMPP 서버 간 연결이 대표적이다. 서버가 다른 서버에 접속할 때 자기 인증서를 내밀고, 받는 쪽은 OpenSSL로 그 인증서를 검증한다. Prosody가 2026-02-06 공지에서 이 문제를 알렸다. 받는 쪽이 ejabberd라면 25.08 이상이어야 하고, Openfire는 문제없다. dialback(DNS로 상대 서버를 확인하는 예비 방식)을 켜 둔 서버끼리는 연결이 계속 될 수 있어서, 문제가 늦게 드러난다.

Microsoft Teams Direct Routing의 SBC 인증서도 볼 대상이다. Microsoft SIP 엔드포인트는 clientAuth 없는 SBC 인증서도 받아 주고, 당분간 그럴 거라고 밝혔다. 다만 SBC 장비나 그 안의 TLS 라이브러리가 clientAuth 없는 인증서로는 나가는 연결을 만들지 않을 수 있다고 함께 적었다. 갱신 전에 SBC 제조사 문서를 확인한다.

그 밖에 공인 인증서를 클라이언트 인증서로 등록해 둔 곳을 찾는다. 파트너사 API가 mTLS로 공인 인증서를 요구하는 경우, nginx의 ssl_verify_client나 Apache의 SSLVerifyClient로 서버 간 호출을 거르는 경우, 메일 서버끼리 인증서로 상대를 확인하게 설정한 경우가 그렇다.

해당되는지 확인

Certbot으로 받은 인증서라면 leaf와 중간 인증서가 파일로 나뉘어 있다. leaf를 클라이언트 용도로 검증해 본다.

cd /etc/letsencrypt/live/example.com
openssl verify -purpose sslclient -untrusted chain.pem cert.pem

clientAuth가 없으면 이렇게 나온다.

CN=example.com
error 26 at 0 depth lookup: unsuitable certificate purpose
error cert.pem: verification failed

-purpose sslserver로 바꾸면 통과한다. 서버 인증서로는 아무 문제가 없다는 뜻이다. macOS 기본 openssl은 LibreSSL이라 같은 오류를 unsupported certificate purpose로 적는다. 번호 26은 같다.

받는 쪽 로그도 이 번호로 찾는다. 위에서 본 verify error:num=26이 남아 있으면 원인은 용도 칸이다. 체인이나 만료 문제라면 번호가 다르다(만료는 10, 발급자를 못 찾으면 20).

옮겨 갈 곳

클라이언트 인증은 사설 CA로 옮긴다. 상호 인증은 미리 합의한 상대끼리 하는 일이라 브라우저가 믿는 공인 루트가 필요 없다. 받는 쪽이 공인 루트를 통째로 믿게 해 두면, 인증서 이름까지 확인하지 않는 한 그 CA가 발급한 아무 인증서나 통과한다. 믿을 루트를 직접 정하는 쪽이 더 좁고 안전하다.

서버 몇 대 사이라면 openssl로 루트 하나를 만들고 클라이언트 인증서를 직접 서명하면 된다.

# 사설 루트 (10년)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout ca.key -out ca.crt -days 3650 -subj "/CN=Example Internal mTLS CA" \
  -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"

# 클라이언트 인증서 (clientAuth만, 1년)
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout client.key -out client.csr -subj "/CN=batch01"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt -days 365 \
  -extfile <(printf "extendedKeyUsage=clientAuth\nkeyUsage=critical,digitalSignature\n")

받는 쪽은 이 루트만 믿게 한다. nginx라면 이렇다.

ssl_client_certificate /etc/nginx/mtls/ca.crt;   # 사설 루트만 신뢰
ssl_verify_client      on;

ca.key는 서명할 때만 꺼내 쓰고 서버에 두지 않는다. 대수가 많거나 자동 갱신이 필요하면 ACME를 지원하는 사설 CA(step-ca 같은)나 클라우드의 관리형 사설 CA(AWS Private CA, Google Cloud Certificate Authority Service)를 쓴다.

상대 회사가 공인 CA 인증서를 요구한다면 다음 갱신 때 clientAuth를 넣어 줄 수 있는지 CA에 묻고, 위 표의 기한 안에 사설 CA로 옮기도록 상대와 일정을 맞춘다.

옮기고 나면 서버 인증서(공인)와 클라이언트 인증서(사설)를 따로 관리하게 된다. 사설 인증서는 CT 로그에 올라가지 않아서 외부 감시 도구가 대신 봐 주지 않는다. 만료 알림은 직접 만드는 방법대로 사설 인증서 파일에도 건다.

참조