<?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>Service Mesh on IAMDevBox</title><link>https://www.iamdevbox.com/tags/service-mesh/</link><description>Recent content in Service Mesh on IAMDevBox</description><image><title>IAMDevBox</title><url>https://www.iamdevbox.com/IAMDevBox.com.jpg</url><link>https://www.iamdevbox.com/IAMDevBox.com.jpg</link></image><generator>Hugo -- 0.146.0</generator><language>en-us</language><lastBuildDate>Mon, 25 May 2026 19:36:25 +0000</lastBuildDate><atom:link href="https://www.iamdevbox.com/tags/service-mesh/index.xml" rel="self" type="application/rss+xml"/><item><title>mTLS Certificate Authentication for Microservices in Kubernetes</title><link>https://www.iamdevbox.com/posts/mtls-certificate-authentication-microservices-kubernetes/</link><pubDate>Thu, 21 May 2026 20:30:00 +0000</pubDate><guid>https://www.iamdevbox.com/posts/mtls-certificate-authentication-microservices-kubernetes/</guid><description>mTLS certificate authentication for Kubernetes microservices — enable Istio STRICT mode, automate rotation with cert-manager, debug CERTIFICATE_VERIFY_FAILED errors, and implement SPIFFE/SPIRE workload identity for zero-trust service meshes.</description><content:encoded><![CDATA[<p>Microservices communicate over the network dozens or hundreds of times per second. Without mutual authentication, any compromised pod inside your cluster can impersonate a legitimate service, intercept traffic, or make unauthorized calls. mTLS (mutual TLS) closes this gap by requiring <em>both</em> ends of every connection to present a valid X.509 certificate — no certificate, no connection.</p>
<p>This guide covers mTLS from first principles through production deployment: how the handshake works, enabling it in Istio, automating certificate lifecycle with cert-manager, implementing SPIFFE/SPIRE workload identity, and debugging the errors you&rsquo;ll inevitably encounter.</p>
<h2 id="why-mtls-matters-for-zero-trust-kubernetes">Why mTLS Matters for Zero-Trust Kubernetes</h2>
<p>Traditional network security assumed that traffic inside the cluster perimeter was safe. Zero-trust inverts this: <strong>trust nothing, verify everything</strong>. mTLS is the cryptographic mechanism that enforces this at the transport layer.</p>
<p>Without mTLS, a compromised <code>frontend</code> pod can call <code>billing-service</code> APIs directly. With mTLS, the <code>billing-service</code> Envoy proxy rejects any connection whose client certificate was not issued by the cluster&rsquo;s trusted CA — even if the request comes from inside the cluster.</p>
<p>The practical benefits:</p>
<ul>
<li><strong>Workload identity</strong>: Certificates encode the service account identity (via SPIFFE ID), enabling policy decisions based on <em>who is calling</em>, not just what IP address is calling</li>
<li><strong>Encryption in transit</strong>: All inter-service traffic is encrypted end-to-end, including east-west traffic that never leaves the cluster</li>
<li><strong>Compliance</strong>: PCI-DSS 4.0 (Requirement 4), SOC 2 Type II, and HIPAA all require encryption of data in transit — mTLS satisfies this for internal APIs</li>
<li><strong>Audit trail</strong>: Certificate subject/issuer fields appear in access logs, providing cryptographic proof of which workload made each call</li>
</ul>
<h2 id="how-the-mtls-handshake-works">How the mTLS Handshake Works</h2>
<p>A regular TLS handshake has 3 steps: ClientHello → ServerHello+Certificate → Finished. mTLS adds one more:</p>
<ol>
<li>Client sends <code>ClientHello</code></li>
<li>Server responds with its certificate + a <code>CertificateRequest</code></li>
<li>Client sends <strong>its own certificate</strong> along with its <code>CertificateVerify</code> (a signature proving it holds the private key)</li>
<li>Both sides derive the session key and begin encrypted communication</li>
</ol>
<p>Both certificates must chain to a CA that the other side trusts. In Istio, this CA is Istiod, which acts as an internal PKI and issues certificates automatically to every Envoy sidecar.</p>
<p>The certificate encodes the workload&rsquo;s <strong>SPIFFE ID</strong> in the Subject Alternative Name (SAN) field:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>spiffe://cluster.local/ns/payments/sa/billing-service
</span></span></code></pre></div><p>This URI uniquely identifies the Kubernetes service account running the workload, enabling identity-based authorization policies.</p>
<h2 id="enabling-mtls-with-istio">Enabling mTLS with Istio</h2>
<p>Istio&rsquo;s service mesh uses Envoy sidecar proxies injected into every pod. These proxies handle mTLS transparently — your application code never manages certificates directly.</p>
<h3 id="step-1-verify-istio-is-installed">Step 1: Verify Istio is Installed</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>istioctl version
</span></span><span style="display:flex;"><span>kubectl get pods -n istio-system
</span></span></code></pre></div><p>Istiod must be running. It serves as the Certificate Authority (CA) that issues certificates to all sidecars.</p>
<h3 id="step-2-enable-sidecar-injection">Step 2: Enable Sidecar Injection</h3>
<p>Label your namespace to automatically inject Envoy sidecars:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl label namespace payments istio-injection<span style="color:#f92672">=</span>enabled
</span></span></code></pre></div><p>Restart existing deployments to inject sidecars:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl rollout restart deployment -n payments
</span></span></code></pre></div><h3 id="step-3-apply-peerauthentication-policy">Step 3: Apply PeerAuthentication Policy</h3>
<p>The <code>PeerAuthentication</code> resource controls whether mTLS is required. Start with <code>PERMISSIVE</code> (allows both mTLS and plain HTTP) during migration, then switch to <code>STRICT</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#75715e"># peer-auth-permissive.yaml — migration phase</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">security.istio.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">PeerAuthentication</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">payments</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mtls</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">PERMISSIVE</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#75715e"># peer-auth-strict.yaml — final enforcement</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">security.istio.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">PeerAuthentication</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">payments</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mtls</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">STRICT</span>
</span></span></code></pre></div><p>Apply with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f peer-auth-strict.yaml
</span></span></code></pre></div><h3 id="step-4-configure-destinationrule-for-outbound-traffic">Step 4: Configure DestinationRule for Outbound Traffic</h3>
<p>The <code>DestinationRule</code> tells Envoy to use mTLS when calling services. Without this, even if the server enforces mTLS, outbound connections may use plain HTTP:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.istio.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">DestinationRule</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">payments-mtls</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">payments</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">host</span>: <span style="color:#e6db74">&#34;*.payments.svc.cluster.local&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">trafficPolicy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tls</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">ISTIO_MUTUAL</span>
</span></span></code></pre></div><p><code>ISTIO_MUTUAL</code> instructs Envoy to use certificates issued by Istiod — no manual certificate management needed.</p>
<h3 id="step-5-verify-mtls-is-active">Step 5: Verify mTLS is Active</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Check mTLS status for a specific pod</span>
</span></span><span style="display:flex;"><span>istioctl x describe pod billing-service-7d9f6-xk2p3.payments
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Inspect the certificate Envoy is using</span>
</span></span><span style="display:flex;"><span>kubectl exec -it billing-service-7d9f6-xk2p3 -n payments -c istio-proxy -- <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  pilot-agent request GET certs/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Confirm traffic is encrypted (look for TLS handshake in Kiali or Grafana Istio dashboard)</span>
</span></span><span style="display:flex;"><span>istioctl dashboard kiali
</span></span></code></pre></div><h2 id="automating-certificate-rotation-with-cert-manager">Automating Certificate Rotation with cert-manager</h2>
<p>Istio&rsquo;s built-in CA (Istiod) handles certificate rotation for sidecar-to-sidecar mTLS automatically. But for services that need certificates outside the mesh — external load balancers, ingress TLS, job runners without sidecars — cert-manager is the standard solution.</p>
<h3 id="install-cert-manager">Install cert-manager</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
</span></span><span style="display:flex;"><span>kubectl wait --for<span style="color:#f92672">=</span>condition<span style="color:#f92672">=</span>Available deployment --all -n cert-manager --timeout<span style="color:#f92672">=</span>60s
</span></span></code></pre></div><h3 id="create-a-clusterissuer-using-internal-ca">Create a ClusterIssuer (using internal CA)</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">apiVersion</span>: <span style="color:#ae81ff">cert-manager.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterIssuer</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">internal-ca</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ca</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">secretName</span>: <span style="color:#ae81ff">ca-key-pair </span> <span style="color:#75715e"># Secret containing ca.crt and tls.key</span>
</span></span></code></pre></div><h3 id="issue-a-certificate-for-a-service">Issue a Certificate for a Service</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">apiVersion</span>: <span style="color:#ae81ff">cert-manager.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Certificate</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">billing-service-cert</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">payments</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">secretName</span>: <span style="color:#ae81ff">billing-service-tls</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">duration</span>: <span style="color:#ae81ff">24h</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">renewBefore</span>: <span style="color:#ae81ff">8h       </span> <span style="color:#75715e"># Renew 8 hours before expiry</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">subject</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">organizations</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">corp.example.com</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dnsNames</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">billing-service.payments.svc.cluster.local</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">billing-service.payments.svc</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uris</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">spiffe://cluster.local/ns/payments/sa/billing-service</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">issuerRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">internal-ca</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterIssuer</span>
</span></span></code></pre></div><p>The <code>spiffe://</code> URI in <code>uris</code> makes this certificate SPIFFE-compliant — it can participate in SPIFFE-aware identity verification alongside Istio-managed certificates.</p>
<p>Check certificate status:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl get certificate -n payments
</span></span><span style="display:flex;"><span><span style="color:#75715e"># NAME                    READY   SECRET                  AGE</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># billing-service-cert    True    billing-service-tls     2m</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>kubectl describe certificate billing-service-cert -n payments
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Events: Successfully issued certificate from ClusterIssuer &#34;internal-ca&#34;</span>
</span></span></code></pre></div><h2 id="spiffespire-federation-ready-workload-identity">SPIFFE/SPIRE: Federation-Ready Workload Identity</h2>
<p>SPIFFE (Secure Production Identity Framework For Everyone) solves a harder problem: <strong>how do services in different clusters, clouds, or data centers authenticate each other</strong> without sharing a common CA?</p>
<p>SPIRE (the SPIFFE Runtime Environment) is the reference implementation. It:</p>
<ol>
<li>Attests each workload&rsquo;s identity using platform evidence (Kubernetes node/pod metadata, AWS instance metadata, TPM attestation)</li>
<li>Issues short-lived X.509 SVIDs (SPIFFE Verifiable Identity Documents) to each workload</li>
<li>Federates trust across domains — a service in AWS us-east-1 can verify a certificate issued by a SPIRE server in GCP us-central1</li>
</ol>
<h3 id="deploy-spire-in-kubernetes">Deploy SPIRE in Kubernetes</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Clone SPIRE quickstart</span>
</span></span><span style="display:flex;"><span>git clone https://github.com/spiffe/spire-tutorials.git
</span></span><span style="display:flex;"><span>cd spire-tutorials/k8s/quickstart
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Deploy SPIRE server and agent</span>
</span></span><span style="display:flex;"><span>kubectl apply -f spire-namespace.yaml
</span></span><span style="display:flex;"><span>kubectl apply -f server-account.yaml server-cluster-role.yaml server-configmap.yaml server-statefulset.yaml server-service.yaml
</span></span><span style="display:flex;"><span>kubectl apply -f agent-account.yaml agent-cluster-role.yaml agent-configmap.yaml agent-daemonset.yaml
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Verify SPIRE server is running</span>
</span></span><span style="display:flex;"><span>kubectl get pods -n spire
</span></span></code></pre></div><h3 id="register-a-workload-entry">Register a Workload Entry</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Register billing-service with its Kubernetes service account</span>
</span></span><span style="display:flex;"><span>kubectl exec -n spire spire-server-0 -- <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  /opt/spire/bin/spire-server entry create <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -spiffeID spiffe://cluster.local/ns/payments/sa/billing-service <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -parentID spiffe://cluster.local/spire/agent/k8s_sat/payments/<span style="color:#66d9ef">$(</span>kubectl get node -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{.items[0].metadata.name}&#39;</span><span style="color:#66d9ef">)</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -selector k8s:ns:payments <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -selector k8s:sa:billing-service
</span></span></code></pre></div><p>SPIRE agents on each node deliver SVIDs to workloads via the SPIFFE Workload API (a Unix domain socket). Applications retrieve certificates programmatically without any manual secret management.</p>
<h2 id="implementing-authorizationpolicy-with-mtls-identity">Implementing AuthorizationPolicy with mTLS Identity</h2>
<p>Once mTLS is active and certificates carry SPIFFE IDs, you can write fine-grained authorization policies based on workload identity — not IP addresses, which are ephemeral in Kubernetes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">apiVersion</span>: <span style="color:#ae81ff">security.istio.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">AuthorizationPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">billing-service-policy</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">payments</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">billing-service</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">action</span>: <span style="color:#ae81ff">ALLOW</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">from</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">source</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">principals</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#75715e"># Only allow calls from checkout-service in the same namespace</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;cluster.local/ns/payments/sa/checkout-service&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">to</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">operation</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">methods</span>: [<span style="color:#e6db74">&#34;POST&#34;</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">paths</span>: [<span style="color:#e6db74">&#34;/api/v1/charge&#34;</span>]
</span></span></code></pre></div><p>This policy allows only the <code>checkout-service</code> service account to call <code>POST /api/v1/charge</code>. Any other workload — even inside the cluster — gets a 403. The decision is based on the cryptographic identity in the mTLS certificate, not on IP allowlists or network ACLs.</p>
<h2 id="debugging-mtls-certificate-errors">Debugging mTLS Certificate Errors</h2>
<h3 id="error-certificate_verify_failed">Error: CERTIFICATE_VERIFY_FAILED</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>SSL routines:ssl3_read_bytes:certificate verify failed
</span></span></code></pre></div><p><strong>Cause</strong>: The CA that signed the client certificate is not in the server&rsquo;s trust bundle.</p>
<p><strong>Fix</strong>: Verify both sides use the same CA root:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Get the CA cert Istio is using</span>
</span></span><span style="display:flex;"><span>kubectl get configmap istio-ca-root-cert -n istio-system -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{.data.root-cert\.pem}&#39;</span> | openssl x509 -text -noout | grep Issuer
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Verify the client certificate was signed by the same CA</span>
</span></span><span style="display:flex;"><span>openssl verify -CAfile ca.crt client.crt
</span></span></code></pre></div><h3 id="error-upstream-connect-error-reset-reason-connection-termination">Error: upstream connect error, reset reason: connection termination</h3>
<p><strong>Cause</strong>: STRICT mTLS policy is blocking a client that doesn&rsquo;t have a sidecar (e.g., a curl from a debug pod).</p>
<p><strong>Fix</strong>: Either inject a sidecar into the debug pod, or add a port-level exception:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-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:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mtls</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">STRICT</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">portLevelMtls</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">9090</span>:                <span style="color:#75715e"># Health check port</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">DISABLE</span>
</span></span></code></pre></div><h3 id="error-ssl_error_rx_record_too_long">Error: SSL_ERROR_RX_RECORD_TOO_LONG</h3>
<p><strong>Cause</strong>: The server is expecting TLS but the client sent plain HTTP (or vice versa).</p>
<p><strong>Fix</strong>: Check if DestinationRule has the correct TLS mode:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl get destinationrule -A -o yaml | grep -A <span style="color:#ae81ff">10</span> tls
</span></span></code></pre></div><h3 id="general-debug-workflow">General Debug Workflow</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. Check Envoy proxy logs for TLS errors</span>
</span></span><span style="display:flex;"><span>kubectl logs &lt;pod-name&gt; -c istio-proxy | grep -i <span style="color:#e6db74">&#34;tls\|cert\|handshake&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. Get full mTLS status for a pod</span>
</span></span><span style="display:flex;"><span>istioctl x describe pod &lt;pod-name&gt;.&lt;namespace&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. Inspect what certificates Envoy holds</span>
</span></span><span style="display:flex;"><span>kubectl exec -it &lt;pod-name&gt; -n &lt;namespace&gt; -c istio-proxy -- <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  curl -s localhost:15000/certs | python3 -m json.tool
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4. Check cluster-level TLS configuration</span>
</span></span><span style="display:flex;"><span>istioctl proxy-config cluster &lt;pod-name&gt;.&lt;namespace&gt; --fqdn billing-service.payments.svc.cluster.local
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 5. Test TLS handshake from a debug pod</span>
</span></span><span style="display:flex;"><span>kubectl run debug --image<span style="color:#f92672">=</span>nicolaka/netshoot -it --rm -- <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  openssl s_client -connect billing-service.payments.svc.cluster.local:8080 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -cert /tmp/client.crt -key /tmp/client.key -CAfile /tmp/ca.crt
</span></span></code></pre></div><h2 id="certificate-lifecycle-best-practices">Certificate Lifecycle Best Practices</h2>
<table>
  <thead>
      <tr>
          <th>Practice</th>
          <th>Implementation</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Short-lived certs</td>
          <td>24-72 hour TTL with cert-manager or SPIRE</td>
          <td>Limits blast radius if private key is compromised</td>
      </tr>
      <tr>
          <td>Automated rotation</td>
          <td>cert-manager <code>renewBefore</code> = 1/3 of duration</td>
          <td>Prevents expiry-induced outages</td>
      </tr>
      <tr>
          <td>No wildcard certs</td>
          <td>One cert per service</td>
          <td>Wildcard compromise affects all services</td>
      </tr>
      <tr>
          <td>SPIFFE SAN</td>
          <td><code>spiffe://cluster.local/ns/&lt;ns&gt;/sa/&lt;sa&gt;</code> in SAN</td>
          <td>Enables cryptographic workload identity</td>
      </tr>
      <tr>
          <td>Separate CAs per cluster</td>
          <td>Federation via SPIFFE bundle endpoint</td>
          <td>Breach of one cluster&rsquo;s CA doesn&rsquo;t compromise others</td>
      </tr>
      <tr>
          <td>CRL/OCSP</td>
          <td>Vault PKI with CRL endpoints</td>
          <td>Enables immediate revocation of compromised certs</td>
      </tr>
  </tbody>
</table>
<h2 id="production-rollout-checklist">Production Rollout Checklist</h2>
<p>Before switching to <code>STRICT</code> mTLS, validate each step in a staging environment:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. Confirm all pods have sidecars injected</span>
</span></span><span style="display:flex;"><span>kubectl get pods -n payments -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{range .items[*]}{.metadata.name}{&#34;\t&#34;}{.spec.containers[*].name}{&#34;\n&#34;}{end}&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. Check no services are using host networking (bypasses Envoy)</span>
</span></span><span style="display:flex;"><span>kubectl get pods -n payments -o jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;{range .items[?(@.spec.hostNetwork==true)]}{.metadata.name}{&#34;\n&#34;}{end}&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. Verify no hardcoded IP connections (these bypass service discovery and mTLS)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Review application configs for direct IP references</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4. Test with PERMISSIVE first, monitor for errors, then switch to STRICT</span>
</span></span><span style="display:flex;"><span>kubectl apply -f peer-auth-strict.yaml
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 5. Monitor Istio metrics for TLS handshake failures</span>
</span></span><span style="display:flex;"><span>kubectl exec -it &lt;pod&gt; -c istio-proxy -- curl -s localhost:15090/stats | grep ssl.handshake
</span></span></code></pre></div><h2 id="internal-linking">Internal Linking</h2>
<p>For token-based authentication in your APIs alongside mTLS, see the <a href="/posts/client-credentials-flow-in-oauth-20-complete-guide-with-real-world-examples/">Client Credentials Flow in OAuth 2.0</a> guide — mTLS client authentication (<code>token_endpoint_auth_method: tls_client_auth</code>) is a supported OAuth 2.0 client authentication method (RFC 8705) and eliminates the need for client secrets entirely.</p>
<p>For workload identity that spans beyond Kubernetes into AWS, GCP, and Azure, see our guide on <a href="/posts/workload-identity-federation-aws-gcp-azure-oidc-kubernetes/">Workload Identity Federation</a> — mTLS certificates can serve as the attestation mechanism in cross-cloud identity federation.</p>
<p>For the Kubernetes security layer above mTLS, the <a href="/posts/understanding-kubernetes-networking-a-comprehensive-guide/">Kubernetes Service Account Security</a> article covers projected tokens and IRSA patterns that complement mTLS-based service authentication.</p>
]]></content:encoded></item></channel></rss>