Reference for IP assignments, firewall rules, DNS configuration, Tailscale ACLs, and traffic flow.
| Node | IP var | Role |
|---|---|---|
homelab-edge |
ip_edge |
Edge, DNS, Ansible control |
homelab-observe |
ip_observe |
Monitoring |
homelab-svc-01 |
ip_svc_01 |
Camunda, databases |
homelab-svc-02 |
ip_svc_02 |
GreenTechHub |
homelab-svc-03 |
ip_svc_03 |
Jellyfin |
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-svc-01 |
100.x.x.3 |
homelab-svc-02 |
100.x.x.4 |
homelab-svc-03 |
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 |
| Camunda | homelab-svc-01 |
8080 |
| GreenTechHub | homelab-svc-02 |
8000 |
| Jellyfin | homelab-svc-03 |
8096 |
| Pi-hole admin | homelab-edge |
80 |
| 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) |
No ports are forwarded from the router. Cloudflare Tunnel connects outbound — all external traffic enters through it.
| 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 |
No public exposure. Accessible via Tailscale from admin devices.
| Port | Protocol | Source | Reason |
|---|---|---|---|
ssh_port |
TCP | LAN / VPN | SSH (edge only) |
| 8080 | TCP | LAN / VPN | Camunda |
| 5432 | TCP | LAN / VPN | PostgreSQL (internal only) |
| 9200 | TCP | LAN / VPN | Elasticsearch (internal only) |
| Port | Protocol | Source | Reason |
|---|---|---|---|
ssh_port |
TCP | LAN / VPN | SSH (edge only) |
| 8000 | TCP | LAN / VPN | GreenTechHub (svc-02) |
| 8096 | TCP | LAN / VPN | Jellyfin (svc-03) |
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 driven by pihole_custom_dns in group_vars/edge.yml and rendered via roles/edge_services/templates/pihole/custom.list.j2. Do not edit /etc/pihole/custom.list directly — it is overwritten on every deploy.
| 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 |
camunda.homelab.local |
ip_svc_01 |
8080 |
greentechhub.homelab.local |
ip_svc_02 |
8000 |
jellyfin.homelab.local |
ip_svc_03 |
8096 |
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.
- Add an entry to both
pihole_custom_dnsandcaddy_routesingroup_vars/edge.yml(DNS resolves toip_edge; Caddy proxies to the backend) - Run the edge deploy playbook:
ansible-playbook playbooks/deploy_edge.yml --tags pihole,caddy
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.
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, 8000 | Cloudflare Tunnel routing |
| 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.
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
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