Split View: TLS 인증서 완전 가이드: openssl 명령으로 끝까지 다루기
TLS 인증서 완전 가이드: openssl 명령으로 끝까지 다루기
- 들어가며
- 1. 파일 형식 구분 — 무엇을 받았는지부터 확인
- 2. 인증서 읽기 — 이 파일이 무엇인지 확인
- 3. 키와 인증서가 짝인지 확인
- 4. CSR 만들기 — SAN을 반드시 넣기
- 5. 체인 검증 — 중간 인증서 누락 잡기
- 6. 서버에 붙어 진단하기 — s_client
- 7. 자주 겪는 오류와 판별 순서
- 퀴즈: 실력을 확인해 보세요
- 마치며
- 참고 자료
- 이어서 읽기
들어가며
인증서 문제는 항상 급합니다. 갱신을 놓쳐 서비스가 멈췄거나, 새로 설치했는데 일부 클라이언트만 실패하거나, 파일 형식이 맞지 않아 로드가 안 됩니다. 그때 필요한 것은 TLS 핸드셰이크 이론이 아니라 지금 이 파일이 무엇이고 무엇이 빠졌는지 확인하는 명령입니다.
이 블로그에는 SSL/TLS 인증서 완벽 가이드가 이미 있습니다. 그 글은 Let's Encrypt 발급, Nginx 설정, 자동 갱신 같은 발급과 운영 흐름을 다룹니다. 이 글은 다른 것을 목표로 합니다. openssl 명령 자체의 레퍼런스입니다. 인증서 파일을 앞에 두고 무엇을 확인해야 하는지, 어떤 명령이 어떤 사실을 알려 주는지를 목적별로 정리합니다. 발급 방법이 아니라 진단 방법이 주제입니다.
이 글의 명령은 전부 읽기와 진단 위주입니다. 인증서를 잘못 읽어서 생기는 피해는 없지만, 개인 키를 다루는 명령은 다릅니다. 키 파일을 화면에 출력하거나 잘못된 권한으로 저장하면 그 순간 유출입니다. 그래서 이 글은 가능한 한 키 내용을 출력하지 않고 다이제스트만 비교하는 방식을 기본으로 삼습니다.
기준은 OpenSSL 3.x입니다. OpenSSL 1.1.1과 3.x는 일부 옵션과 기본 동작이 다릅니다. 또한 macOS 기본 환경이나 일부 배포판은 LibreSSL을 쓰며, 이 경우 옵션이 다를 수 있습니다. 실행 전에 버전을 확인하세요.
openssl version -a
1. 파일 형식 구분 — 무엇을 받았는지부터 확인
인증서 관련 사고의 상당수는 형식 착오에서 시작합니다. 확장자는 참고일 뿐 내용을 보장하지 않습니다.
| 형식 | 내용 | 흔한 확장자 |
|---|---|---|
| PEM | Base64 텍스트. 헤더 줄이 있음 | .pem .crt .cer .key |
| DER | 이진 인코딩 | .der .cer |
| PKCS#12 | 인증서와 개인 키를 한 파일에 | .p12 .pfx |
| PKCS#7 | 인증서 묶음(키 없음) | .p7b .p7c |
| PKCS#8 | 개인 키 표준 형식 | .key .pem |
먼저 파일이 텍스트인지 이진인지 봅니다.
file server.crt
head -1 server.crt
PEM이면 첫 줄에 -----BEGIN 으로 시작하는 표시가 있습니다. 그 표시가 무엇인지가 곧 내용입니다.
BEGIN CERTIFICATE: 인증서BEGIN CERTIFICATE REQUEST: CSRBEGIN PRIVATE KEY: PKCS#8 개인 키BEGIN RSA PRIVATE KEY: 전통적 RSA 개인 키BEGIN ENCRYPTED PRIVATE KEY: 암호화된 개인 키
형식 변환은 다음과 같습니다.
openssl x509 -in server.der -inform DER -out server.pem -outform PEM
openssl x509 -in server.pem -outform DER -out server.der
openssl pkcs12 -in bundle.pfx -nodes -out bundle.pem
openssl pkcs12 -export -inkey server.key -in server.crt -certfile chain.crt -out bundle.pfx
openssl pkcs7 -print_certs -in chain.p7b -out chain.pem
-inform과 -outform은 입력·출력 인코딩을 지정합니다. PKCS#12 변환에서 -nodes는 개인 키를 암호화하지 않고 출력한다는 뜻이므로, 결과 파일의 권한을 즉시 제한해야 합니다.
chmod 600 bundle.pem
형식 착오가 실제로 어떻게 사고로 이어지는지 하나만 예를 들면 이렇습니다. 윈도우 서버에서 내보낸 .pfx 파일을 받아 Nginx에 그대로 지정하면 서버가 뜨지 않습니다. Nginx는 PEM 형식의 인증서와 키를 각각 요구하기 때문입니다. 반대로 자바 애플리케이션에 PEM 쌍을 그대로 주면 keystore 형식이 아니라며 거부합니다. 파일을 받으면 확장자를 믿지 말고 내용을 먼저 확인하는 것이 30분을 아껴 줍니다.
PKCS#12 묶음에서 필요한 부분만 꺼낼 수도 있습니다. 인증서만, 또는 키만 분리해야 하는 상황이 자주 생깁니다.
openssl pkcs12 -in bundle.pfx -clcerts -nokeys -out server.crt
openssl pkcs12 -in bundle.pfx -cacerts -nokeys -out chain.crt
openssl pkcs12 -in bundle.pfx -nocerts -nodes -out server.key
-clcerts는 클라이언트(최종 개체) 인증서만, -cacerts는 CA 인증서만, -nocerts는 인증서를 제외한 키만 출력합니다.
2. 인증서 읽기 — 이 파일이 무엇인지 확인
가장 많이 쓰는 명령부터 봅니다.
openssl x509 -in server.crt -noout -text
openssl x509 -in server.crt -noout -subject -issuer -dates
openssl x509 -in server.crt -noout -serial -fingerprint -sha256
openssl x509 -in server.crt -noout -ext subjectAltName
옵션의 의미는 문서 기준으로 다음과 같습니다.
-noout은 인코딩된 원본 출력을 억제하고, 요청한 항목만 보여 줍니다.-text는 인증서 전체를 사람이 읽는 형태로 출력합니다.-subject,-issuer는 주체와 발급자,-dates는 유효 시작·종료 시각을 보여 줍니다.-startdate,-enddate로 각각 볼 수도 있습니다.-fingerprint는 DER 인코딩본의 다이제스트를 계산합니다.-ext는 지정한 X.509 확장을 출력합니다.
실무에서 가장 자주 확인해야 하는 것은 SAN(Subject Alternative Name) 입니다. 현대 브라우저와 라이브러리는 CN을 보지 않고 SAN만 봅니다. CN에 도메인이 있어도 SAN에 없으면 실패합니다.
openssl x509 -in server.crt -noout -ext subjectAltName
만료 여부는 초 단위로 판정할 수 있습니다. 문서에 따르면 -checkend는 지정한 초 이내에 만료되는지 확인합니다.
openssl x509 -in server.crt -noout -checkend 0
openssl x509 -in server.crt -noout -checkend 2592000
echo "exit=$?"
종료 코드가 0이면 그 기간 안에 만료되지 않는다는 뜻입니다. 30일(2592000초)을 넣어 두면 갱신 알림 스크립트를 그대로 만들 수 있습니다.
인증서 본문에서 실제로 확인할 가치가 있는 항목은 생각보다 적습니다. 유효 기간과 SAN이 8할이고, 나머지는 문제 상황에서만 봅니다. -text 출력에서 눈여겨볼 것을 꼽으면 다음과 같습니다.
- Signature Algorithm: 서명 알고리즘입니다. 오래된 SHA-1 서명은 최신 클라이언트가 거부합니다.
- Public Key Algorithm과 키 길이: RSA 1024비트 같은 짧은 키는 거부됩니다.
- Basic Constraints: CA 여부입니다. 서버 인증서인데 CA로 표시되어 있으면 잘못 발급된 것입니다.
- Key Usage와 Extended Key Usage: 서버 인증 용도가 포함되어 있어야 합니다. 클라이언트 인증용으로 발급된 인증서를 서버에 설치하면 실패합니다.
- Authority Information Access: 발급자 인증서와 OCSP 응답기 주소가 들어 있습니다.
이 항목들은 새 CA를 도입하거나 발급 요청 양식을 바꿨을 때 한 번은 확인해 두는 편이 좋습니다.
#!/usr/bin/env bash
set -uo pipefail
for CRT in /etc/pki/tls/certs/*.crt; do
if ! openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1; then
echo "EXPIRING SOON: $CRT"
openssl x509 -in "$CRT" -noout -subject -enddate
fi
done
3. 키와 인증서가 짝인지 확인
"인증서를 바꿨는데 서버가 안 뜬다"의 절반은 키와 인증서가 짝이 아니어서입니다. 판별 방법은 공개 키 부분을 비교하는 것입니다.
RSA 키라면 modulus를 비교합니다.
openssl x509 -in server.crt -noout -modulus | openssl sha256
openssl rsa -in server.key -noout -modulus | openssl sha256
openssl req -in server.csr -noout -modulus | openssl sha256
세 값이 같으면 CSR, 인증서, 키가 모두 한 쌍입니다. 다르면 짝이 아닙니다.
RSA가 아닌 키(ECDSA, Ed25519)에는 -modulus가 없습니다. 이 경우 공개 키 자체를 뽑아 비교합니다.
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
이 방법은 키 종류와 무관하게 동작하므로, 기억할 명령을 하나만 고른다면 이쪽이 낫습니다.
개인 키 자체의 정보는 다음으로 봅니다.
openssl pkey -in server.key -noout -text
openssl rsa -in server.key -check -noout
파괴적 명령 경고 대신 유출 주의: 개인 키를 출력하는 명령은 터미널 기록과 화면 공유에 그대로 남습니다. 키 내용 전체를 출력하는 대신 위처럼 다이제스트만 비교하는 습관을 들이세요.
4. CSR 만들기 — SAN을 반드시 넣기
CSR 생성에서 가장 흔한 실수는 SAN을 넣지 않는 것입니다. 설정 파일을 쓰는 편이 확실합니다.
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[dn]
C = KR
ST = Seoul
O = Example Corp
CN = www.example.com
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = api.example.com
이 파일을 csr.cnf로 저장한 뒤 생성합니다.
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -config csr.cnf
openssl req -in server.csr -noout -text
openssl req -in server.csr -noout -verify
ECDSA 키를 쓸 경우입니다.
openssl ecparam -name prime256v1 -genkey -noout -out server-ec.key
openssl req -new -key server-ec.key -out server-ec.csr -config csr.cnf
생성 후에는 반드시 SAN이 들어갔는지 확인하세요. 여기서 놓치면 발급을 다시 받아야 합니다.
openssl req -in server.csr -noout -text | grep -A3 'Subject Alternative Name'
내부 테스트용 사설 CA와 서버 인증서는 다음처럼 만듭니다.
openssl req -x509 -new -nodes -newkey rsa:4096 -sha256 -days 3650 \
-subj '/C=KR/O=Example Internal/CN=Example Internal Root CA' \
-keyout ca.key -out ca.crt
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext
-CAcreateserial은 일련번호 파일이 없으면 만듭니다. -extfile과 -extensions를 지정하지 않으면 SAN이 서명 결과에 포함되지 않습니다. 사설 CA에서 만든 인증서가 브라우저에서 실패하는 가장 흔한 원인입니다.
5. 체인 검증 — 중간 인증서 누락 잡기
"내 브라우저에서는 되는데 서버에서 curl로는 안 된다"의 표준 원인은 중간 인증서 누락입니다. 브라우저는 캐시나 AIA 정보로 중간 인증서를 보충하지만, 명령줄 도구와 서버 간 통신은 그렇지 않습니다.
로컬 파일로 검증합니다.
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile root.crt -untrusted intermediate.crt server.crt
openssl verify -show_chain -CAfile /etc/pki/tls/certs/ca-bundle.crt server.crt
-CAfile은 신뢰 앵커(루트)를 지정합니다.-untrusted는 중간 인증서를 제공합니다.-show_chain은 구성된 체인을 보여 줍니다.
시스템 신뢰 저장소 경로는 배포판마다 다릅니다. RHEL 계열은 /etc/pki/tls/certs/ca-bundle.crt, Debian/Ubuntu 계열은 /etc/ssl/certs/ca-certificates.crt 입니다.
인증서 파일 안에 몇 장이 들어 있는지 세는 것도 유용합니다.
grep -c 'BEGIN CERTIFICATE' fullchain.pem
서버에 설치하는 파일은 보통 서버 인증서 다음에 중간 인증서 순서로 이어 붙입니다. 루트는 포함하지 않는 것이 관례입니다. 순서가 뒤바뀌면 일부 클라이언트가 실패합니다.
cat server.crt intermediate.crt > fullchain.pem
openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout
마지막 명령은 묶음 파일 안의 각 인증서의 subject와 issuer를 나열해 줍니다. 앞 인증서의 issuer가 다음 인증서의 subject와 일치하는지 확인하면 순서가 맞는지 알 수 있습니다.
체인 구성에서 헷갈리기 쉬운 지점을 정리하면 이렇습니다. 서버가 보내야 하는 것은 자신의 인증서와, 루트에 도달하기까지 필요한 중간 인증서 전부입니다. 루트 인증서는 클라이언트가 이미 신뢰 저장소에 가지고 있으므로 보낼 필요가 없고, 보내도 대개 무시됩니다. 다만 보내면 핸드셰이크마다 불필요한 바이트가 오가므로 관례적으로 제외합니다.
중간 인증서가 여러 장인 경우도 있습니다. 요즘 상용 CA는 두 단계 중간 구조를 쓰는 경우가 있어, 발급 메일에 첨부된 번들을 그대로 쓰지 않고 직접 조립하다가 한 장을 빠뜨리는 사고가 생깁니다. 조립했다면 반드시 검증까지 하세요.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted intermediate.crt server.crt
이 명령이 OK를 출력하면 로컬 신뢰 저장소 기준으로 체인이 완성된 것입니다. 실패하면 어느 단계에서 끊겼는지 오류 메시지에 나옵니다.
6. 서버에 붙어 진단하기 — s_client
실제 서버가 무엇을 보내는지 확인하는 명령입니다.
openssl s_client -connect example.com:443 -servername example.com
openssl s_client -connect example.com:443 -servername example.com -showcerts
openssl s_client -connect example.com:443 -servername example.com -brief
openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -status
openssl s_client -connect smtp.example.com:587 -starttls smtp
옵션의 의미는 문서 기준입니다.
-connect는 접속 대상입니다.-servername은 ClientHello에 SNI를 넣습니다. 가상 호스팅 환경에서 이것이 없으면 엉뚱한 기본 인증서를 받습니다. 진단할 때 반드시 넣으세요.-showcerts는 서버가 보낸 인증서 목록을 그대로 보여 줍니다. 문서가 명시하듯 이것은 검증된 체인이 아니라 서버가 보낸 그대로입니다. 그래서 중간 인증서 누락을 확인하는 데 딱 맞습니다.-status는 OCSP 스테이플링 응답을 요청합니다.-brief는 연결 요약만 출력합니다.-starttls는 프로토콜별 전환 메시지를 보냅니다. smtp, imap, pop3, ftp, postgres, mysql, ldap 등을 지원합니다.
명령이 입력을 기다리며 멈춰 있으므로, 스크립트에서는 표준 입력을 닫아 줍니다.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates
이 한 줄이 원격 인증서 만료 확인의 표준 관용구입니다. 배치로 여러 호스트를 점검할 때 그대로 씁니다.
for H in www.example.com api.example.com admin.example.com; do
printf '%s ' "$H"
echo | openssl s_client -connect "$H:443" -servername "$H" 2>/dev/null \
| openssl x509 -noout -enddate
done
체인 검증 결과는 출력 앞부분의 verify 관련 줄에서 확인합니다. unable to get local issuer certificate가 보이면 중간 인증서가 빠졌거나 로컬 신뢰 저장소에 루트가 없는 것입니다.
7. 자주 겪는 오류와 판별 순서
| 증상 | 확인 명령 | 원인 후보 |
|---|---|---|
| 브라우저는 되고 curl은 실패 | s_client -showcerts | 중간 인증서 누락 |
| 특정 도메인만 실패 | x509 -ext subjectAltName | SAN 누락 |
| 서버 기동 실패, 키 오류 | modulus 또는 pubkey 다이제스트 비교 | 키-인증서 불일치 |
| 갱신했는데 옛 인증서가 보임 | s_client -servername | 재적재 누락, 다른 가상 호스트 |
| 오래된 클라이언트만 실패 | s_client -tls1_2 | 프로토콜·암호군 불일치 |
| 사설 CA 인증서가 거부됨 | verify -CAfile | 신뢰 저장소에 CA 미등록 |
| 만료 직전 알림이 안 옴 | x509 -checkend | 모니터링 부재 |
판별 순서는 항상 같습니다. 첫째, 파일이 무엇인지 확인합니다. 둘째, 키와 짝인지 확인합니다. 셋째, 체인이 완전한지 확인합니다. 넷째, 서버가 실제로 무엇을 보내는지 확인합니다. 이 순서를 지키면 대부분 두 번째나 세 번째에서 원인이 나옵니다.
사설 CA를 시스템 신뢰 저장소에 등록하는 방법도 배포판마다 다릅니다.
# RHEL 계열
sudo cp internal-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
# Debian 계열
sudo cp internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates
주의: 파일 확장자가 배포판마다 요구되는 형태가 다릅니다. Debian 계열은 .crt 확장자와 PEM 형식을 요구합니다. 등록 후에는 실제로 신뢰되는지 확인하세요.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1
갱신 후 반영되지 않는 문제도 흔합니다. 파일만 바꾸고 서비스를 재적재하지 않으면 프로세스가 메모리에 올려 둔 옛 인증서를 계속 씁니다. 파일 시각과 실제 제공 중인 인증서를 대조하면 즉시 판별할 수 있습니다.
ls -l --time-style=long-iso /etc/pki/tls/certs/server.crt
openssl x509 -in /etc/pki/tls/certs/server.crt -noout -enddate
echo | openssl s_client -connect localhost:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -enddate
파일의 만료일과 서버가 실제로 제공하는 인증서의 만료일이 다르면 재적재가 빠진 것입니다. 자동 갱신을 설정했다면 갱신 훅에 서비스 재적재까지 포함되어 있는지 반드시 확인하세요. 갱신은 성공했는데 반영이 안 되어 만료로 장애가 나는 사례가 실제로 적지 않습니다.
또 하나 자주 잊는 것이 애플리케이션마다 신뢰 저장소가 다르다는 사실입니다. 자바는 자체 keystore를, Python은 certifi 번들을, Node.js는 내장 목록을 씁니다. 시스템에 CA를 등록했는데도 특정 애플리케이션만 실패한다면 그 런타임의 신뢰 저장소를 따로 확인해야 합니다.
퀴즈: 실력을 확인해 보세요
퀴즈 1: 브라우저에서는 정상인데 서버 간 API 호출만 인증서 오류가 납니다. 무엇을 먼저 확인하나요?
정답: 서버가 중간 인증서를 함께 보내는지 확인합니다
설명: 브라우저는 캐시나 AIA 정보로 중간 인증서를 보충하지만, 명령줄 도구와 대부분의 언어 런타임은 그렇지 않습니다.
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
1이 나오면 서버 인증서만 보내는 것이므로 중간 인증서가 빠진 것입니다. 서버에 설치할 파일을 서버 인증서 다음에 중간 인증서 순서로 이어 붙여 다시 설치하세요.
퀴즈 2: 인증서와 개인 키가 한 쌍인지 확인하는, 키 종류와 무관한 방법은?
정답: 양쪽에서 공개 키를 뽑아 다이제스트를 비교합니다
설명: -modulus 비교는 RSA에만 통합니다. ECDSA나 Ed25519에서는 다음 방법을 씁니다.
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
두 값이 같으면 한 쌍입니다. 이 방법은 키 종류를 가리지 않으므로 하나만 외운다면 이쪽입니다. 개인 키 내용 자체를 화면에 출력하지 않는다는 점에서도 안전합니다.
퀴즈 3: 사설 CA로 만든 서버 인증서가 브라우저에서 도메인 불일치로 거부됩니다. 무엇을 빠뜨렸을까요?
정답: 서명 시 SAN 확장을 포함하지 않았습니다
설명: CSR에 SAN을 넣었더라도 openssl x509 -req로 서명할 때 확장을 명시하지 않으면 결과 인증서에 SAN이 들어가지 않습니다.
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext
서명 후 반드시 확인하세요.
openssl x509 -in server.crt -noout -ext subjectAltName
현대 클라이언트는 CN을 보지 않고 SAN만 봅니다.
퀴즈 4: 여러 도메인이 한 IP에 있습니다. s_client로 특정 도메인의 인증서를 확인하려면?
정답: -servername으로 SNI를 지정합니다
설명: SNI를 주지 않으면 서버가 기본 가상 호스트의 인증서를 반환하므로, 확인하려던 도메인의 인증서가 아닌 것을 보게 됩니다.
echo | openssl s_client -connect 203.0.113.10:443 -servername api.example.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
문서에 따르면 -servername을 생략하면 -connect의 호스트명이 기본값으로 쓰이므로, IP로 직접 접속할 때는 반드시 명시해야 합니다.
퀴즈 5: 30일 안에 만료되는 인증서를 자동으로 찾아내려면?
정답: -checkend에 초 단위 값을 주고 종료 코드로 판정합니다
설명: 문서에 따르면 -checkend는 지정한 초 이내에 인증서가 만료되는지 확인합니다. 30일은 2592000초입니다.
for CRT in /etc/pki/tls/certs/*.crt; do
openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1 \
|| echo "EXPIRING: $CRT"
done
원격 서버도 같은 방식으로 점검할 수 있습니다.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -checkend 2592000
퀴즈 6: 시스템 신뢰 저장소에 사내 CA를 등록했는데 자바 애플리케이션만 여전히 실패합니다. 왜일까요?
정답: 애플리케이션 런타임이 자체 신뢰 저장소를 쓰기 때문입니다
설명: 자바는 자체 keystore를, Python은 certifi 번들을, Node.js는 내장 CA 목록을 사용합니다. OS 신뢰 저장소를 갱신해도 그 런타임에는 반영되지 않습니다.
먼저 시스템 수준에서는 정상인지 확인합니다.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1
curl은 되는데 애플리케이션만 실패한다면 원인이 확정됩니다. 각 런타임에 CA를 등록하는 정확한 절차는 해당 런타임 문서에서 확인하세요.
마치며
인증서 진단은 순서만 지키면 어렵지 않습니다. 파일이 무엇인지, 키와 짝인지, 체인이 완전한지, 서버가 실제로 무엇을 보내는지. 이 네 가지를 차례로 확인하면 원인이 드러납니다.
그리고 급할 때 기억나지 않는 명령은 소용이 없습니다. 다음 세 줄만은 외워 두세요.
# 1. 로컬 파일 확인
openssl x509 -in server.crt -noout -text
# 2. 원격 서버 확인
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates
# 3. 키와 인증서 짝 확인
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
이 세 블록이 인증서 관련 작업의 90퍼센트를 커버합니다.
마지막으로, 만료로 인한 장애는 100퍼센트 예방 가능한 사고입니다. -checkend 한 줄로 만드는 점검 스크립트를 오늘 안에 걸어 두세요.
참고 자료
- openssl-x509 공식 문서 (2026-08-15 확인)
- openssl-s_client 공식 문서 (2026-08-15 확인)
- openssl-req 공식 문서 (2026-08-15 확인)
- openssl-verify 공식 문서 (2026-08-15 확인)
이어서 읽기
- 이전 글: 백업과 복구 완전 가이드
- 다음 글: 파일 디스크립터와 inode 완전 가이드
- SSL/TLS 인증서 완벽 가이드 — 발급과 자동 갱신 흐름
- SSH 운영 완전 가이드 — 같은 계열의 키 관리 문제
- curl 빌더 — TLS 옵션이 붙은 curl 명령 만들기
- Linux 터미널 — openssl 명령 연습
The Complete Guide to TLS Certificates: Handling Them End to End with openssl
- Introduction
- 1. Telling formats apart — start by confirming what you received
- 2. Reading a certificate — confirming what this file is
- 3. Checking whether the key and the certificate are a pair
- 4. Creating a CSR — always include the SAN
- 5. Chain verification — catching a missing intermediate
- 6. Diagnosing against a live server — s_client
- 7. Common errors and the order of diagnosis
- Quiz: check your understanding
- Closing
- References
- Further reading
Introduction
Certificate problems are always urgent. You missed a renewal and the service stopped, or you installed a new certificate and only some clients fail, or the file format is wrong and it will not load. What you need at that moment is not TLS handshake theory but the commands that tell you what this file is and what is missing from it right now.
This blog already has the complete guide to SSL/TLS certificates. That article covers the issuance and operations flow: Let's Encrypt issuance, Nginx configuration, automatic renewal. This article aims at something different. It is a reference for the openssl commands themselves. With a certificate file in front of you, it organizes by purpose what you should check and which command tells you which fact. The subject is diagnosis, not issuance.
Every command in this article is centered on reading and diagnosing. Reading a certificate wrong causes no damage, but commands that handle private keys are different. Print a key file to the screen or save it with the wrong permissions and it has leaked at that instant. So this article takes as its baseline the approach of comparing digests without printing key contents wherever possible.
The baseline is OpenSSL 3.x. OpenSSL 1.1.1 and 3.x differ in some options and default behavior. Also, the default environment on macOS and some distributions uses LibreSSL, where options may differ. Check the version before you run anything.
openssl version -a
1. Telling formats apart — start by confirming what you received
A large share of certificate incidents begins with a format mix-up. The extension is only a hint; it guarantees nothing about the contents.
| Format | Contents | Common extensions |
|---|---|---|
| PEM | Base64 text with header lines | .pem .crt .cer .key |
| DER | Binary encoding | .der .cer |
| PKCS#12 | Certificate and private key in one file | .p12 .pfx |
| PKCS#7 | Certificate bundle (no key) | .p7b .p7c |
| PKCS#8 | Standard private key format | .key .pem |
First look at whether the file is text or binary.
file server.crt
head -1 server.crt
If it is PEM, the first line carries a marker starting with -----BEGIN. What that marker says is the contents.
BEGIN CERTIFICATE: a certificateBEGIN CERTIFICATE REQUEST: a CSRBEGIN PRIVATE KEY: a PKCS#8 private keyBEGIN RSA PRIVATE KEY: a traditional RSA private keyBEGIN ENCRYPTED PRIVATE KEY: an encrypted private key
Format conversion works like this.
openssl x509 -in server.der -inform DER -out server.pem -outform PEM
openssl x509 -in server.pem -outform DER -out server.der
openssl pkcs12 -in bundle.pfx -nodes -out bundle.pem
openssl pkcs12 -export -inkey server.key -in server.crt -certfile chain.crt -out bundle.pfx
openssl pkcs7 -print_certs -in chain.p7b -out chain.pem
-inform and -outform specify the input and output encodings. In a PKCS#12 conversion, -nodes means the private key is written out unencrypted, so you must restrict the permissions on the resulting file immediately.
chmod 600 bundle.pem
Here is one example of how a format mix-up actually turns into an incident. You receive a .pfx file exported from a Windows server, point Nginx straight at it, and the server will not start. Nginx wants a PEM certificate and a PEM key as separate files. Conversely, hand a Java application a PEM pair and it refuses them because they are not in keystore format. When you receive a file, do not trust the extension; check the contents first — that saves you 30 minutes.
You can also pull out only the part you need from a PKCS#12 bundle. Situations where you have to separate out just the certificate, or just the key, come up often.
openssl pkcs12 -in bundle.pfx -clcerts -nokeys -out server.crt
openssl pkcs12 -in bundle.pfx -cacerts -nokeys -out chain.crt
openssl pkcs12 -in bundle.pfx -nocerts -nodes -out server.key
-clcerts outputs only the client (end-entity) certificate, -cacerts only the CA certificates, and -nocerts only the key with the certificates excluded.
2. Reading a certificate — confirming what this file is
Start with the commands you will use most.
openssl x509 -in server.crt -noout -text
openssl x509 -in server.crt -noout -subject -issuer -dates
openssl x509 -in server.crt -noout -serial -fingerprint -sha256
openssl x509 -in server.crt -noout -ext subjectAltName
Per the documentation, the options mean the following.
-nooutsuppresses the encoded original output and shows only what you asked for.-textprints the whole certificate in human-readable form.-subjectand-issuershow the subject and the issuer, and-datesshows the validity start and end times. You can also see them individually with-startdateand-enddate.-fingerprintcomputes the digest of the DER encoding.-extprints the X.509 extension you name.
In practice the thing you have to check most often is the SAN (Subject Alternative Name). Modern browsers and libraries ignore the CN and look only at the SAN. Even if the domain is in the CN, it fails when it is not in the SAN.
openssl x509 -in server.crt -noout -ext subjectAltName
Expiry can be judged down to the second. According to the documentation, -checkend checks whether the certificate expires within the given number of seconds.
openssl x509 -in server.crt -noout -checkend 0
openssl x509 -in server.crt -noout -checkend 2592000
echo "exit=$?"
An exit code of 0 means it does not expire within that window. Put in 30 days (2592000 seconds) and you have a renewal alerting script as-is.
There are fewer fields in the certificate body worth actually checking than you might think. Validity period and SAN are 80 percent of it, and the rest you only look at when something is wrong. If you had to pick what to watch for in the -text output, it would be these.
- Signature Algorithm: the signing algorithm. Modern clients reject old SHA-1 signatures.
- Public Key Algorithm and key length: short keys such as RSA 1024-bit are rejected.
- Basic Constraints: whether it is a CA. If a server certificate is marked as a CA, it was mis-issued.
- Key Usage and Extended Key Usage: server authentication has to be included. Install a certificate issued for client authentication on a server and it fails.
- Authority Information Access: it carries the issuer certificate and OCSP responder addresses.
It is worth checking these fields once when you adopt a new CA or change the issuance request form.
#!/usr/bin/env bash
set -uo pipefail
for CRT in /etc/pki/tls/certs/*.crt; do
if ! openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1; then
echo "EXPIRING SOON: $CRT"
openssl x509 -in "$CRT" -noout -subject -enddate
fi
done
3. Checking whether the key and the certificate are a pair
Half of all "I swapped the certificate and the server will not start" cases come down to the key and the certificate not being a pair. The way to tell is to compare the public key portion.
For an RSA key, compare the modulus.
openssl x509 -in server.crt -noout -modulus | openssl sha256
openssl rsa -in server.key -noout -modulus | openssl sha256
openssl req -in server.csr -noout -modulus | openssl sha256
If all three values match, the CSR, the certificate, and the key are one set. If they differ, they are not a pair.
Non-RSA keys (ECDSA, Ed25519) have no -modulus. In that case extract the public key itself and compare.
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
This works regardless of key type, so if you are going to memorize just one command, make it this one.
For information about the private key itself, use the following.
openssl pkey -in server.key -noout -text
openssl rsa -in server.key -check -noout
A leak warning in place of a destructive command warning: commands that print a private key leave it in terminal history and in screen shares. Instead of printing the full key contents, build the habit of comparing only digests as shown above.
4. Creating a CSR — always include the SAN
The most common mistake when generating a CSR is leaving out the SAN. Using a configuration file is the reliable way.
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[dn]
C = KR
ST = Seoul
O = Example Corp
CN = www.example.com
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = api.example.com
Save this file as csr.cnf and then generate.
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -config csr.cnf
openssl req -in server.csr -noout -text
openssl req -in server.csr -noout -verify
If you use an ECDSA key.
openssl ecparam -name prime256v1 -genkey -noout -out server-ec.key
openssl req -new -key server-ec.key -out server-ec.csr -config csr.cnf
After generating, always confirm that the SAN went in. Miss it here and you have to get the certificate reissued.
openssl req -in server.csr -noout -text | grep -A3 'Subject Alternative Name'
A private CA and a server certificate for internal testing are made like this.
openssl req -x509 -new -nodes -newkey rsa:4096 -sha256 -days 3650 \
-subj '/C=KR/O=Example Internal/CN=Example Internal Root CA' \
-keyout ca.key -out ca.crt
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext
-CAcreateserial creates the serial number file if it does not exist. If you do not specify -extfile and -extensions, the SAN is not included in the signed result. That is the most common reason a certificate made by a private CA fails in the browser.
5. Chain verification — catching a missing intermediate
The standard cause of "it works in my browser but not with curl on the server" is a missing intermediate certificate. Browsers fill in the intermediate from cache or from AIA information, but command-line tools and server-to-server traffic do not.
Verify against local files.
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile root.crt -untrusted intermediate.crt server.crt
openssl verify -show_chain -CAfile /etc/pki/tls/certs/ca-bundle.crt server.crt
-CAfilespecifies the trust anchor (the root).-untrustedsupplies intermediate certificates.-show_chaindisplays the chain that was built.
The system trust store path differs per distribution. On RHEL-family systems it is /etc/pki/tls/certs/ca-bundle.crt, and on Debian/Ubuntu systems it is /etc/ssl/certs/ca-certificates.crt.
Counting how many certificates are inside a certificate file is also useful.
grep -c 'BEGIN CERTIFICATE' fullchain.pem
The file you install on the server is normally concatenated with the server certificate first and the intermediate certificates after it. By convention the root is not included. If the order is reversed, some clients fail.
cat server.crt intermediate.crt > fullchain.pem
openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout
The last command lists the subject and issuer of each certificate in the bundle file. Checking whether the issuer of the preceding certificate matches the subject of the next one tells you whether the order is right.
Here is a summary of the parts of chain construction that are easy to confuse. What the server has to send is its own certificate plus every intermediate certificate needed to reach the root. The client already has the root certificate in its trust store, so there is no need to send it, and it is usually ignored if you do. Sending it does push unnecessary bytes on every handshake, which is why it is conventionally excluded.
There are also cases with multiple intermediates. Commercial CAs today sometimes use a two-level intermediate structure, so incidents happen where someone assembles the chain by hand instead of using the bundle attached to the issuance email and drops one certificate. If you assembled it, always verify afterwards.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted intermediate.crt server.crt
If this command prints OK, the chain is complete as judged against the local trust store. If it fails, the error message says at which step the chain broke.
6. Diagnosing against a live server — s_client
These are the commands for checking what the actual server sends.
openssl s_client -connect example.com:443 -servername example.com
openssl s_client -connect example.com:443 -servername example.com -showcerts
openssl s_client -connect example.com:443 -servername example.com -brief
openssl s_client -connect example.com:443 -servername example.com -tls1_2
openssl s_client -connect example.com:443 -servername example.com -status
openssl s_client -connect smtp.example.com:587 -starttls smtp
The option meanings are per the documentation.
-connectis the connection target.-servernameputs SNI in the ClientHello. In a virtual hosting environment, without this you get the wrong default certificate. Always include it when diagnosing.-showcertsshows the certificate list the server sent, exactly as sent. As the documentation states explicitly, this is not a verified chain but what the server sent verbatim. That makes it exactly right for confirming a missing intermediate.-statusrequests an OCSP stapling response.-briefprints only a connection summary.-starttlssends the protocol-specific upgrade message. It supports smtp, imap, pop3, ftp, postgres, mysql, ldap, and others.
The command sits waiting for input, so in a script close standard input.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates
This one line is the standard idiom for checking remote certificate expiry. Use it as-is when batch-checking several hosts.
for H in www.example.com api.example.com admin.example.com; do
printf '%s ' "$H"
echo | openssl s_client -connect "$H:443" -servername "$H" 2>/dev/null \
| openssl x509 -noout -enddate
done
Check the chain verification result in the verify-related lines near the top of the output. If you see unable to get local issuer certificate, either the intermediate is missing or the root is not in the local trust store.
7. Common errors and the order of diagnosis
| Symptom | Command to check | Likely cause |
|---|---|---|
| Works in the browser, fails with curl | s_client -showcerts | Missing intermediate |
| Only one domain fails | x509 -ext subjectAltName | Missing SAN |
| Server fails to start, key error | Compare modulus or pubkey digests | Key and certificate mismatch |
| Renewed but the old certificate shows | s_client -servername | Missing reload, wrong virtual host |
| Only old clients fail | s_client -tls1_2 | Protocol or cipher suite mismatch |
| Private CA certificate is rejected | verify -CAfile | CA not in the trust store |
| No alert before expiry | x509 -checkend | No monitoring |
The order of diagnosis is always the same. First, confirm what the file is. Second, confirm it pairs with the key. Third, confirm the chain is complete. Fourth, confirm what the server actually sends. Follow this order and the cause usually surfaces at step two or three.
The way to register a private CA into the system trust store also differs per distribution.
# RHEL family
sudo cp internal-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
# Debian family
sudo cp internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates
Caution: the required file extension differs per distribution. Debian-family systems require a .crt extension and PEM format. After registering, confirm that it is actually trusted.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1
The problem of a renewal not taking effect is also common. If you only replace the file and do not reload the service, the process keeps using the old certificate it loaded into memory. Comparing the file timestamp with the certificate actually being served identifies this immediately.
ls -l --time-style=long-iso /etc/pki/tls/certs/server.crt
openssl x509 -in /etc/pki/tls/certs/server.crt -noout -enddate
echo | openssl s_client -connect localhost:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -enddate
If the file expiry date and the expiry date of the certificate the server actually serves differ, the reload was skipped. If you have set up automatic renewal, always confirm that the renewal hook includes a service reload. There are plenty of real cases where the renewal succeeded, it was never picked up, and the service went down on expiry.
Another thing that is frequently forgotten is the fact that the trust store differs per application. Java uses its own keystore, Python uses the certifi bundle, and Node.js uses a built-in list. If you registered the CA with the system and only one application still fails, you have to check that runtime's trust store separately.
Quiz: check your understanding
Quiz 1: It works in the browser but only server-to-server API calls throw certificate errors. What do you check first?
Answer: Check whether the server also sends the intermediate certificate
Explanation: Browsers fill in the intermediate from cache or AIA information, but command-line tools and most language runtimes do not.
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
If it prints 1, the server is sending only its own certificate, which means the intermediate is missing. Concatenate the file for the server with the server certificate first and the intermediate after it, then reinstall.
Quiz 2: What is a key-type-agnostic way to confirm that a certificate and a private key are a pair?
Answer: Extract the public key from both sides and compare the digests
Explanation: Comparing -modulus only works for RSA. For ECDSA or Ed25519, use the following method.
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
If the two values match, they are a pair. This method does not care about key type, so if you memorize only one, memorize this one. It is also safer in that it never prints the private key contents to the screen.
Quiz 3: A server certificate made with a private CA is rejected by the browser for a domain mismatch. What was left out?
Answer: The SAN extension was not included at signing time
Explanation: Even if you put the SAN in the CSR, if you do not name the extension when signing with openssl x509 -req, the SAN does not end up in the resulting certificate.
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 397 -sha256 -extfile csr.cnf -extensions req_ext
Always confirm after signing.
openssl x509 -in server.crt -noout -ext subjectAltName
Modern clients ignore the CN and look only at the SAN.
Quiz 4: Several domains live on one IP. How do you check a specific domain's certificate with s_client?
Answer: Specify SNI with -servername
Explanation: Without SNI, the server returns the certificate of the default virtual host, so you end up looking at a certificate other than the one for the domain you meant to check.
echo | openssl s_client -connect 203.0.113.10:443 -servername api.example.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
According to the documentation, omitting -servername makes the hostname from -connect the default, so when you connect directly by IP you must state it explicitly.
Quiz 5: How do you automatically find certificates that expire within 30 days?
Answer: Pass a value in seconds to -checkend and judge by the exit code
Explanation: According to the documentation, -checkend checks whether the certificate expires within the given number of seconds. 30 days is 2592000 seconds.
for CRT in /etc/pki/tls/certs/*.crt; do
openssl x509 -in "$CRT" -noout -checkend 2592000 >/dev/null 2>&1 \
|| echo "EXPIRING: $CRT"
done
Remote servers can be checked the same way.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -checkend 2592000
Quiz 6: You registered the internal CA in the system trust store, but only the Java application still fails. Why?
Answer: Because the application runtime uses its own trust store
Explanation: Java uses its own keystore, Python uses the certifi bundle, and Node.js uses a built-in CA list. Updating the OS trust store does not reach those runtimes.
First confirm that things are fine at the system level.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt
curl -sSI https://internal.example.com | head -1
If curl works and only the application fails, the cause is confirmed. Check the exact procedure for registering a CA with each runtime in that runtime's documentation.
Closing
Certificate diagnosis is not hard as long as you keep to the order. What the file is, whether it pairs with the key, whether the chain is complete, and what the server actually sends. Check those four in sequence and the cause reveals itself.
And a command you cannot recall in a hurry is useless. Memorize at least these three blocks.
# 1. Inspect the local file
openssl x509 -in server.crt -noout -text
# 2. Check the remote server
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates
# 3. Check that the key and certificate match
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
These three blocks cover 90 percent of certificate-related work.
Finally, an outage caused by expiry is a 100 percent preventable incident. Put a check script built from a single -checkend line in place before the day is out.
References
- openssl-x509 official documentation (verified 2026-08-15)
- openssl-s_client official documentation (verified 2026-08-15)
- openssl-req official documentation (verified 2026-08-15)
- openssl-verify official documentation (verified 2026-08-15)
Further reading
- Previous: The Complete Guide to Backup and Restore
- Next: The Complete Guide to File Descriptors and Inodes
- The Complete Guide to SSL/TLS Certificates — the issuance and automatic renewal flow
- The Complete Guide to SSH Operations — key management problems in the same family
- curl builder — build curl commands with TLS options attached
- Linux terminal — practice openssl commands