<?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>Pihole on hippotion</title><link>https://blog.hippotion.com/tags/pihole/</link><description>Recent content in Pihole on hippotion</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 11 Apr 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.hippotion.com/tags/pihole/index.xml" rel="self" type="application/rss+xml"/><item><title>🔐 Same Hostname, Two Traffic Paths: Local HTTPS Without a VPN</title><link>https://blog.hippotion.com/posts/homelab-dual-path-tls/</link><pubDate>Fri, 11 Apr 2025 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/homelab-dual-path-tls/</guid><description>No open ports. Real TLS at home. One IngressRoute per app. This is the networking setup I landed on after ruling out everything that required a compromise.</description><content:encoded><![CDATA[<h2 id="the-three-options-that-didnt-work">The three options that didn&rsquo;t work</h2>
<p>When I started the homelab I looked at the standard ways to make self-hosted services accessible and found the usual compromises:</p>
<p><strong>Open ports on the router.</strong> Point a DNS record at your public IP, forward 443 to your server. Simple. Also: your home IP is now publicly associated with your services, your ISP can see your traffic, and a misconfigured app means an open door. Hard pass.</p>
<p><strong>VPN for everything.</strong> WireGuard on the router, every device gets a tunnel. Secure, but every phone and laptop needs to be configured, split tunnelling adds complexity, and &ldquo;let me pull up the recipe&rdquo; becomes a two-tap operation that my family won&rsquo;t do. And I still want public access for some services.</p>
<p><strong>Cloudflare Tunnel, but broken HTTPS locally.</strong> This is the one I started with. The cloudflared pod dials out to Cloudflare, no open ports, external access works great. But locally, <code>*.hippotion.com</code> resolves to Cloudflare&rsquo;s anycast IP, traffic leaves the house, Cloudflare terminates TLS, traffic comes back in through the tunnel. Every local request makes a round trip to a Cloudflare edge node. Worse: browsers cache HSTS for <code>hippotion.com</code>, so <code>http://</code> URLs on the local network silently upgrade to <code>https://</code>, which fails because there&rsquo;s no local certificate. Intermittent, confusing, and hard to explain to anyone else on the network.</p>
<p>What I wanted: the tunnel for external access, direct-to-server for local access, real TLS in both cases, and one configuration per application.</p>
<hr>
<h2 id="the-insight-pi-hole-already-controls-local-dns">The insight: Pi-hole already controls local DNS</h2>
<p>My network already runs Pi-hole for ad blocking. Pi-hole uses dnsmasq under the hood and can resolve any hostname to any IP you want. One config line in Pi-hole&rsquo;s values:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">dnsmasq</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">customDnsEntries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">address=/hippotion.com/192.168.0.109</span><span class="w">
</span></span></span></code></pre></div><p>The <code>address=</code> directive is a wildcard. Every device on the LAN that uses Pi-hole for DNS — which is all of them, because the router hands out Pi-hole&rsquo;s IP via DHCP — will now resolve <code>anything.hippotion.com</code> to <code>192.168.0.109</code>, the server&rsquo;s LAN address. External traffic still goes to Cloudflare&rsquo;s IP because it uses the public authoritative DNS. The split is automatic; no per-device configuration.</p>
<p>Local browser → Pi-hole → server&rsquo;s LAN IP directly.</p>
<p>That solves routing. Now TLS.</p>
<hr>
<h2 id="why-http-01-wont-work-here-and-why-dns-01-will">Why HTTP-01 won&rsquo;t work here, and why DNS-01 will</h2>
<p>The standard way to get a Let&rsquo;s Encrypt certificate is the HTTP-01 challenge: Let&rsquo;s Encrypt sends a request to <code>http://yourdomain.com/.well-known/acme-challenge/&lt;token&gt;</code> and your server responds. This requires Let&rsquo;s Encrypt&rsquo;s servers to reach your server over the public internet.</p>
<p>That doesn&rsquo;t work here. There are no open ports. Let&rsquo;s Encrypt can&rsquo;t reach the server. HTTP-01 is out.</p>
<p>DNS-01 is different. Instead of proving you control a server, you prove you control the DNS zone by creating a temporary TXT record at <code>_acme-challenge.yourdomain.com</code>. Let&rsquo;s Encrypt checks DNS, finds the record, issues the cert. No inbound connection required — just API access to your DNS provider.</p>
<p><code>hippotion.com</code> is on Cloudflare. cert-manager has a Cloudflare DNS solver that calls the Cloudflare API to create and delete the TXT record automatically. The certificate request flow:</p>
<ol>
<li>cert-manager creates an ACME Order with Let&rsquo;s Encrypt</li>
<li>cert-manager calls the Cloudflare API: add <code>_acme-challenge.hippotion.com TXT &lt;token&gt;</code></li>
<li>Let&rsquo;s Encrypt queries DNS, finds the record, issues the cert</li>
<li>cert-manager deletes the TXT record, writes the cert to a Kubernetes Secret</li>
<li>cert-manager renews automatically ~30 days before expiry</li>
</ol>
<p>The Cloudflare API token needs <code>Zone:DNS:Edit</code> permission for <code>hippotion.com</code>. It lives in Vault and syncs to the <code>sys-cert-manager</code> namespace via External Secrets Operator — same pattern as every other secret in the cluster, nothing in Git.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Certificate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hippotion-wildcard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">sys-traefik</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">hippotion-wildcard-tls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-cloudflare</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dnsNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;hippotion.com&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;*.hippotion.com&#34;</span><span class="w">
</span></span></span></code></pre></div><p>One certificate. Every subdomain. cert-manager stores it as <code>hippotion-wildcard-tls</code> in the <code>sys-traefik</code> namespace, where Traefik can read it.</p>
<hr>
<h2 id="two-traefik-entrypoints">Two Traefik entrypoints</h2>
<p>Traefik has three entrypoints configured:</p>
<table>
	<thead>
			<tr>
					<th>Entrypoint</th>
					<th>Port</th>
					<th>Purpose</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>web</code></td>
					<td>80</td>
					<td>Redirects all traffic to <code>websecure</code></td>
			</tr>
			<tr>
					<td><code>websecure</code></td>
					<td>443</td>
					<td>Local HTTPS, serves the wildcard cert</td>
			</tr>
			<tr>
					<td><code>cloudflare</code></td>
					<td>7080</td>
					<td>Receives plain HTTP from the cloudflared pod</td>
			</tr>
	</tbody>
</table>
<p>The <code>cloudflare</code> entrypoint is the key piece. Cloudflare Tunnel terminates TLS at Cloudflare&rsquo;s edge and forwards plain HTTP to the cluster. If that plain HTTP landed on <code>web</code> (port 80), it would get redirected to <code>websecure</code> (port 443), which would fail because cloudflared isn&rsquo;t sending HTTPS. A separate entrypoint on a separate port handles tunnel traffic without redirection.</p>
<p>Traefik is configured to use the wildcard cert as its default:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">tlsStore</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">defaultCertificate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">hippotion-wildcard-tls</span><span class="w">
</span></span></span></code></pre></div><p>Any <code>websecure</code> request that doesn&rsquo;t match a more specific TLS configuration gets the wildcard cert. No per-app certificate configuration.</p>
<hr>
<h2 id="one-ingressroute-handles-both-paths">One IngressRoute handles both paths</h2>
<p>Every application gets a single IngressRoute with both entrypoints:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">traefik.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">IngressRoute</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">myapp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">myapp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryPoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">cloudflare  </span><span class="w"> </span><span class="c"># plain HTTP from cloudflared</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">websecure   </span><span class="w"> </span><span class="c"># local HTTPS with wildcard cert</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">routes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">match</span><span class="p">:</span><span class="w"> </span><span class="l">Host(`myapp.hippotion.com`)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Rule</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">middlewares</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">oauth-auth</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">sys-oauth2-gitlab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">myapp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><p>That&rsquo;s it. The same hostname, the same routing rule, the same middleware — served correctly on both paths. No conditional logic, no separate ingress for local vs external.</p>
<p>The OAuth middleware (<code>oauth-auth</code>) works on both paths too. Local browsers get redirected to GitLab for authentication the same way external browsers do. The SSO cookie is set on <code>hippotion.com</code>, so it works across all subdomains regardless of which path the traffic came through.</p>
<hr>
<h2 id="what-the-two-traffic-paths-look-like-end-to-end">What the two traffic paths look like end to end</h2>
<pre tabindex="0"><code>External browser (anywhere):
  Browser
    → Cloudflare DNS (hippotion.com → Cloudflare anycast IP)
    → Cloudflare edge (TLS terminated, certificate managed by Cloudflare)
    → cloudflared pod in cluster (plain HTTP)
    → Traefik :7080 (cloudflare entrypoint)
    → app pod

Local browser (home WiFi):
  Browser
    → Pi-hole DNS (*.hippotion.com → 192.168.0.109)
    → Traefik :443 (websecure entrypoint)
    → Traefik serves hippotion-wildcard-tls (Let&#39;s Encrypt cert, trusted by browser)
    → app pod
</code></pre><p>Both paths hit the same Traefik IngressRoute rule. The app sees an HTTP request either way. TLS is handled at the edge — Cloudflare for external traffic, Traefik for local traffic.</p>
<hr>
<h2 id="the-hsts-detail">The HSTS detail</h2>
<p>Cloudflare likely has HSTS enabled for your domain. Browsers cache this: once they see an HSTS header for <code>hippotion.com</code>, they&rsquo;ll refuse to load any <code>http://</code> URL under that domain for the duration of the max-age. They silently upgrade to <code>https://</code> and fail if there&rsquo;s no cert.</p>
<p>This is why the original setup — tunnel only, no local cert — felt unreliable locally. The browser was doing the right thing (enforcing HTTPS) but the cert didn&rsquo;t exist. The wildcard cert fixes this because HTTPS now actually works locally. The HSTS enforcement is fine once TLS is real.</p>
<hr>
<h2 id="what-its-like-to-operate">What it&rsquo;s like to operate</h2>
<p>Adding a new service means writing one IngressRoute with two entrypoints and pushing to Git. No DNS records to create (cloudflared picks up hostnames from a config list), no certificates to request (the wildcard covers everything), no VPN profiles to distribute. The platform handles it.</p>
<p>Local access works when the internet is down. The Pi-hole DNS and the wildcard cert are entirely on-premises — as long as the server is up, the services are reachable, Cloudflare outage or not. I noticed this during a brief Cloudflare incident a few months ago: external access went down, everything inside the house kept working without interruption.</p>
<p>I&rsquo;m not a networking expert. I just followed the constraint — no open ports, no VPN — and the DNS-01 + split DNS solution fell out naturally. It turned out to be simpler to configure than the alternatives, and cleaner to operate.</p>
]]></content:encoded></item></channel></rss>