DEV Community

Marcelo
Marcelo

Posted on AI-assisted

I self-hosted Pi-hole + Tailscale on Oracle's free tier — until a 180-day key expiry I didn't know about blacked out my whole network

I moved ad blocking off my home network and onto a free Oracle VM: Pi-hole as the resolver, Tailscale as the mesh, the same box also acting as an exit node. It ran for six months without complaint. Suddenly, my PC and my phone lost name resolution at the same time.

Here's the setup, the evening it broke, and the disk surgery that got me back in. If you run a Pi-hole or any headless Tailscale node, the failure is the part worth reading before you need it.

The setup

┌────────────┐
│ Galaxy S23 ├──┐
└────────────┘  │ WireGuard      ┌──────────────────────────────┐
                ├───────────────▶│ OCI VM (Oracle Always Free)  │
┌────────────┐  │  (Tailscale)   │   tailscale0 → Pi-hole :53   ├──▶ DNS filtered
│ Windows 11 ├──┘                │   exit node → Internet       ├──▶ encrypted egress
└────────────┘                   └──────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Four pieces:

  • One Oracle Always Free VM, VM.Standard.E2.1.Micro (AMD), in São Paulo. The region keeps latency down; the free tier keeps the bill at zero.
  • Pi-hole v6 on that VM, filtering ads and trackers at the DNS layer.
  • Tailscale joining the Windows 11 desktop, the Galaxy S23, and the VM into one mesh. The VM advertises itself as an exit node, so traffic on public Wi-Fi leaves through São Paulo.
  • Tailscale's global DNS pointing every device at the VM's tailnet IP, override enabled.

A couple of details took me longer to get right than they should have:

  • Bind Pi-hole to the tailscale0 interface and set dns.listeningMode=SINGLE. That mode means "permit all origins, accept only on the specified interface," which is exactly the reachability you want. I first used ALL, which accepts on every interface; the Pi-hole docs describe that as a way to end up running an open resolver.
  • Close UDP and TCP port 53 in the OCI security list. Pi-hole should only be reachable inside the mesh. An exposed resolver gets found and used for amplification, and you don't want your tenancy in the middle of that.

One policy worth knowing before you build on this: Oracle reclaims idle Always Free instances. Its definition of idle is CPU 95th percentile under 20%, network under 20%, and memory under 20% for A1 shapes, measured over a 7-day window. A resolver plus exit node clears that comfortably, but if your VM genuinely sits idle, the policy applies to you. Oracle's FAQ says Always Free resources aren't charged after you upgrade to Pay-As-You-Go, and community threads say the upgrade also stops the reclamation. The docs don't state the second part, so I treat it as folklore with a good track record, not a guarantee.

Then the Windows 11 part, which has a real trade-off. To reach the desktop with plain RDP over the tailnet I disabled "Only allow Windows Hello sign-in for Microsoft accounts" and used a local admin account. That re-enables password auth, which weakens the machine. My mitigation: a long unique password, with NLA left on. If you don't need the full desktop, Tailscale SSH is the tighter option.

The failure

My devices on the tailnet lost DNS at the same time. ping 1.1.1.1 worked, so the internet itself was fine. Names didn't resolve. That combination narrows it to one layer fast.

Three things lined up badly that evening:

  1. Tailscale expires node keys after 180 days by default, and the VM's key came due. The node dropped off the mesh without a sound. This is on the Linux install page, which I read when I set the box up, expiry section included and immediately forgotten. I've seen people describe this as hidden. It isn't. I skipped it.
  2. Tailscale's global DNS had exactly one resolver configured, the VM. When the VM left the mesh, every query went nowhere. One resolver means one point of failure, by construction.
  3. I couldn't get back in to fix it. Port 22 was closed in the VCN ingress rules, since I only ever reached the box over Tailscale. The original SSH private key was misplaced. And I couldn't get the GRUB menu to come up for the life of me. My keyboard suffered.

Three locks, one evening.

The rescue: attach the boot volume to a second instance

I had a second Always Free instance (an A1) already running, which turned out to be the whole trick. Instead of rebuilding the micro from scratch:

  1. Generated a new key pair locally: ~/.ssh/oracle-micro and ~/.ssh/oracle-micro.pub.
  2. Stopped the locked micro instance and detached its boot volume.
  3. Attached that volume to the healthy A1 instance as a paravirtualized block volume.
  4. On the A1, mounted the root partition at /mnt/rescue and appended the new public key to /home/ubuntu/.ssh/authorized_keys.
  5. Unmounted, detached from the A1, reattached to the micro as its boot volume, powered on, logged in with the new key.
# on the A1, once the volume is attached
lsblk                                  # find the root partition, usually sdb1, sometimes not
sudo mount /dev/sdb1 /mnt/rescue
sudo tee -a /mnt/rescue/home/ubuntu/.ssh/authorized_keys < ~/.ssh/oracle-micro.pub
sudo umount /mnt/rescue
Enter fullscreen mode Exit fullscreen mode

About 15 minutes, no data loss, no rebuild. One caveat before you reach for the disks: if you can still get to the machine, Tailscale's admin console has a "Temporarily extend key" option that gives an expired node 30 minutes to reauthenticate. Reauth needs shell access, which is the thing I had lost, so it did nothing for me. Check it first anyway.

What I do differently now

Why I kept one resolver

Running a single resolver with no public fallback looks like the bug in this story. It's a decision.

If the Pi-hole dies, everything I own visibly loses DNS, and I can't miss it. The alternative, a public secondary like 1.1.1.1, keeps the internet up while quietly serving unfiltered DNS. Ads return. Trackers return. Nothing tells me. I'd rather have an outage I notice in ten seconds than a silent regression I find weeks later.

The cost is that fail-loud only works if something is watching the resolver. Nothing is right now, beyond me noticing when my own internet stops resolving. The tag keeps the node from expiring on its own; it doesn't tell me when the box falls over for another reason. Closing that gap is the next thing on my list.

What DNS blocking still can't do

A DNS sinkhole is cheap and covers every device with no client software. It also stops at hard limits:

  • Ads served from the same domains as the content can't be separated at the DNS layer. YouTube and Instagram fall into that bucket. uBlock Origin handles YouTube for me. It never did anything for Twitch, which as I understand it stitches ads into the stream before they reach the player, so there's no client-side request to block. I don't have a browser blocker that fixes it.
  • CNAME cloaking hides third-party trackers behind first-party subdomains, and it erodes blocklists over time.
  • Anything that bypasses your resolver entirely still works, whether that's a hardcoded DNS server or DNS-over-HTTPS.

References

  1. Oracle, Always Free Resources: https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm and the Free Tier FAQ: https://www.oracle.com/cloud/free/faq/
  2. Tailscale, Key expiry: https://tailscale.com/kb/1028/key-expiry
  3. Pi-hole, FTL configuration reference (listening modes): https://docs.pi-hole.net/ftldns/configfile/#listeningmode
  4. Community thread on Oracle idle reclamation and Pay-As-You-Go: https://www.reddit.com/r/selfhosted/comments/10phyja/received_an_email_about_oracle_reclaiming_idle/

AI usage disclosure

The infrastructure here was planned and configured with AI tools (OpenCode and a few LLMs), and this write-up was drafted with AI assistance. Every step was executed, broken, and recovered by me on real hardware. The key expiry, the lockout, and the disk rescue all happened.

Top comments (0)