<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://kushalkrishnappa.github.io/blogs/</id>
    <title>Kushal Krishnappa — Writing Blog</title>
    <updated>2026-07-31T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://kushalkrishnappa.github.io/blogs/"/>
    <subtitle>Kushal Krishnappa — Writing Blog</subtitle>
    <icon>https://kushalkrishnappa.github.io/blogs/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Deploying a Self-Hosted Temporal Cluster with mTLS, Auto-Renewing Certs and an SSO-Protected UI]]></title>
        <id>https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls</id>
        <link href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls"/>
        <updated>2026-07-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Standing up Temporal on a single EC2 box: workers authenticated over mutual TLS with self-renewing certs, an SSO-protected dashboard and exactly one inbound port open.]]></summary>
        <content type="html"><![CDATA[<p>This is the story of standing up a production-ish <a href="https://temporal.io/" target="_blank" rel="noopener noreferrer" class="">Temporal</a> cluster on a single EC2 box — where the workers authenticate over mutual TLS with certificates that renew themselves and the web dashboard sits behind single sign-on, all with exactly <strong>one</strong> inbound port open to the internet.</p>
<p>It's also a story about the wrong turns, because the wrong turns are where the actual learning is. If you're setting up something similar, the dead-ends below will save you an afternoon.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-set-out-to-build">What I set out to build<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#what-i-set-out-to-build" class="hash-link" aria-label="Direct link to What I set out to build" title="Direct link to What I set out to build" translate="no">​</a></h2>
<p>Three machines already sit on a Tailscale mesh for unrelated reasons. The goal was narrow:</p>
<ul>
<li class="">Run a Temporal cluster on <strong>one</strong> of those nodes.</li>
<li class="">Let workers connect from the other nodes — or from anywhere off the mesh entirely.</li>
<li class="">Keep it secure without a lot of moving parts.</li>
</ul>
<p>The workers being able to live <em>anywhere</em>, mesh or not, turned out to be the constraint that drove every subsequent decision.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="understanding-what-temporal-server-actually-is">Understanding what "Temporal server" actually is<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#understanding-what-temporal-server-actually-is" class="hash-link" aria-label="Direct link to Understanding what &quot;Temporal server&quot; actually is" title="Direct link to Understanding what &quot;Temporal server&quot; actually is" translate="no">​</a></h2>
<p>Before any of this made sense, I had to fix a mental model. I thought "server," "frontend" and "Postgres" were three separate components. They're not.</p>
<p>The <strong>Temporal Server</strong> is a single program containing four internal roles: Frontend, History, Matching and an internal Worker. Of those, <strong>only the Frontend is reachable from outside</strong> — it's the gRPC API on port <code>7233</code>. The other three are internal plumbing.</p>
<p>And crucially: <strong>"Frontend" does not mean the web UI.</strong> That naming collision trips everyone up. In Temporal-speak, the Frontend is the API door. The dashboard you look at in a browser is a <em>separate</em> program (the <strong>Web UI</strong>, port <code>8080</code>) that is itself just another client of the Frontend.</p>
<p>So the real component list is:</p>
<ol>
<li class=""><strong>Temporal Server</strong> — one program; its only external door is the Frontend (gRPC, <code>7233</code>).</li>
<li class=""><strong>Web UI</strong> — a separate program (HTTP, <code>8080</code>) that also talks to the Frontend behind the scenes.</li>
<li class=""><strong>PostgreSQL</strong> — the database (<code>5432</code>).</li>
</ol>
<p>The payoff of understanding this: there's really only <strong>one door to secure</strong> — the Frontend on <code>7233</code>. Everything else is either internal or a browser app.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dead-end-1-exposing-grpc-through-a-cloudflare-tunnel">Dead end #1: exposing gRPC through a Cloudflare Tunnel<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#dead-end-1-exposing-grpc-through-a-cloudflare-tunnel" class="hash-link" aria-label="Direct link to Dead end #1: exposing gRPC through a Cloudflare Tunnel" title="Direct link to Dead end #1: exposing gRPC through a Cloudflare Tunnel" translate="no">​</a></h2>
<p>My first instinct for the "workers from anywhere" problem was a Cloudflare Tunnel with an Access login policy. It seemed clean: no open ports, SSO in front.</p>
<p>It doesn't work for the Frontend, for two independent reasons:</p>
<ol>
<li class=""><strong>"Application with login" is interactive browser SSO.</strong> Workers are headless processes — they can't complete an OAuth redirect. For non-interactive clients Cloudflare uses <em>service tokens</em>, not login.</li>
<li class=""><strong>Cloudflare doesn't proxy gRPC over a public hostname.</strong> Per their own docs, gRPC over Tunnel is only supported via private-network routing, not public-hostname ingress.</li>
</ol>
<p>The workable Cloudflare paths for gRPC (<code>cloudflared access tcp</code> with a service token on each worker, or WARP + private routing) all boil down to <em>"install a Cloudflare client on every worker."</em> Which is the same operational footprint as just putting the worker on Tailscale — except it's a second overlay to manage.</p>
<p><strong>Lesson:</strong> Cloudflare Tunnel + Access login is perfect for the <em>browser UI</em>. It's the wrong tool for a gRPC API consumed by machines.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-worker-connectivity-decision-mtls-for-everyone">The worker connectivity decision: mTLS for everyone<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#the-worker-connectivity-decision-mtls-for-everyone" class="hash-link" aria-label="Direct link to The worker connectivity decision: mTLS for everyone" title="Direct link to The worker connectivity decision: mTLS for everyone" translate="no">​</a></h2>
<p>That left two real options for worker → Frontend:</p>
<ul>
<li class=""><strong>Tailscale:</strong> workers join the mesh, connect in plaintext, encryption and access control handled by WireGuard. Simplest — <em>for me</em>. But it costs the worker operator more: they have to install a daemon and become a node on my private network.</li>
<li class=""><strong>mTLS:</strong> hand each worker a certificate. Works from anywhere on the internet, no network entanglement. Costs <em>me</em> more: I run a small certificate authority.</li>
</ul>
<p>I chose <strong>mTLS for all workers</strong> and the reasoning is the useful part: <em>a cert is just a file; network membership is a relationship.</em> For third parties running workers, a cert is the more professional, decoupled interface. And one uniform path beats maintaining "mesh workers here, cert workers there."</p>
<p>The honest catch: mTLS doesn't remove overhead, it moves it to you. You're now responsible for issuing certs, <strong>rotating them before they expire</strong> (the 2am outage waiting to happen) and exposing the Frontend port (mTLS-gated, but reachable). The rotation problem is the one that bites — which is why the CA choice mattered.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="mtls-in-one-paragraph">mTLS in one paragraph<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#mtls-in-one-paragraph" class="hash-link" aria-label="Direct link to mTLS in one paragraph" title="Direct link to mTLS in one paragraph" translate="no">​</a></h3>
<p>Normal TLS: the server proves its identity (the padlock). Mutual TLS: <em>both</em> sides prove identity. The worker proves it's a legit worker; the server proves it's the real server. Since Temporal's Frontend has no password, the <strong>certificate is the password</strong> — the server only admits connections presenting a cert signed by a CA it trusts. Think keycard system: the <strong>CA</strong> is the keycard printer, the server checks cards at the door, each worker carries a card.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="infisical-as-the-certificate-authority">Infisical as the certificate authority<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#infisical-as-the-certificate-authority" class="hash-link" aria-label="Direct link to Infisical as the certificate authority" title="Direct link to Infisical as the certificate authority" translate="no">​</a></h2>
<p>Rather than hand-rolling a CA with <code>openssl</code> (fine to learn once, miserable to operate), I used <a href="https://infisical.com/" target="_blank" rel="noopener noreferrer" class="">Infisical</a>, which has a full private-PKI product: private CA hierarchies, lifecycle management and — the important bit — <strong>automated renewal with expiry alerts</strong>. It's open-source and has a free cloud tier.</p>
<p>The setup order in Infisical's Certificate Manager is layered and the UI does not make this obvious:</p>
<ol>
<li class=""><strong>Certificate Authority</strong> — the thing that signs. (I created a single Root CA; for a small deployment you don't need the textbook root + intermediate split, especially since Infisical holds the keys either way.)</li>
<li class=""><strong>Certificate Policy</strong> — the rules (I left it permissive).</li>
<li class=""><strong>Certificate Profile</strong> — bundles a CA + policy into a reusable template.</li>
<li class=""><strong>Application</strong> — a workload that issues certs through a profile.</li>
</ol>
<p>The gotcha: an "Application" is <em>not</em> a CA and it's the <em>last</em> step, not the first. I created one first (out of order) and left it empty until the end.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-one-profile-trick">The one profile trick<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#the-one-profile-trick" class="hash-link" aria-label="Direct link to The one profile trick" title="Direct link to The one profile trick" translate="no">​</a></h3>
<p>A certificate can carry <code>serverAuth</code>, <code>clientAuth</code> or both. Instead of two profiles, I made <strong>one mTLS profile with both</strong> extended key usages. That single template issues my server cert <em>and</em> my worker certs — they differ only by the name on them.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-san-gotcha">The SAN gotcha<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#the-san-gotcha" class="hash-link" aria-label="Direct link to The SAN gotcha" title="Direct link to The SAN gotcha" translate="no">​</a></h3>
<p>This is the single most common thing that breaks Temporal mTLS. The server certificate's Subject Alternative Names must match what clients dial. Temporal also does IP-to-IP internal comms, so you pin a logical <code>serverName</code> and put it in the cert as a SAN. My server cert ended up with four SANs:</p>
<ul>
<li class=""><code>temporal.kaplabs.dev</code> — what external workers dial</li>
<li class=""><code>temporal-frontend</code> — the internal name for the local UI/services</li>
<li class=""><code>localhost</code></li>
<li class=""><code>127.0.0.1</code></li>
</ul>
<p>Get this wrong and you get <code>x509: certificate is valid for X, not Y</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dead-end-2-avoided-native-mtls-in-the-all-in-one-image">Dead end #2 (avoided): native mTLS in the all-in-one image<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#dead-end-2-avoided-native-mtls-in-the-all-in-one-image" class="hash-link" aria-label="Direct link to Dead end #2 (avoided): native mTLS in the all-in-one image" title="Direct link to Dead end #2 (avoided): native mTLS in the all-in-one image" translate="no">​</a></h2>
<p>Temporal's <code>auto-setup</code> Docker image <em>can</em> do mTLS via <code>TEMPORAL_TLS_*</code> environment variables. But turning on <code>requireClientAuth</code> means the image's <strong>own internal health checks and namespace setup</strong> also have to speak TLS and getting that right is genuinely fiddly.</p>
<p>So I sidestepped it entirely with a cleaner architecture:</p>
<blockquote>
<p><strong>Temporal runs plaintext, locked to the internal Docker network. A tiny nginx sits in front doing the mTLS.</strong></p>
</blockquote>
<p>nginx terminates the worker's mutual-TLS handshake using the Infisical certs, then forwards plain gRPC to Temporal over the private network. Workers get full mTLS; Temporal stays vanilla and boots cleanly; the local UI talks to it with zero cert config. The only publicly exposed thing is nginx on <code>7233</code> and it rejects anyone without a valid client cert at the handshake.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-stack">The stack<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#the-stack" class="hash-link" aria-label="Direct link to The stack" title="Direct link to The stack" translate="no">​</a></h3>
<p><code>docker-compose.yml</code>:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">services</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">postgresql</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> postgres</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">16</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">alpine</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">environment</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">POSTGRES_USER</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> temporal</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">POSTGRES_PASSWORD</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> temporal   </span><span class="token comment" style="color:#999988;font-style:italic"># change this</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">POSTGRES_DB</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> temporal</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">volumes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> temporal</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">pgdata</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">/var/lib/postgresql/data</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">healthcheck</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">test</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"CMD-SHELL"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"pg_isready -U temporal"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">interval</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 5s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 5s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">retries</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">restart</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> unless</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">stopped</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">temporal</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> temporalio/auto</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">setup</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">1.27.2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">depends_on</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">postgresql</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">condition</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> service_healthy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">environment</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> DB=postgres12</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> DB_PORT=5432</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> POSTGRES_SEEDS=postgresql</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> POSTGRES_USER=temporal</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> POSTGRES_PWD=temporal</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> NUM_HISTORY_SHARDS=512</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> BIND_ON_IP=0.0.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> TEMPORAL_BROADCAST_ADDRESS=127.0.0.1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> SKIP_ADD_CUSTOM_SEARCH_ATTRIBUTES=true</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># No ports published. Reachable only inside the network as temporal:7233.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">restart</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> unless</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">stopped</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">temporal-ui</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> temporalio/ui</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">2.34.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">depends_on</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> temporal</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">environment</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> TEMPORAL_ADDRESS=temporal</span><span class="token punctuation" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">7233</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> TEMPORAL_UI_PORT=8080</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">ports</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"127.0.0.1:8080:8080"</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># localhost only; cloudflared picks this up</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">restart</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> unless</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">stopped</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">nginx</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> nginx</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">1.27</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">alpine</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">depends_on</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> temporal</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">volumes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> ./nginx.conf</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">/etc/nginx/nginx.conf</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">ro</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> /etc/temporal/certs</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">/certs</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">ro</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">ports</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"7233:7233"</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># the ONLY public port; mTLS-gated</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">restart</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> unless</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">stopped</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">volumes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  temporal</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">pgdata</span><span class="token punctuation" style="color:#393A34">:</span><br></div></code></pre></div></div>
<p><code>nginx.conf</code> — the mTLS terminator:</p>
<div class="language-nginx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-nginx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">worker_processes auto;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">events { worker_connections 1024; }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">http {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    server {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        listen 7233 ssl;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        http2 on;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        server_name temporal.kaplabs.dev;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ssl_certificate     /certs/server.pem;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ssl_certificate_key /certs/server.key;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        # The "m" in mTLS: require + verify client certs against our CA.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ssl_client_certificate /certs/ca.pem;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ssl_verify_client on;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ssl_verify_depth 2;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ssl_protocols TLSv1.2 TLSv1.3;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        # Temporal workers hold long-poll gRPC streams open. Let them.</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        grpc_read_timeout 3600s;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        grpc_send_timeout 3600s;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        location / {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            grpc_pass grpc://temporal:7233;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<p>Three files on the host under <code>/etc/temporal/certs/</code>: <code>server.pem</code>, <code>server.key</code>, <code>ca.pem</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-gotchas-that-actually-cost-time">The gotchas that actually cost time<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#the-gotchas-that-actually-cost-time" class="hash-link" aria-label="Direct link to The gotchas that actually cost time" title="Direct link to The gotchas that actually cost time" translate="no">​</a></h2>
<p>The configs above are clean in hindsight. Getting there involved a gauntlet:</p>
<p><strong>The Cloudflare orange-cloud trap.</strong> After adding the <code>temporal.kaplabs.dev</code> A record, <code>nc</code> timed out — and the resolved IP was <code>172.67.x.x</code>, a Cloudflare proxy address. The record was <strong>proxied (orange cloud)</strong>, so gRPC traffic was hitting Cloudflare's proxy, which doesn't handle it. Fix: flip it to <strong>grey cloud (DNS only)</strong> so it resolves to the real EC2 IP. (Note the contrast with the UI later, which <em>should</em> be orange.)</p>
<p><strong>The OOM freeze.</strong> First <code>docker compose up</code> on a 1 GB instance exhausted memory, thrashed and froze the entire OS — which took Tailscale SSH down with it. The box looked dead but wasn't; a Stop/Start from the EC2 console recovered it. <strong>This stack wants at least 2 GB</strong> (<code>t3.small</code> floor, <code>t3.medium</code> comfortable). A stop/start also changes the public IP unless you attach an Elastic IP — worth doing to avoid re-chasing DNS.</p>
<p><strong>The scary-but-harmless errors.</strong> During startup, the logs spat <code>context deadline exceeded</code> and <code>shard status unknown</code>. These <em>look</em> like failures but are startup churn — Temporal acquiring its 512 shards while Postgres warms up. If they taper off within a couple minutes, ignore them. If they never stop, the box is still starved.</p>
<p><strong>Docker permissions.</strong> <code>permission denied ... docker.sock</code> — the user wasn't in the <code>docker</code> group. <code>usermod -aG docker $USER</code> fixes it, but <strong>only in a new login session</strong>; the current shell keeps the old groups until you reconnect.</p>
<p><strong>Certs weren't on the box.</strong> nginx crash-looped with <code>cannot load certificate "/certs/server.pem"</code> — the files had been downloaded to a laptop, not copied to the server. Over Tailscale, <code>tailscale file cp</code> moved them across without needing the public IP or any open port. (Also: Infisical hands you the private key as a <code>.txt</code> — the extension is cosmetic; what matters is the <code>-----BEGIN PRIVATE KEY-----</code> inside.)</p>
<p><strong>cloudflared install is two steps, not one.</strong> The dashboard shows three commands. Step 1 installs the binary; step 2 registers the service with a token. I ran step 2 without step 1, so no service got created (<code>Unit cloudflared.service could not be found</code>). They're sequential, not alternatives.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dns-and-the-firewall-one-port">DNS and the firewall: one port<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#dns-and-the-firewall-one-port" class="hash-link" aria-label="Direct link to DNS and the firewall: one port" title="Direct link to DNS and the firewall: one port" translate="no">​</a></h2>
<p>DNS: <code>temporal.kaplabs.dev</code> → EC2 public IP, <strong>grey cloud</strong> (direct, for gRPC).</p>
<p>AWS security group <strong>inbound rules</strong>:</p>
<table><thead><tr><th>Type</th><th>Port</th><th>Source</th><th>Why</th></tr></thead><tbody><tr><td>Custom TCP</td><td>7233</td><td>0.0.0.0/0</td><td>Workers (mTLS-gated)</td></tr></tbody></table>
<p>That's the entire inbound surface. <code>0.0.0.0/0</code> is acceptable <em>because of mTLS</em> — an open port just means "anyone can attempt a handshake and fail." No SSH rule (Tailscale SSH is outbound), no 8080 rule (the UI goes out through a tunnel), no 5432 (Postgres is internal). Keep <strong>outbound</strong> allow-all, since Tailscale, cloudflared and image pulls all dial out.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="auto-renewal-the-infisical-agent">Auto-renewal: the Infisical agent<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#auto-renewal-the-infisical-agent" class="hash-link" aria-label="Direct link to Auto-renewal: the Infisical agent" title="Direct link to Auto-renewal: the Infisical agent" translate="no">​</a></h2>
<p>The whole reason for choosing Infisical. A small daemon runs on the VM, watches the server cert and ~30 days before the 90-day cert expires, reissues a fresh one, writes it to disk and <strong>gracefully reloads nginx</strong> via a post-hook. You never touch a cert again.</p>
<p>Key design decision: the agent manages <strong>only</strong> <code>server.pem</code> and <code>server.key</code> (the rotating pieces). It leaves <code>ca.pem</code> alone, because the Root CA doesn't rotate (10-year validity) — which also sidesteps any chain-format guesswork.</p>
<p><code>agent.yaml</code> (the parts that matter):</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">version</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">infisical</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">address</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"https://app.infisical.com"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">auth</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"universal-auth"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">config</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">client-id</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"REPLACE"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">client-secret</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"REPLACE"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">certificates</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">profile-name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"temporal-mtls"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">project-slug</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"REPLACE"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">attributes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">common-name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"temporal.kaplabs.dev"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">alt-names</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"temporal.kaplabs.dev"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"temporal-frontend"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"localhost"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"127.0.0.1"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">key-algorithm</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"RSA_2048"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">signature-algorithm</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"RSA-SHA256"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">key-usages</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"digital_signature"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"key_encipherment"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">extended-key-usages</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"server_auth"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"client_auth"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">ttl</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"90d"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">lifecycle</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">renew-before-expiry</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"30d"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">status-check-interval</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"6h"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">file-output</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">private-key</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">path</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/etc/temporal/certs/server.key"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">permission</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0600"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">certificate</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">path</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/etc/temporal/certs/server.pem"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">permission</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0644"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">post-hooks</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">on-issuance</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">command</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"docker exec temporal-onprem-stack-nginx-1 nginx -s reload"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">on-renewal</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">command</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"docker exec temporal-onprem-stack-nginx-1 nginx -s reload"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>Run it as a systemd service (<code>infisical cert-manager agent --config /etc/infisical/agent.yaml</code>) so it survives reboots. On EC2 you can swap Universal Auth for <code>aws-iam</code> and hold no secret at all — the agent authenticates via the instance's IAM role.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-ui-cloudflare-tunnel--access-the-right-use">The UI: Cloudflare Tunnel + Access (the <em>right</em> use)<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#the-ui-cloudflare-tunnel--access-the-right-use" class="hash-link" aria-label="Direct link to the-ui-cloudflare-tunnel--access-the-right-use" title="Direct link to the-ui-cloudflare-tunnel--access-the-right-use" translate="no">​</a></h2>
<p>For the dashboard, the tunnel finally earns its keep — because it's a browser/HTTP app, exactly what Access login is designed for.</p>
<p>Modern Cloudflare steers you to <strong>remotely-managed tunnels</strong>: create the tunnel at <code>one.dash.cloudflare.com → Networks → Tunnels</code> and run a single <code>cloudflared service install &lt;token&gt;</code> on the box. The connection is <strong>outbound only</strong> — which is why 8080 never needs a firewall rule.</p>
<p>Then:</p>
<ul>
<li class=""><strong>Public Hostname:</strong> <code>temporal-ui.kaplabs.dev</code> → <code>HTTP</code> → <code>localhost:8080</code>. This DNS record is <strong>proxied (orange cloud)</strong> — correct here, the opposite of the gRPC record.</li>
<li class=""><strong>Access application:</strong> self-hosted, hostname <code>temporal-ui.kaplabs.dev</code>, with an Allow policy (your email, or a domain). Built-in one-time-PIN works with zero setup. <strong>Create the Access policy immediately</strong>, or the URL is briefly open to the world.</li>
</ul>
<p>Two gotchas here mirror earlier ones: <code>Error 1033</code> means the tunnel is routed but no connector is running (<code>Active replicas: 0</code>); <code>502 Bad Gateway</code> means the connector is up but can't reach the service URL — almost always <code>https://</code> instead of <code>http://</code>, or a wrong port. And you do <strong>not</strong> open 8080 in AWS: cloudflared reaches the UI over <code>localhost</code>, which never touches the firewall.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-final-architecture">The final architecture<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#the-final-architecture" class="hash-link" aria-label="Direct link to The final architecture" title="Direct link to The final architecture" translate="no">​</a></h2>
<!-- -->
<p>The result:</p>
<ul>
<li class="">A Temporal cluster on a single EC2 box.</li>
<li class="">Workers authenticating over <strong>real mutual TLS</strong> from anywhere, no mesh membership required.</li>
<li class="">Certificates that <strong>rotate themselves</strong> and reload nginx without intervention.</li>
<li class="">A dashboard behind <strong>SSO</strong>, exposed with no inbound port.</li>
<li class="">Exactly <strong>one</strong> inbound port open to the internet, gated by client certs. Everything else is internal or outbound-only.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="takeaways">Takeaways<a href="https://kushalkrishnappa.github.io/blogs/deploying-temporal-with-mtls#takeaways" class="hash-link" aria-label="Direct link to Takeaways" title="Direct link to Takeaways" translate="no">​</a></h2>
<ul>
<li class=""><strong>Learn what "Frontend" means in Temporal</strong> before anything else. There's only one door to secure.</li>
<li class=""><strong>Cloudflare Tunnel + Access is for browser apps, not gRPC APIs.</strong> Don't fight the protocol.</li>
<li class=""><strong>A cert is a file; network membership is a relationship.</strong> For external workers, mTLS is the cleaner interface — but the cost is running a CA and the cost of a CA is rotation. Automate it from day one.</li>
<li class=""><strong>Terminate mTLS in nginx</strong>, keep Temporal vanilla. The all-in-one image + <code>requireClientAuth</code> is a headache you can skip.</li>
<li class=""><strong>Grey-cloud gRPC records, orange-cloud browser records.</strong> Same domain, opposite settings.</li>
<li class=""><strong>Give it 2 GB.</strong> 1 GB freezes the whole box on first boot.</li>
<li class="">Most of the "scary" errors were startup churn or a misplaced file — not real failures. Read the logs before assuming the worst.</li>
</ul>]]></content>
        <category label="temporal" term="temporal"/>
        <category label="mtls" term="mtls"/>
        <category label="infrastructure" term="infrastructure"/>
        <category label="security" term="security"/>
        <category label="devops" term="devops"/>
    </entry>
</feed>