인증서를 갱신했더니 mTLS가 unsuitable certificate purpose로 실패할 때
인증서를 갱신했는데 웹사이트는 멀쩡하고 서버끼리 붙는 연결만 끊긴다. 상대편 로그에는 이런 줄이 남는다.
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 로그에 올라가지 않아서 외부 감시 도구가 대신 봐 주지 않는다. 만료 알림은 직접 만드는 방법대로 사설 인증서 파일에도 건다.
참조
- Chrome Root Program Policy 1.8: 1.3.2 Dedicated TLS Server Authentication PKI Hierarchies
- Let’s Encrypt: Ending TLS Client Authentication Certificate Support in 2026
- Let’s Encrypt: Profiles
- DigiCert: Removing the client authentication EKU from public TLS certificates
- Sectigo: Deprecation of client authentication EKU
- Google Trust Services: Client Authentication Certificates Deprecation
- Prosody: Upcoming changes to Let’s Encrypt and how they affect operators
- Microsoft: What’s new in Direct Routing
- OpenSSL: openssl-verify
- nginx: ngx_http_ssl_module