<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Rotation on jyukki's Blog</title><link>https://jyukki.com/tags/rotation/</link><description>Recent content in Rotation on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Mon, 06 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/rotation/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Certificate Lifecycle과 mTLS Rotation 운영 플레이북</title><link>https://jyukki.com/learning/deep-dive/deep-dive-certificate-lifecycle-rotation-playbook/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-certificate-lifecycle-rotation-playbook/</guid><description>TLS 인증서와 mTLS 클라이언트 인증서를 단순 설정 파일이 아니라 발급, 배포, 교체, 폐기, 알람을 가진 운영 자산으로 관리하는 기준을 정리합니다.</description><content:encoded><![CDATA[<p>TLS 인증서는 처음 붙일 때보다 <strong>갱신하고 교체할 때</strong> 더 자주 장애를 만듭니다. 도메인 인증서가 만료되어 사용자가 브라우저 경고를 보고, 중간 인증서 체인이 빠져 일부 클라이언트만 실패하고, mTLS 클라이언트 인증서가 회전되지 않아 파트너 API 호출이 전부 막히는 식입니다. 기능 개발 중에는 잘 보이지 않지만, 만료일이 오면 한 번에 서비스 가용성 문제가 됩니다.</p>
<p>그래서 인증서는 &ldquo;보안팀이 발급해 준 파일&quot;이 아니라 운영 자산으로 봐야 합니다. 누가 소유하고, 언제 만료되고, 어떤 시스템이 신뢰하고, 어떤 순서로 새 인증서를 배포하고, 실패하면 어떻게 되돌릴지까지 계약이 있어야 합니다. 이 글은 <a href="/learning/deep-dive/deep-dive-tls-handshake/">TLS Handshake 1.3</a>, <a href="/learning/deep-dive/deep-dive-secret-management/">비밀 관리</a>, <a href="/learning/deep-dive/deep-dive-api-key-lifecycle-rotation-revocation-playbook/">API Key Lifecycle</a>, <a href="/learning/deep-dive/deep-dive-service-mesh-istio/">Service Mesh Istio</a>를 인증서 수명주기 운영 관점으로 묶어 정리합니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>서버 TLS 인증서와 mTLS 클라이언트 인증서를 운영 자산으로 관리하는 기준을 잡을 수 있습니다.</li>
<li>만료 알람, 자동 갱신, 신뢰 번들 교체, dual trust, rollback window를 숫자로 설계할 수 있습니다.</li>
<li>인증서 장애를 handshake 실패, issuer 문제, SAN 불일치, old CA 잔존, private key 노출 관점으로 분류할 수 있습니다.</li>
<li>cert-manager, ACM, Vault PKI, service mesh를 쓸 때도 반드시 남겨야 할 소유권·검증·감사 항목을 정리할 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-인증서는-세-종류의-수명주기가-다르다">1) 인증서는 세 종류의 수명주기가 다르다</h3>
<p>운영에서 &ldquo;인증서 갱신&quot;이라고 말하지만 실제로는 최소 세 가지가 섞입니다.</p>
<table>
  <thead>
      <tr>
          <th>대상</th>
          <th>역할</th>
          <th>흔한 장애</th>
          <th>운영 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>서버 leaf 인증서</td>
          <td>클라이언트가 서버를 신뢰</td>
          <td>만료, SAN 누락, 체인 누락</td>
          <td>자동 갱신 + 배포 검증</td>
      </tr>
      <tr>
          <td>클라이언트 leaf 인증서</td>
          <td>mTLS에서 호출자를 증명</td>
          <td>파트너/서비스 인증 실패</td>
          <td>issuer·subject·scope 기록</td>
      </tr>
      <tr>
          <td>CA/trust bundle</td>
          <td>어떤 issuer를 믿을지 결정</td>
          <td>새 CA 미신뢰, old CA 조기 제거</td>
          <td>dual trust window 필요</td>
      </tr>
  </tbody>
</table>
<p>서버 인증서는 비교적 단순합니다. 새 인증서를 발급하고 로드밸런서, ingress, 프록시, 앱 서버에 배포하면 됩니다. 반면 mTLS는 양쪽이 서로 신뢰해야 합니다. 호출하는 쪽은 client cert를 제시하고, 받는 쪽은 그 cert를 서명한 CA를 trust bundle에서 확인합니다. 따라서 CA를 바꾸려면 새 leaf만 배포해서는 안 됩니다. 먼저 모든 검증자에게 새 CA를 믿게 하고, 그다음 새 leaf를 발급해야 합니다.</p>
<p>실무 기본 순서는 아래처럼 둡니다.</p>
<ol>
<li>새 CA 또는 새 issuer를 trust bundle에 추가</li>
<li>모든 서버/프록시가 새 CA를 신뢰하는지 배포 확인</li>
<li>새 leaf 인증서 발급 및 일부 클라이언트 canary</li>
<li>handshake 실패율과 issuer 분포 확인</li>
<li>old CA로 발급된 leaf가 0건 또는 허용치 이하인지 확인</li>
<li>old CA를 trust bundle에서 제거</li>
</ol>
<p>이 순서를 거꾸로 하면 일부 서비스가 새 인증서를 &ldquo;모르는 발급자&quot;로 보고 연결을 거부합니다.</p>
<h3 id="2-만료일-알람만으로는-부족하다">2) 만료일 알람만으로는 부족하다</h3>
<p>인증서 운영에서 가장 흔한 대응은 &ldquo;만료 며칠 전 알람&quot;입니다. 필요하지만 충분하지 않습니다. 자동 갱신이 실패했는데 만료 알람만 울리면 이미 시간이 부족할 수 있고, 인증서는 갱신됐지만 런타임이 reload하지 못하면 실제 트래픽은 여전히 old cert를 씁니다.</p>
<p>최소 지표는 아래처럼 나눠야 합니다.</p>
<ul>
<li><code>cert_expiry_days</code>: 인증서별 남은 일수</li>
<li><code>cert_renewal_failure_count</code>: 발급/갱신 실패 횟수</li>
<li><code>cert_deploy_success_rate</code>: 발급 후 대상 런타임 반영 성공률</li>
<li><code>tls_handshake_failure_rate</code>: 클라이언트 관점 TLS 실패율</li>
<li><code>mtls_auth_reject_rate</code>: 인증서 검증 실패율</li>
<li><code>cert_issuer_distribution</code>: old/new issuer 사용 비율</li>
<li><code>trust_bundle_version</code>: 각 서비스가 로드한 신뢰 번들 버전</li>
</ul>
<p>초기 알람 기준은 보수적으로 잡습니다.</p>
<table>
  <thead>
      <tr>
          <th>신호</th>
          <th style="text-align: right">Warning</th>
          <th style="text-align: right">Critical</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>public server cert 만료</td>
          <td style="text-align: right">30일 이하</td>
          <td style="text-align: right">7일 이하</td>
      </tr>
      <tr>
          <td>internal mTLS leaf 만료</td>
          <td style="text-align: right">14일 이하</td>
          <td style="text-align: right">3일 이하</td>
      </tr>
      <tr>
          <td>CA 만료</td>
          <td style="text-align: right">180일 이하</td>
          <td style="text-align: right">90일 이하</td>
      </tr>
      <tr>
          <td>자동 갱신 실패</td>
          <td style="text-align: right">1회</td>
          <td style="text-align: right">3회 또는 30분 지속</td>
      </tr>
      <tr>
          <td>handshake 실패율</td>
          <td style="text-align: right">평시 대비 2배</td>
          <td style="text-align: right">1% 이상 5분 지속</td>
      </tr>
      <tr>
          <td>old issuer 잔존</td>
          <td style="text-align: right">계획 대비 지연</td>
          <td style="text-align: right">제거 예정 24시간 전 5% 이상</td>
      </tr>
  </tbody>
</table>
<p>CA 만료는 leaf보다 훨씬 일찍 봐야 합니다. leaf 인증서는 며칠 안에도 바꿀 수 있지만, CA나 trust bundle은 모든 클라이언트와 서버에 퍼져야 하므로 90일도 짧을 수 있습니다.</p>
<h3 id="3-private-key는-인증서보다-더-강하게-보호해야-한다">3) private key는 인증서보다 더 강하게 보호해야 한다</h3>
<p>인증서는 공개돼도 됩니다. 문제는 private key입니다. leaf 인증서 회전은 가용성 이슈지만, private key 유출은 신뢰 경계 침해입니다. 특히 wildcard 인증서, 내부 gateway 인증서, 파트너 mTLS key가 유출되면 공격자는 정상 주체처럼 보일 수 있습니다.</p>
<p>권장 원칙:</p>
<ul>
<li>private key는 가능하면 HSM, KMS, Vault, cloud certificate manager 안에서 생성하고 export하지 않는다.</li>
<li>export가 필요한 경우 접근 권한은 break-glass 수준으로 제한하고 모든 조회를 감사 로그에 남긴다.</li>
<li>wildcard 인증서는 사용 범위를 좁히고, 서비스별 인증서 또는 SAN별 분리를 우선한다.</li>
<li>key file 권한은 런타임 사용자만 읽게 하고, image·repo·CI artifact에 포함하지 않는다.</li>
<li>private key 노출 의심 시 leaf revoke, trust 경계 점검, 관련 secret rotation을 같은 incident로 처리한다.</li>
</ul>
<p>이 부분은 <a href="/learning/deep-dive/deep-dive-secret-management/">비밀 관리</a>와 같습니다. 인증서 파일을 Kubernetes Secret에 넣는 순간 보안이 끝나는 것이 아닙니다. 누가 그 Secret을 읽을 수 있고, 로그·백업·디버그 덤프에 남는지까지 봐야 합니다.</p>
<h3 id="4-hot-reload가-없으면-회전은-배포-장애가-된다">4) hot reload가 없으면 회전은 배포 장애가 된다</h3>
<p>인증서가 갱신되어도 프로세스가 새 파일을 읽지 않으면 의미가 없습니다. Nginx, Envoy, HAProxy, Java 앱, gRPC client, message broker마다 reload 방식이 다릅니다. 어떤 런타임은 파일 변경을 감지하고, 어떤 런타임은 SIGHUP이 필요하며, 어떤 앱은 재시작해야 합니다.</p>
<p>운영 기준:</p>
<ul>
<li>public ingress/LB: 인증서 교체 후 5분 안에 외부 probe가 새 serial number 확인</li>
<li>internal service: sidecar 또는 client pool이 10분 안에 새 cert/trust bundle 반영</li>
<li>reload 실패 시: 트래픽 차단 전 old cert 유효기간이 최소 24시간 이상 남아 있어야 함</li>
<li>인증서 교체는 배포 freeze와 별개로 긴급 수행 가능해야 함</li>
</ul>
<p>hot reload가 없다면 회전은 일반 배포와 같은 위험을 갖습니다. rolling restart, connection drain, readiness, rollback을 함께 설계해야 합니다. 이 기준은 <a href="/learning/deep-dive/deep-dive-drain-aware-deployment-playbook/">무중단 배포와 드레인</a>과 연결됩니다.</p>
<h3 id="5-mtls는-인증과-인가를-혼동하면-안-된다">5) mTLS는 인증과 인가를 혼동하면 안 된다</h3>
<p>mTLS는 &ldquo;이 호출자가 어떤 인증서를 가진 주체인가&quot;를 증명합니다. 하지만 그 주체가 어떤 API를 호출할 수 있는지는 별도 인가 문제입니다. <code>client cert valid</code>만 보고 모든 endpoint를 열면, 내부 서비스 하나가 탈취됐을 때 권한이 과하게 넓어집니다.</p>
<p>실무에서는 certificate identity를 service account나 workload identity와 연결하고, endpoint별 authorization policy를 별도로 둡니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">mtls_identity_policy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">subject</span>: <span style="color:#f1fa8c">&#34;spiffe://prod/payment-api&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">allowed_audience</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;order-api&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">allowed_methods</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;GET /internal/orders/{id}&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;POST /internal/payments/authorize&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">denied_methods</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;POST /admin/refund/manual&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">max_cert_ttl</span>: <span style="color:#f1fa8c">&#34;24h&#34;</span>
</span></span></code></pre></div><p>Service mesh가 mTLS를 자동화해도 이 경계는 사라지지 않습니다. mesh는 인증서 발급과 sidecar 간 암호화를 쉽게 만들지만, 어떤 서비스가 어떤 리소스에 접근해도 되는지까지 자동으로 맞춰 주지는 않습니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-certificate-inventory를-만든다">1) certificate inventory를 만든다</h3>
<p>첫 단계는 자동화보다 목록입니다. 아래 필드는 최소로 남깁니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">certificate_inventory</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">name</span>: api.example.com
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">type</span>: public_server_tls
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">owner</span>: platform-edge
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">issuer</span>: letsencrypt
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">domains</span>:
</span></span><span style="display:flex;"><span>    - api.example.com
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">deployed_to</span>:
</span></span><span style="display:flex;"><span>    - cloudfront
</span></span><span style="display:flex;"><span>    - api-ingress-prod
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">expiry</span>: 2026-09-12
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">renewal_method</span>: cert-manager
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">reload_method</span>: ingress-controller-auto
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">alert_channels</span>:
</span></span><span style="display:flex;"><span>    - oncall-platform
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">rollback</span>: <span style="color:#f1fa8c">&#34;previous cert valid until 2026-08-20&#34;</span>
</span></span></code></pre></div><p>내부 mTLS는 <code>subject</code>, <code>spiffe_id</code>, <code>trust_bundle_version</code>, <code>max_ttl</code>, <code>consumer_services</code>를 추가합니다. 파트너 API라면 <code>partner_contact</code>, <code>test_endpoint</code>, <code>rotation_notice_days</code>도 필요합니다.</p>
<h3 id="2-rotation-window를-짧은-인증서와-긴-ca로-나눠-설계한다">2) rotation window를 짧은 인증서와 긴 CA로 나눠 설계한다</h3>
<p>권장 기본값:</p>
<ul>
<li>public leaf: TTL 60~90일, 만료 30일 전 자동 갱신 시작</li>
<li>internal mTLS leaf: TTL 24시간~7일, 자동 발급/교체 필수</li>
<li>partner client cert: TTL 90<del>365일, 만료 30</del>60일 전 양측 검증</li>
<li>root/intermediate CA: TTL 1~5년, 만료 180일 전 migration plan 시작</li>
</ul>
<p>단명 인증서는 보안성이 좋지만 자동화가 약하면 장애를 빨리 만듭니다. 24시간 TTL을 쓰려면 발급 서비스, sidecar reload, clock sync, retry, 알람이 충분히 성숙해야 합니다. 처음부터 짧게 줄이기보다 30일, 7일, 24시간 순으로 내려가는 편이 안전합니다.</p>
<h3 id="3-ca-교체-runbook">3) CA 교체 runbook</h3>
<p>mTLS CA 교체는 leaf 교체보다 신중하게 다룹니다.</p>
<ol>
<li>새 CA 발급 및 fingerprint 기록</li>
<li>trust bundle v2 생성: old CA + new CA</li>
<li>전체 server verifier에 trust bundle v2 배포</li>
<li>canary client 1~5%에 new CA leaf 발급</li>
<li><code>mtls_auth_reject_rate</code>, issuer 분포, endpoint별 실패 확인</li>
<li>전체 client leaf를 new CA로 교체</li>
<li>old CA leaf 잔존율이 0.1% 이하이고 24시간 이상 안정이면 trust bundle v3에서 old CA 제거</li>
<li>old CA private key 폐기 또는 offline archive 처리</li>
</ol>
<p>핵심은 dual trust 기간을 너무 짧게 잡지 않는 것입니다. 모바일 앱, 파트너, 오래 떠 있는 JVM client, 장기 연결 gRPC가 있으면 old leaf가 예상보다 오래 남습니다.</p>
<h3 id="4-운영-대시보드">4) 운영 대시보드</h3>
<p>인증서 대시보드는 만료일 목록만 보여주면 부족합니다.</p>
<ul>
<li>만료 30일 이내 인증서 목록</li>
<li>issuer/CA 버전별 active connection 분포</li>
<li>endpoint별 TLS handshake 실패율</li>
<li>mTLS reject reason: expired, unknown_ca, san_mismatch, revoked, clock_skew</li>
<li>자동 갱신 job 성공률과 마지막 성공 시각</li>
<li>trust bundle 버전별 서비스 수</li>
<li>reload 후 serial number probe 성공률</li>
</ul>
<p>알람 우선순위는 <strong>private key 노출 의심 &gt; CA/trust bundle 오류 &gt; public cert 만료 &gt; mTLS leaf 만료 &gt; reload 지연</strong> 순으로 둡니다. 사용자가 보는 장애와 내부 신뢰 경계 사고를 먼저 봐야 합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, 자동 갱신은 필수지만 자동 배포는 검증 없이는 위험합니다. 인증서는 발급됐지만 SAN이 빠졌거나 chain이 달라 일부 구형 클라이언트가 실패할 수 있습니다. 자동 배포 후 외부 probe, 내부 mTLS probe, serial number 확인을 붙여야 합니다.</p>
<p>둘째, wildcard 인증서는 편하지만 blast radius가 큽니다. 한 key로 여러 하위 도메인을 보호하면 운영은 단순해지지만 유출 시 영향 범위가 커집니다. public edge에는 제한적으로 쓸 수 있어도 내부 서비스 간 mTLS에는 서비스별 identity를 권장합니다.</p>
<p>셋째, mTLS를 켜면 네트워크 보안은 좋아지지만 디버깅 난이도가 올라갑니다. &ldquo;연결 안 됨&quot;이 DNS, routing, TLS version, CA, SAN, clock skew, authorization policy 중 어디서 난 것인지 분리해야 합니다. reject reason을 로그와 metric에 남기지 않으면 온콜은 감으로 추적하게 됩니다.</p>
<p>넷째, certificate pinning은 신중해야 합니다. 모바일 앱이나 파트너 SDK가 특정 leaf를 pin하면 서버 인증서 회전이 배포 문제로 바뀝니다. pinning이 필요하다면 leaf가 아니라 intermediate 또는 public key pinning을 검토하고, backup pin과 emergency rotation 절차를 문서화해야 합니다.</p>
<p>다섯째, CA 교체는 보통 기술 문제가 아니라 coordination 문제입니다. 내부 팀, 파트너, 모바일 앱, 오래 떠 있는 배치 작업이 모두 같은 날 바뀌지 않습니다. 신뢰 번들 dual period, 호환성 테스트, old issuer 잔존 측정을 먼저 준비해야 합니다.</p>
<p>의사결정 우선순위는 <strong>신뢰 경계 유지 &gt; 사용자 연결 가용성 &gt; 자동화 단순성 &gt; 인증서 비용</strong>입니다. 갱신 비용을 줄이려고 회전 절차를 느슨하게 두면, 만료 한 번이 전체 장애가 됩니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 모든 public/internal 인증서에 owner, issuer, expiry, deploy target이 있다.</li>
<li><input disabled="" type="checkbox"> 만료 알람과 자동 갱신 실패 알람이 분리되어 있다.</li>
<li><input disabled="" type="checkbox"> 인증서 교체 후 serial number와 chain을 외부 probe로 확인한다.</li>
<li><input disabled="" type="checkbox"> private key가 repo, container image, CI artifact에 포함되지 않는지 검사한다.</li>
<li><input disabled="" type="checkbox"> mTLS trust bundle은 버전 관리되고 서비스별 로드 버전을 볼 수 있다.</li>
<li><input disabled="" type="checkbox"> CA 교체에는 dual trust window와 old issuer 잔존율 기준이 있다.</li>
<li><input disabled="" type="checkbox"> handshake failure reason을 expired, unknown CA, SAN mismatch, revoked, clock skew로 분리한다.</li>
<li><input disabled="" type="checkbox"> partner client cert 회전은 최소 30일 전 test endpoint로 검증한다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>운영 중인 TLS 인증서 10개를 골라 owner, 만료일, issuer, 배포 위치, reload 방식, rollback 가능 여부를 표로 만들어 보세요. 한 항목이라도 모르면 inventory가 아직 부족합니다.</li>
<li>내부 mTLS CA를 90일 뒤 교체해야 한다고 가정하고, dual trust 배포부터 old CA 제거까지 8단계 runbook을 작성해 보세요.</li>
<li>인증서 만료 장애가 발생했을 때 10분 안에 확인할 명령과 대시보드 지표를 적어 보세요. <code>openssl s_client</code>, ingress cert serial, mTLS reject metric, trust bundle version이 빠지면 보완합니다.</li>
</ol>
<h2 id="다음-글">다음 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-tls-handshake/">TLS Handshake 1.3</a></li>
<li><a href="/learning/deep-dive/deep-dive-secret-management/">비밀 관리</a></li>
<li><a href="/learning/deep-dive/deep-dive-api-key-lifecycle-rotation-revocation-playbook/">API Key Lifecycle</a></li>
<li><a href="/learning/deep-dive/deep-dive-service-mesh-istio/">Service Mesh Istio</a></li>
</ul>
]]></content:encoded></item></channel></rss>