인증서 실무
인증서가 무엇을 확인해 주는지 종류별로 정리하고, ACME로 도메인을 검증받아 발급과 갱신을 자동화하는 방법과 짧아지는 유효 기간에 맞춘 갱신 계획을 계산합니다. 여러 서버에 배포한 인증서를 확인하는 방법, 개발용 인증서와 SNI, ECH, 서로를 확인하는 mTLS, 폐기와 키 유출 대응도 함께 봅니다.
앞 글에서 브라우저가 인증서의 이름과 사슬을 검증하는 과정을 보았습니다. 이 글에서는 반대편, 곧 서버를 운영하는 쪽에서 인증서를 발급받고 갱신하고 배포하는 일을 다룹니다. 인증서 장애의 대부분은 암호의 문제가 아니라 운영의 문제입니다. 갱신을 잊어 만료되거나, 새 인증서를 일부 서버에만 올리거나, 중간 인증서를 빠뜨리는 일이 해마다 큰 서비스의 장애로 이어집니다.
인증서가 확인해 주는 것
공개 웹에서 쓰는 인증서는 CA가 무엇을 확인하고 발급했는지에 따라 나뉩니다. 도메인 검증(domain validation, DV) 인증서는 신청자가 그 도메인을 실제로 다룰 수 있는지만 확인합니다. 조직 검증(OV)과 확장 검증(EV) 인증서는 회사의 실재 여부까지 서류로 확인하지만, 지금의 브라우저는 주소창에서 이를 따로 표시하지 않으므로 사용자가 보기에는 차이가 거의 없습니다. 그래서 대부분의 서비스가 자동으로 발급할 수 있는 DV 인증서를 씁니다. DV 인증서는 "이 서버가 이 도메인의 주인이 운영하는 서버"라는 것까지만 보증하며, 그 회사가 믿을 만하다는 뜻은 아닙니다.
ACME로 도메인을 검증받는다
ACME(automatic certificate management environment, RFC 8555)는 도메인 검증부터 발급, 갱신, 폐기까지를 사람의 손 없이 처리하는 프로토콜입니다. 무료 CA인 Let's Encrypt가 이 방식으로 HTTPS를 크게 넓혔고, 지금은 여러 CA가 ACME를 지원합니다. 클라이언트가 인증서를 주문하면 CA는 "이 도메인을 정말 다룰 수 있다면 이것을 해 보라"는 도전(challenge)을 내고, 클라이언트가 그것을 해내면 인증서를 발급합니다.
| 도전 방식 | 해야 하는 일 | 쓰는 곳 |
|---|---|---|
| HTTP-01 | http://도메인/.well-known/acme-challenge/ 아래에 정해진 파일을 올린다 | 웹 서버가 80번 포트로 열려 있는 일반적인 사이트 |
| DNS-01 | _acme-challenge.도메인에 정해진 TXT 레코드를 넣는다 | 와일드카드 인증서, 바깥에서 접속할 수 없는 내부 서버 |
| TLS-ALPN-01 | 443번 포트의 TLS 핸드셰이크로 정해진 인증서를 보여 준다 | 80번 포트를 열 수 없는 환경 |
가장 널리 쓰는 ACME 클라이언트인 certbot은 다음처럼 씁니다.
sudo certbot --nginx -d example.com -d www.example.com # 발급받고 nginx 설정까지 바꾼다
sudo certbot certificates # 가진 인증서와 만료일
sudo certbot renew --dry-run # 시험용 CA로 갱신 과정을 시험한다
systemctl list-timers | grep certbot # 자동 갱신 타이머가 있는지
sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d "example.com"--dry-run은 시험용 CA로 갱신 과정을 끝까지 해 보고 결과를 운영용 파일로 저장하지 않는다는 뜻이지, 서버에 아무것도 하지 않는다는 뜻은 아닙니다. 도전을 통과하려고 웹 서버 설정을 잠시 바꾸고 다시 불러올 수 있습니다(certbot 문서). 마지막 명령처럼 DNS-01을 손으로 하는 방식은 TXT 레코드를 사람이 넣어야 하므로, DNS 사업자의 API를 쓰는 플러그인 없이는 자동으로 갱신되지 않습니다.
짧아지는 유효 기간과 갱신 계획
인증서의 유효 기간은 계속 짧아지고 있습니다. 2026년 9월 기준으로 Let's Encrypt의 기본 프로필은 90일, tlsserver 프로필은 45일, shortlived 프로필은 약 6일이며, 공개된 일정에 따라 기본 프로필도 2027년 2월 10일부터 64일, 2028년 2월 16일부터 45일로 줄어듭니다. 공개 인증서의 최대 유효 기간 자체도 업계의 합의로 단계적으로 줄어 2029년에는 47일이 됩니다. 키가 유출되거나 잘못 발급되었을 때 피해가 이어지는 기간을 줄이기 위해서인데, 그만큼 갱신은 반드시 자동이어야 합니다.
흔히 쓰는 기준은 수명의 3분의 1이 남았을 때 갱신하는 것입니다. 다음 코드는 이 기준과 갱신 작업이 도는 주기로 실제 갱신일을 계산합니다.
from datetime import date, timedelta
def renewal_plan(issued, lifetime_days, check_every_days):
expires = issued + timedelta(days=lifetime_days)
renew_from = expires - timedelta(days=lifetime_days // 3) # 수명의 3분의 1이 남으면 갱신한다
day = issued
while day < renew_from: # 갱신 작업이 도는 날에만 갱신할 수 있다
day += timedelta(days=check_every_days)
return expires, renew_from, day, (expires - day).days
for lifetime, every in [(90, 1), (45, 1), (45, 60)]:
expires, renew_from, renewed, margin = renewal_plan(date(2026, 11, 1), lifetime, every)
print(f"수명 {lifetime}일, {every}일마다 확인: 만료 {expires}, 갱신 시작 {renew_from}, "
f"실제 갱신 {renewed}, 남은 여유 {margin}일")수명 90일, 1일마다 확인: 만료 2027-01-30, 갱신 시작 2026-12-31, 실제 갱신 2026-12-31, 남은 여유 30일
수명 45일, 1일마다 확인: 만료 2026-12-16, 갱신 시작 2026-12-01, 실제 갱신 2026-12-01, 남은 여유 15일
수명 45일, 60일마다 확인: 만료 2026-12-16, 갱신 시작 2026-12-01, 실제 갱신 2026-12-31, 남은 여유 -15일매일 확인한다면 90일 인증서는 30일, 45일 인증서는 15일의 여유를 두고 갱신합니다. 이 여유는 갱신이 실패했을 때 고칠 시간입니다. 그런데 90일 인증서 시절에 맞춰 두 달마다 도는 작업을 그대로 두면, 45일 인증서는 갱신하기도 전에 15일이 지나 만료됩니다. 유효 기간이 바뀌면 갱신 작업의 주기와 실패 알림을 함께 점검해야 하고, ACME의 갱신 정보 확장(ACME Renewal Information, ARI)을 지원하는 클라이언트라면 CA가 권하는 시점에 맞춰 갱신하게 할 수 있습니다.
발급과 배포는 다른 일이다
인증서 파일을 새로 받았다고 해서 사용자가 새 인증서를 받는 것은 아닙니다. 웹 서버가 새 파일을 다시 읽어야 하고, 서버가 여러 대라면 모든 서버에 파일이 가야 하며, 로드 밸런서나 CDN이 TLS를 끝낸다면 그곳의 인증서도 바꿔야 합니다.
인증서 발급과 실제 서비스 적용
ACME로 도메인 제어를 확인합니다. 외부 접점에서 실제 제공본의 이름·체인·만료를 확인합니다. 각 단계의 값과 전환 이유를 비교합니다.
인증서 운영
갱신 성공 뒤에, 실제 서비스의 인증서를 확인합니다
ACME 검증과 발급은 시작입니다. 서비스에 배포하고 설정을 반영한 뒤, 외부에서 접속하여 실제 제공되는 인증서의 이름·체인·만료일을 확인합니다.
갱신 명령의 성공과 모든 서버의 적용 완료는 다릅니다. 실제 엔드포인트의 만료일·체인·호스트 이름을 확인하고, 실패 알림은 인증서가 만료되기 전에 전달되게 합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 ACME 검증부터 제공본 확인까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | 인증서 발급과 실제 서비스 적용 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| ACME 검증 ACME로 도메인 제어를 확인합니다. |
|
|
| 발급 키와 새 인증서 파일을 보관합니다. |
|
|
| 배포 각 TLS 종단에서 설정 검사와 reload를 수행합니다. |
|
|
| 제공본 확인 외부 접점에서 실제 제공본의 이름·체인·만료를 확인합니다. |
|
|
인증서 발급과 실제 서비스 적용
ACME로 도메인 제어를 확인합니다. 외부 접점에서 실제 제공본의 이름·체인·만료를 확인합니다. 각 단계의 값과 전환 이유를 비교합니다.
인증서 운영
갱신 성공 뒤에, 실제 서비스의 인증서를 확인합니다
ACME 검증과 발급은 시작입니다. 서비스에 배포하고 설정을 반영한 뒤, 외부에서 접속하여 실제 제공되는 인증서의 이름·체인·만료일을 확인합니다.
갱신 명령의 성공과 모든 서버의 적용 완료는 다릅니다. 실제 엔드포인트의 만료일·체인·호스트 이름을 확인하고, 실패 알림은 인증서가 만료되기 전에 전달되게 합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 ACME 검증부터 제공본 확인까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | 인증서 발급과 실제 서비스 적용 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| ACME 검증 ACME로 도메인 제어를 확인합니다. |
|
|
| 발급 키와 새 인증서 파일을 보관합니다. |
|
|
| 배포 각 TLS 종단에서 설정 검사와 reload를 수행합니다. |
|
|
| 제공본 확인 외부 접점에서 실제 제공본의 이름·체인·만료를 확인합니다. |
|
|
certbot의 --deploy-hook 'systemctl reload nginx'처럼 갱신에 성공했을 때 웹 서버를 다시 불러오는 동작을 함께 걸어 두는 것이 기본입니다. 그리고 배포가 끝났는지는 바깥에서 실제로 접속해 확인합니다. 서버가 여러 대라면 대표 주소 하나로는 부족하므로, 각 서버의 주소로 직접 접속해 보내 주는 인증서를 확인합니다.
for ip in 192.0.2.11 192.0.2.12; do
echo | openssl s_client -connect "$ip:443" -servername example.com 2>/dev/null \
| openssl x509 -noout -serial -enddate
done두 서버의 일련번호(serial)와 만료일이 같아야 둘 다 새 인증서를 보내는 것입니다. 한 대만 다시 불러왔다면 사용자에 따라 새 인증서와 만료 직전의 인증서를 번갈아 받게 되고, 예전 인증서가 만료되는 날 일부 사용자만 접속하지 못하는 장애가 됩니다. AWS ACM이나 Cloudflare처럼 TLS를 끝내는 관리형 서비스에 인증서를 맡기면 발급과 갱신, 배포를 서비스가 대신하지만, DNS 검증 레코드를 지우면 갱신이 실패하므로 그 레코드는 계속 남겨 두어야 합니다.
개발용 인증서
개발 중인 컴퓨터에서 HTTPS를 시험하려면 인증서가 필요하지만, localhost에 공개 CA의 인증서를 받을 수는 없습니다. 이때는 자기가 자기에게 서명한 자체 서명 인증서(self-signed certificate)를 만들거나, 내 컴퓨터에만 설치한 로컬 CA로 인증서를 발급합니다.
openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" # 자체 서명 인증서
mkcert -install # 로컬 CA를 신뢰 저장소에 설치(처음 한 번)
mkcert localhost 127.0.0.1 ::1 # 로컬 CA가 서명한 인증서자체 서명 인증서는 어느 신뢰 저장소에도 없으므로 브라우저가 경고를 띄웁니다. mkcert는 로컬 CA를 내 컴퓨터의 신뢰 저장소에 넣어 두므로 경고 없이 쓸 수 있지만, 그 CA의 개인 키를 가진 사람은 내 컴퓨터가 믿는 어떤 사이트의 인증서든 만들 수 있으므로 키 파일을 남에게 주면 안 됩니다. -nodes는 개인 키 파일을 암호화하지 않는다는 뜻이어서 파일의 접근 권한도 막아 두어야 합니다. 어느 방식이든 인증서에 SAN을 꼭 넣어야 합니다. 브라우저는 CN을 보지 않기 때문입니다.
SNI와 ECH
IP 주소 하나로 여러 도메인을 운영하는 서버는 핸드셰이크를 시작할 때 어느 도메인의 인증서를 보낼지 알아야 합니다. 그래서 클라이언트는 ClientHello에 접속하려는 도메인 이름을 담는데, 이것이 SNI(server name indication)입니다. SNI는 서버가 인증서를 고르는 데 쓰는 값일 뿐이고, 클라이언트는 받은 인증서의 이름을 여전히 직접 검증해야 합니다.
SNI는 암호화되기 전의 ClientHello에 들어 있으므로, 경로 위의 누구나 사용자가 어느 도메인에 접속하는지 볼 수 있습니다. ECH(encrypted client hello, RFC 9849)는 진짜 ClientHello를 서버의 공개 키로 암호화해 바깥의 ClientHello 안에 넣는 방식으로 이를 숨깁니다. 클라이언트는 그 공개 키를 DNS의 HTTPS 레코드로 받아 오므로, DNS 질의가 평문이라면 그 단계에서 이름이 드러납니다. ECH를 쓰더라도 IP 주소와 데이터의 양은 숨겨지지 않습니다.
서로를 확인하는 mTLS
보통의 HTTPS에서는 서버만 인증서를 보여 줍니다. mTLS(mutual TLS)는 클라이언트도 인증서를 보여 주어, 서버가 접속한 상대가 누구인지 TLS 단계에서 확인합니다. 서비스 메시에서 서비스끼리 서로를 확인하거나, 금융 API에서 제휴사를 확인하거나, 기기마다 인증서를 넣은 IoT 장비를 확인할 때 씁니다.
openssl req -x509 -newkey rsa:4096 -nodes -keyout ca.key -out ca.crt -days 3650 -subj "/CN=My Internal CA"
openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=service-a"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365
curl --cert client.crt --key client.key --cacert ca.crt https://internal-api.example.com/health앞의 세 줄은 내부 CA를 만들고 그 CA로 service-a의 클라이언트 인증서를 발급합니다. 마지막 줄의 --cert와 --key는 클라이언트가 보여 줄 인증서와 그 개인 키이고, --cacert는 거꾸로 서버의 인증서를 검증할 때 믿을 CA입니다. 서버 쪽에서도 클라이언트 인증서를 요구하고 이 내부 CA를 믿도록 설정해야 mTLS가 이루어집니다.
mTLS가 확인해 주는 것은 상대가 service-a라는 것까지입니다. service-a가 데이터를 지워도 되는지는 별도의 권한 정책으로 판단해야 합니다. 서비스가 많아지면 인증서를 발급하고 바꾸는 일 자체가 큰 운영 부담이 되므로, Kubernetes의 cert-manager, HashiCorp Vault의 PKI 기능, 서비스의 신원을 표준화한 SPIFFE와 그 구현인 SPIRE 같은 도구로 자동화합니다.
폐기와 키 유출
개인 키가 유출되면 새 키로 인증서를 다시 발급받아 바꾸고, 예전 인증서는 CA에 폐기(revocation)를 요청합니다. 폐기된 인증서의 목록은 CA가 CRL(certificate revocation list)로 공개하고, 브라우저는 이를 모아 배포하는 방식으로 확인합니다. 인증서마다 실시간으로 묻는 OCSP라는 방식도 있었지만, 사용자가 어느 사이트에 접속하는지 CA에 알려지는 문제와 응답 지연 때문에 줄어들고 있어, Let's Encrypt는 2025년에 OCSP를 끝내고 CRL만 제공하기로 했습니다. 폐기가 모든 클라이언트에 바로 전해지지는 않으므로, 결국 키를 잘 지키고 유효 기간을 짧게 두는 것이 가장 확실한 방어입니다.
더 깊이: 내 도메인의 인증서를 누가 발급했는지 아는 방법
폐기는 문제가 된 인증서를 알고 있을 때만 쓸 수 있습니다. 어느 CA가 실수나 침해로 내 도메인의 인증서를 남에게 발급했다면, 그 사실을 알아내는 장치가 따로 있어야 합니다. 인증서 투명성(certificate transparency, CT)은 공개 인증서를 누구나 볼 수 있는 로그에 기록하게 해서, 도메인의 주인이 자기 이름으로 발급된 인증서를 찾아볼 수 있게 합니다. RFC 9162에 따르면 로그는 머클 트리(Merkle tree)로 만들어져, 예전에 기록한 항목을 몰래 지우거나 바꾸지 않았는지 누구나 확인할 수 있습니다.
로그는 인증서를 받으면 그것을 기록하겠다고 약속하는 서명된 인증서 타임스탬프(signed certificate timestamp, SCT)를 돌려주고, CA는 보통 이를 인증서 안에 넣어 발급합니다. Chrome의 CT 정책은 CT를 강제하는 버전에서 공개적으로 신뢰받는 모든 TLS 인증서가 이 정책을 만족해야 검증을 통과한다고 정하며, 인증서에 넣은 SCT라면 수명이 180일 이하일 때 두 로그, 그보다 길 때 세 로그의 것을 요구합니다. 그래서 공개 CA가 로그에 남기지 않고 몰래 발급한 인증서는 이런 브라우저에서 쓸 수 없고, 운영자는 CT 로그를 검색하는 도구로 내 도메인에 예상하지 못한 인증서가 발급되지 않았는지 지켜볼 수 있습니다. RFC 9162는 RFC 6962를 대체한 CT 2.0이며, 둘 다 실험(Experimental) 단계의 문서입니다.
정리
- DV 인증서는 도메인을 다룰 수 있다는 것만 확인하며, OV와 EV도 지금의 브라우저에서는 겉보기에 차이가 거의 없습니다.
- ACME는 HTTP-01, DNS-01, TLS-ALPN-01 같은 도전으로 도메인을 확인하고 발급과 갱신을 자동화하며, 와일드카드 인증서에는 DNS-01이 필요합니다.
- 인증서의 유효 기간은 계속 짧아지므로, 수명의 3분의 1이 남았을 때 갱신하는 식의 기준과 그보다 자주 도는 갱신 작업, 실패 알림을 함께 둡니다.
- 발급과 배포는 다른 일이어서, 갱신 뒤 웹 서버를 다시 불러오고 서버마다 실제로 보내는 인증서의 일련번호와 만료일을 바깥에서 확인합니다.
- 개발용 인증서는 자체 서명이나 로컬 CA로 만들고 SAN을 넣으며, SNI는 평문으로 드러나 ECH로 숨길 수 있습니다.
- mTLS는 클라이언트의 신원을 확인할 뿐 권한은 따로 판단해야 하고, 키가 유출되면 새 키로 바꾸고 예전 인증서를 폐기합니다.
연습 문제
1. 한 대만 다시 불러온 서버
같은 서비스를 맡은 서버 A와 B가 있습니다. 인증서 파일의 갱신은 둘 다 성공했지만 A만 웹 서버를 다시 불러왔습니다. 사용자는 어떤 인증서를 받게 되나요? 갱신 명령이 성공했다는 기록만으로 배포가 끝났다고 판단할 수 있나요?
해설 보기
A에 연결된 사용자는 새 인증서를, B에 연결된 사용자는 예전 인증서를 받습니다. 웹 서버는 시작할 때 읽어 둔 인증서를 계속 쓰기 때문입니다. 지금은 둘 다 유효하니 문제가 보이지 않지만, 예전 인증서가 만료되는 날 B로 가는 사용자만 접속하지 못합니다. 갱신 명령의 성공은 파일을 받았다는 뜻일 뿐이므로, 각 서버의 주소로 openssl s_client -servername을 보내 일련번호와 만료일이 새 인증서와 같은지 확인해야 합니다. 로드 밸런서가 TLS를 끝낸다면 그곳의 인증서도 따로 확인합니다.
2. 20일마다 도는 갱신 작업
본문의 renewal_plan으로 2026년 11월 1일에 발급한 45일짜리 인증서를 20일마다 도는 작업으로 갱신하면, 실제 갱신일과 남은 여유는 얼마인가요? 그날 갱신이 실패한다면 어떻게 되나요?
해설 보기
만료일은 12월 16일이고, 수명의 3분의 1인 15일을 뺀 12월 1일부터 갱신할 수 있습니다. 작업은 11월 21일, 12월 11일에 도는데 11월 21일은 갱신 시작일보다 이르므로 건너뛰고, 12월 11일에 갱신합니다. 남은 여유는 5일입니다. 그날 CA의 장애나 DNS 설정 오류로 갱신이 실패하면 다음 작업은 12월 31일에야 돌므로, 그 전인 12월 16일에 인증서가 만료됩니다. 작업을 매일 돌게 하고, 갱신이 실패하면 바로 알림이 오게 해 두어야 여유를 실제로 쓸 수 있습니다.
3. 확인된 서비스와 그 권한
내부 API 서버가 mTLS로 service-a의 클라이언트 인증서를 내부 CA까지 검증했습니다. service-a에게 데이터 삭제 권한을 준 적이 없다면, mTLS에 성공했다는 이유로 삭제 요청을 처리해도 되나요? curl 예에서 --cacert를 빼면 무엇이 달라지나요?
해설 보기
처리하면 안 됩니다. mTLS는 상대가 service-a라는 것을 확인했을 뿐이고, service-a가 무엇을 해도 되는지는 서비스마다 허락한 작업을 정해 둔 권한 정책으로 따로 판단해야 합니다. 인증서의 이름을 서비스의 역할과 이어 주는 설정이 그 사이에 있어야 합니다. --cacert를 빼면 curl은 서버의 인증서를 운영체제의 기본 신뢰 저장소로 검증하려 하므로, 내부 CA가 발급한 서버 인증서를 믿지 못해 연결이 실패합니다. --cacert는 클라이언트 인증서와는 상관없는, 서버를 검증하는 방향의 설정입니다.
다음 글 네트워크 보안 위협과 방어에서는 스니핑과 스푸핑, 중간자 공격, DDoS 같은 네트워크의 대표적인 공격과 이를 막는 방어를 정리합니다.