Reference for IP assignments, firewall rules, DNS configuration, Tailscale ACLs, and traffic flow.
Quick links: 🌐 IP Assignments · 🔥 Firewall Rules · 🧭 DNS · 🔒 Tailscale · 🔀 Traffic Flow
Full outline
| Node | IP var | Role |
|---|---|---|
homelab-edge |
ip_edge |
Edge, DNS, Ansible control |
homelab-observe |
ip_observe |
Monitoring |
homelab-data-01 |
ip_data_01 |
Postgres, Redis (shared data tier) |
homelab-svc-01 |
ip_svc_01 |
Camunda, n8n, Authentik, Jellyfin |
homelab-svc-02 |
ip_svc_02 |
GreenTechHub |
IP values and lan_subnet are defined in inventories/group_vars/all/overrides.yml. main.yml holds EDIT_BEFORE_USE placeholders; overrides.yml overrides them with real values. When IPs change, update overrides.yml only. Also configure static DHCP reservations on your router by MAC address.
| Node | Tailscale IP |
|---|---|
homelab-edge |
100.x.x.1 |
homelab-observe |
100.x.x.2 |
homelab-data-01 |
100.x.x.3 |
homelab-svc-01 |
100.x.x.4 |
homelab-svc-02 |
100.x.x.5 |
Tailscale IPs are assigned by the coordination server and stable per device. Update group_vars/all/overrides.yml if they change.
| Service | Node | Port |
|---|---|---|
| Grafana | homelab-observe |
3000 |
| Prometheus | homelab-observe |
9090 |
| Alertmanager | homelab-observe |
9093 |
| Uptime Kuma | homelab-observe |
3001 |
| Portainer | homelab-observe |
9000 |
| ntfy | homelab-observe |
8085 |
| Camunda | homelab-svc-01 |
8080 |
| Authentik | homelab-svc-01 |
9000 |
| n8n | homelab-svc-01 |
5678 |
| Jellyfin | homelab-svc-01 |
8096 |
| GreenTechHub | homelab-svc-02 |
8000 |
| Postgres | homelab-data-01 |
5432 |
| Redis | homelab-data-01 |
6379 |
| Pi-hole admin | homelab-edge |
80 |
| Infisical — Caddy HTTPS (Tailscale only) | homelab-edge |
8443 |
| Semaphore — Caddy HTTPS (Tailscale only) | homelab-edge |
8444 |
| Infisical direct (Tailscale only, non-browser) | homelab-edge |
8222 |
| Semaphore direct (Tailscale only, non-browser) | homelab-edge |
3010 |
| SSH (all nodes) | all | ssh_port |
All nodes use ufw with a default-deny inbound policy. Rules are applied by the base_hardening role and node-specific group vars.
| Port | Protocol | Source | Reason |
|---|---|---|---|
ssh_port |
TCP | any | SSH admin access |
| 53 | TCP/UDP | LAN | Pi-hole DNS (LAN only) |
| 80 | TCP | LAN | Pi-hole admin UI (LAN only) |
| 8443 | TCP | tailscale_cgnat_range |
Caddy HTTPS — Infisical (Tailscale) |
| 8444 | TCP | tailscale_cgnat_range |
Caddy HTTPS — Semaphore (Tailscale) |
| 3000 | TCP | tailscale_cgnat_range |
Caddy HTTPS — Grafana (Tailscale) |
| 9090 | TCP | tailscale_cgnat_range |
Caddy HTTPS — Prometheus (Tailscale) |
| 9093 | TCP | tailscale_cgnat_range |
Caddy HTTPS — Alertmanager (Tailscale) |
| 3001 | TCP | tailscale_cgnat_range |
Caddy HTTPS — Uptime Kuma (Tailscale) |
| 9000 | TCP | tailscale_cgnat_range |
Caddy HTTPS — Portainer (Tailscale) |
| 8085 | TCP | tailscale_cgnat_range |
Caddy HTTPS — ntfy (Tailscale) |
| 8222 | TCP | tailscale_cgnat_range (100.64.0.0/10) |
Infisical direct port (Tailscale only — non-browser clients) |
| 3010 | TCP | tailscale_cgnat_range (100.64.0.0/10) |
Semaphore direct port (Tailscale only — non-browser clients) |
No ports are forwarded from the router. Cloudflare Tunnel connects outbound — all external traffic enters through it.
🔒 All
tailscale_cgnat_rangerows above are Tailscale-only, served ashttps://homelab-edge.<tailnet>.ts.net:<port>via Caddy (browser-trusted Let's Encrypt cert fromtailscale cert) — no Pi-hole DNS involved. Ports 8222/3010 are the equivalent direct (non-Caddy) ports for non-browser clients. Don't widen any of these beyondtailscale_cgnat_range— seegroup_vars/all/main.yml. Request path: Tailscale-Only Service Access.
| Port | Protocol | Source | Reason |
|---|---|---|---|
ssh_port |
TCP | LAN / VPN | SSH (edge + admin devices) |
| 3000 | TCP | LAN / VPN | Grafana |
| 9090 | TCP | LAN / VPN | Prometheus |
| 9093 | TCP | LAN / VPN | Alertmanager |
| 3001 | TCP | LAN / VPN | Uptime Kuma |
| 9000 | TCP | LAN / VPN | Portainer |
| 8085 | TCP | LAN / VPN | ntfy |
No public exposure. Accessible via Tailscale from admin devices.
| Port | Protocol | Source | Reason |
|---|---|---|---|
ssh_port |
TCP | LAN / VPN | SSH (edge only) |
| 5432 | TCP | Tailnet + LAN node IPs | Postgres — consumers only, not open broadly |
| 6379 | TCP | Tailnet + LAN node IPs | Redis — consumers only, not open broadly |
| Port | Protocol | Source | Reason |
|---|---|---|---|
ssh_port |
TCP | LAN / VPN | SSH (edge only) |
| 8080 | TCP | LAN / VPN | Camunda |
| 9000 | TCP | LAN / VPN | Authentik |
| 5678 | TCP | LAN / VPN | n8n |
| 8096 | TCP | LAN / VPN | Jellyfin |
| Port | Protocol | Source | Reason |
|---|---|---|---|
ssh_port |
TCP | LAN / VPN | SSH (edge only) |
| 8000 | TCP | LAN / VPN | GreenTechHub |
Firewall rules are managed by Ansible. Do not apply ufw changes manually — they will be overwritten on the next
deploy. Instead, edit ufw_rules in group_vars/<node>.yml and run:
ansible-playbook playbooks/apply_firewall.yml --limit homelab-edgePi-hole on homelab-edge (ip_edge) is the DNS server for all LAN clients. Set it as the primary DNS on your
router's DHCP config.
Internal hostnames are configured as static entries in the homelab-edge-services repo (Pi-hole's custom.list). Do not edit /etc/pihole/custom.list directly — it is overwritten on every deploy-service deploy of homelab-edge-services.
| Hostname | Resolves to | Port |
|---|---|---|
grafana.homelab.local |
ip_observe |
3000 |
prometheus.homelab.local |
ip_observe |
9090 |
alertmanager.homelab.local |
ip_observe |
9093 |
uptime.homelab.local |
ip_observe |
3001 |
portainer.homelab.local |
ip_observe |
9000 |
ntfy.homelab.local |
ip_observe |
8085 |
camunda.homelab.local |
ip_svc_01 |
8080 |
authentik.homelab.local |
ip_svc_01 |
9000 |
n8n.homelab.local |
ip_svc_01 |
5678 |
jellyfin.homelab.local |
ip_svc_01 |
8096 |
greentechhub.homelab.local |
ip_svc_02 |
8000 |
Clients access services at http://<hostname>:<port>. Traffic between nodes travels over Tailscale (encrypted), so a
separate internal TLS layer is not required.
Upstream resolution is handled by Unbound running on homelab-edge, which performs recursive DNSSEC-validated
lookups directly against root servers — no third-party upstream DNS.
Caddy and Pi-hole config are static files committed in the homelab-edge-services repo, not
Ansible templates — see repo_split_brief.md.
- In
homelab-edge-services, add an entry to both:configs/pihole/custom.list(hostname →ip_edge's literal current value)configs/caddy/Caddyfile(reverse_proxy {env.IP_<NODE>}:<port>) Commit and push.
- Redeploy from
homelab-edge:/opt/deploy-service-venv/bin/deploy-service deploy homelab-edge-services --config /opt/homelab/services.yml
Public hostnames (*.yourdomain.com) are managed in Cloudflare DNS. TLS terminates at Cloudflare. The Cloudflare
Tunnel config in host_vars/homelab-edge.yml maps each public hostname to an internal host:port.
Pi-hole has no involvement in external DNS resolution.
Tailscale is installed on every node individually. Each node has its own Tailscale IP and maintains a direct encrypted connection to the Tailscale coordination server, independently of all other nodes.
homelab-edge additionally runs in subnet router mode, advertising lan_subnet to the Tailscale network. This
is a convenience for reaching LAN devices that do not have Tailscale installed (e.g. a router, NAS, or IoT device) — it
is not a dependency for accessing homelab nodes.
Because every node has its own Tailscale connection, the edge going dark has no impact on your ability to SSH into or
manage any other node over VPN. LAN access (direct IP on ssh_port) remains available regardless.
homelab-edgejoins the tailnet during Phase 1 (not Phase 2, as every other node does) —bootstrap_edge.ymlbrings Tailscale up before Infisical, because both Infisical and Semaphore (provisioned, seeded, and brought up later in that same single-pass play — see What the Bootstrap Playbook Does) are Tailscale-only services that must be reachable for the operator by the time Phase 1 finishes. (The provisioning-and-seed step itself doesn't need Tailscale at all — it runs over loopback, pre-firewall, in the one window where127.0.0.1:8222is reachable unauthenticated; seeroles/infisical/tasks/bootstrap_instance.yml's header comment.)deploy_edge.ymlre-runs thetailscalerole in Phase 2 too — idempotent, a no-op once the node is already joined.
Tailscale ACLs are defined in the Tailscale admin console (not in this repo). Recommended policy:
| Source | Destination | Ports | Reason |
|---|---|---|---|
homelab-edge |
all homelab nodes | 22 | Ansible SSH |
homelab-observe |
all homelab nodes | 9100, 8080 | Prometheus scrape (node + cAdvisor) |
tag:admin |
all homelab nodes | 22 | Admin SSH access |
tag:admin |
homelab-observe |
3000, 3001, 9000 | Grafana, Uptime Kuma, Portainer |
| all homelab nodes | homelab-observe |
3100 | Loki log ingestion |
homelab-edge |
homelab-svc-01/02 |
8080, 8096, 8000 | Cloudflare Tunnel routing (Camunda/Jellyfin on svc-01, GreenTechHub on svc-02) |
| svc nodes | homelab-data-01 |
5432, 6379 | Postgres/Redis (shared data tier) |
| deny | all | all | Default deny |
Tag admin devices in the Tailscale console as tag:admin. This gives you fine-grained per-node ACL control — tighter
than a blanket subnet route allow rule.
The edge subnet router (
lan_subnet) should not be granted broad access in ACLs. It exists only to reach non-Tailscale LAN devices; homelab nodes are accessed directly by their own Tailscale IPs.
Tailscale auth keys are stored in inventories/group_vars/all/vault.yml. On expiry, generate a new reusable key from the Tailscale admin
console and update the vault:
ansible-vault edit inventories/group_vars/all/vault.ymlThen re-run the relevant deploy playbook to apply the new key.
Tailscale MagicDNS resolves node hostnames (e.g. homelab-edge) to Tailscale IPs automatically for devices on the VPN.
This complements Pi-hole's .homelab.local zone — MagicDNS handles node-to-node resolution within Tailscale; Pi-hole
handles service-level .homelab.local resolution for LAN clients.
homelab-edge provisions a browser-trusted Let's Encrypt certificate for its MagicDNS FQDN
(homelab-edge.<tailnet>.ts.net) via the tailscale cert command. Tailscale acts as the DNS-01 ACME proxy — no public
port exposure required. The cert is written to /var/lib/tailscale/certs/ and mounted read-only into the Caddy
container, which serves it on ports 8443/8444 for Infisical and Semaphore respectively.
This gives Tailscale-connected browsers a green padlock at https://homelab-edge.<tailnet>.ts.net:8443/8444 with no
per-device trust setup. LAN access via *.homelab.local uses plain HTTP — no TLS on the LAN path (ACME cannot issue
certs for .local domains and Caddy's local CA requires per-device trust installation).
The cert is provisioned by the tailscale Ansible role (tailscale_cert_enabled: true on the edge node) and renewed
weekly via cron.
Client → Cloudflare DNS → Cloudflare edge (TLS termination)
→ Cloudflare Tunnel (outbound from homelab-edge)
→ cloudflared on homelab-edge
→ target service on internal host:port
No ports open on the router. No direct internet exposure of any homelab node.
LAN client → Pi-hole DNS (<ip_edge>:53)
→ resolves service.homelab.local → node IP
→ direct connection to node:port over LAN
Ports and services covered by this pattern are listed in Firewall Rules → homelab-edge. No Pi-hole hostname, no LAN path — the requesting device must itself be a tailnet member.
Browser (Tailscale):
Admin device (on tailnet) → https://homelab-edge.<tailnet>.ts.net:{8443,8444,...}
→ ufw allows 100.64.0.0/10 on that port → Caddy → container on homelab-edge
Non-browser / API (Tailscale):
Admin device (on tailnet) → http://<edge-tailscale-ip>:{8222,3010}
→ ufw allows 100.64.0.0/10 on that port → container on homelab-edge
homelab-edge (homelab user)
→ SSH to target node (<ssh_port>)
→ executes tasks as homelab user with passwordless sudo
GitHub push → .github/workflows/deploy.yml
→ HTTP POST to n8n/Camunda endpoint (via Cloudflare Tunnel)
→ n8n/Camunda SSHes to homelab-edge as deploy user (via Tailscale)
→ scripts/deploy.sh → ansible-playbook
→ Ansible SSHes to all target nodes