Homelab epilogue: exposing services and publishing the repo
Building the lab: epilogue. (Not part of the numbered series.) ← Homelab observability: Prometheus, Grafana and five focused dashboards · Why a Cloudflare Tunnel is not a fire hose for Jellyfin
The lab is not public yet. I keep saying that, as if it makes the nervousness smaller. The site is public. The workstation config is public. The cluster repos are still private, and I am planning to change that soon.
There is a difference between showing the output and showing the machine. The site is the output. The repos are the machine: the hosts, the manifests, the decisions and the mistakes. Publishing them feels bigger than publishing a post, because a post is a story I chose to tell. A repo is the whole working surface.
This is the part after the numbered series. Part 8 ended with the lab watching itself. This part is about deciding what to show, and what to keep.
The first public hostname
The first time I pointed a real domain at a service in my house, it felt wrong. Nothing was listening on the router. The tunnel dials out. That helped me understand the risk. It did not make me calm.
A tunnel removes an open port. It does not remove the service. Anyone with the hostname can reach the login page. The lock is authentication, not the fact that the machine sits in my spare room.
Here is the shape of the edge today:
The tunnel has two connectors. That was Part 7. One connector can die and the edge still answers. I did not add the second one because it was fun. I added it because one machine had become a single point of failure for everything public.
What is actually exposed
The word “public” gets used too loosely. Not everything is in the same bucket:
| Surface | Who can reach it | What stops them |
|---|---|---|
| Public apps: Jellyfin, Immich, Karakeep, Kavita | Anyone with the hostname | Each app’s own login |
| Admin: ArgoCD, Grafana, Prometheus, Longhorn, media UIs, LazyLibrarian | Anyone who passes the identity check | Cloudflare Access |
| Hosts and cluster internals: SSH, the Kubernetes API, databases, exporters | Not published | No route from the tunnel |
Public does not mean anonymous. The media apps still ask for a password. The difference is that Cloudflare Access is not in front of them, so the app login is the only gate.
Kavita is public because the mobile app cannot complete the Access login flow. That is a trade-off I made on purpose, not an accident. It is also the first gap I would close if I had to pick one.
The fear became instrumentation
I did not become less nervous. I became nervous about specific things. That is an improvement, because a specific fear can become a check.
- The agent has read-only Kubernetes access and a Cloudflare proxy that only allows GET requests. Its curiosity cannot become a change. The write-capable Cloudflare token stays on the host, for the edge sync script.
- The edge is monitored from the outside as well as the inside. Tunnel status, connector count, zone state, certificate state and snapshot age all have alerts.
- A dead-man’s switch runs on the workstation outside the cluster. Every other monitor lives inside the lab. If the lab is down, the monitor is down, and silence looks like health. The outside timer is the exception.
- The backups fail loudly, and a weekly job restores the newest archive and checks the databases inside it. A backup I have not restored is a hope, not a measurement.
- The cluster runs with least privilege where it is easy: kube-state-metrics cannot read Secrets, and images are pinned by version or digest.
- Changes go through pull requests. The cluster self-heals, and an app that drifts for fifteen minutes sends an alert.
None of this makes the lab unhackable. It makes a failure smaller and louder. That is the goal.
The agent is a separate story. I wrote about what it does, how it is set up and where the guardrails are: Hermes as a homelab SRE .
Publishing the repo is another kind of exposure
A repo is not just the files in the working tree. It is the history, branches, tags, issues, pull requests, Actions logs, artifacts, releases, and sometimes the wiki. When the switch flips, all of it becomes public at once.
Here is the checklist I am working through. I am not publishing today.
- Inventory the surface. List the files, then list everything the files do not show: branches, tags, open pull requests, issues, Actions logs, artifacts and releases. The working tree is the smallest part of the job.
- Scan the tree and the history. Use a tool like gitleaks or trufflehog for tokens, private keys, passwords and connection strings. Turn on GitHub secret scanning and push protection, which are free for public repositories. If something did land, rotate it first. A history rewrite does not reach forks, caches or screenshots.
- Remove personal and network details. Names, email addresses, family usernames, LAN addresses, account IDs, zone IDs, tunnel IDs, hostnames, screenshots and logs can all identify a person or a network. The technical reference keeps roles and paths, not addresses. I want a map, not a key.
- Check the security posture. Tunnel-only access, Access on admin surfaces, app login on public apps, least-privilege RBAC, pinned images, weekly image scans and pull-request-only changes. A public repo should describe these controls accurately, not claim more than they do.
- Add repo hygiene. A README that says what the repo is and what it is not. A license. A SECURITY.md with a contact path. A plain statement that these manifests are specific to this hardware, this network and this Cloudflare account. It is not a turnkey product.
- Turn on the controls that private repos could not have. Public repos get branch protection and secret scanning on the free plan. The pre-push hook on the workstation was a workaround for private repos; public removes the excuse.
- Decide what stays private. Not every repo should be public. Operator notes, credential archives and anything that describes a live weakness can stay private. The map can be public. The keys cannot.
Is it enough?
No. The honest list of gaps is still longer than I would like:
- I have not done a full disaster-recovery rehearsal. I have restored backups and checked them, but I have not rebuilt a host from nothing and timed it.
- I have not paid for a penetration test. I have tested the lab myself, so I know its weak spots.
- Kavita is public and protected by a password only. That is the weakest login surface.
- The free Cloudflare plan has no custom rate limiting. Access and app logins do most of the work.
- There is no image signing or SBOM yet.
- The private cluster repos still cannot enforce branch protection on the free plan. That is one reason the public move matters: it makes the controls real instead of approximate.
- The public-repo audit is not finished. I am still walking the checklist.
The lab is in a better state than when it started. That is not the same as finished. Two boxes became three. One connector became two. One dashboard became five. A backup I hoped in became a backup I test. Secrets moved out of git. Hand edits became pull requests.
Enough is a direction, not a finish line.
What I would tell you
If you are about to expose a service, start with what is actually reachable, not with what you are afraid of. Then make the failure small and loud. Then show your work.
I am still nervous about the internet seeing the lab and about publishing the machine behind it. The difference is that the nervousness has a list now. The list is finite. I can work through it.
The detailed map is already public in the technical reference . The repos are the next step. When they go public, I will link them there.
Building the lab: epilogue. Previous: Homelab observability: Prometheus, Grafana and five focused dashboards .