Celilo's zones describe what's allowed to talk to what.
The firewall enforces those rules — but the firewall can only enforce them if
traffic between zones is forced to go through it. Without that, zones are
honor-system: a container in app that knows the gateway IP for
secure can ARP for the database directly, the switch happily floods
the frame, and your "isolation" is a sticker on the box.
802.1q VLAN tagging is what makes zones real. This page walks through how to wire your home lab so Celilo's policy actually has teeth.
Why VLANs
Most home networks live on a single broadcast domain — one subnet, one switch, every device's ARP requests reach every other device. That's fine for a flat network, but it makes every "zone" the same zone. Anything talking to anything, with the firewall as a polite suggestion.
802.1q tags every Ethernet frame with the VLAN it belongs to. The switch refuses
to forward frames between tags, so each zone is its own broadcast domain. A host
on the app VLAN can send packets toward an IP on the
secure VLAN, but those packets only get there if a router decides
to route them — and the only router with interfaces in both VLANs is your
firewall machine. That's where Celilo's policy lives.
The practical upshot:
- Compromising a workload in
appdoesn't get you L2 access tosecure; you have to defeat the firewall, not just guess an IP. - You can put non-Celilo devices on a zone (a NAS in
secure, an IoT camera ininternal) and they get the same policy enforcement as Celilo-managed containers. - Adding a zone is a switch-config change plus a firewall-config change, not re-cabling.
Reference topology
Here's how the zones, hosts, and capabilities fit together at the logical level:
The five zones, with the example modules placed on each:
external— the public internet. Untagged, lives upstream of the firewall. The cloud at the top of the diagram.internal— VLAN 192 by default. Where you, the operator, and (today) the Celilo orchestrator live.dmz— VLAN 10 by default. Public-facing workloads. Caddy lives here, terminating HTTPS.app— VLAN 20 by default. Internal services. Authentik (the identity provider) lives here, as do most application servers.secure— VLAN 30 by default. Sensitive data. Postgres lives here.
Two firewalls do different jobs:
- The perimeter firewall (labeled
perimeter-fwin the diagram) is the only thing facing the public internet. It does NAT for outbound traffic, DNAT for inbound traffic on ports 80/443, and provides thefirewallcapability that public-facing modules (Caddy, anything wantingexposeService) call into. In your setup it can be a port-forward on your ISP router, a Cloudflare tunnel, or a VPS — see Perimeter ingress. iptablessits between zones. It's the inter-zone gateway: every packet fromapptosecure, or fromdmztoapp, passes through it and gets policy applied. Also provides thefirewallcapability — modules don't have to know which firewall is which; they ask forfirewalland Celilo wires them to whichever provider serves their zone.
The capability labels on the diagram (firewall, idp,
public_web) are how modules find each other. Caddy declares it
provides public_web; your web app's manifest declares it
requires public_web; Celilo wires the call without anyone
hardcoding hostnames or IPs. Same for idp (Authentik) and
firewall (whichever module provides the perimeter or inter-zone
firewall in your setup).
Physical layout
That's the logical view. Here's what the actual wiring looks like in a typical deployment:
Five kinds of physical things:
- ISP router with wifi. Stays as you have it. This is where untrusted endpoints (laptops, phones, smart TVs, IoT) live. It's not part of the Celilo network; it's the public side of the firewall.
- Firewall machine. A small Linux box (Raspberry Pi, mini-PC, repurposed thin client) with two interfaces: one to the ISP router (untrusted, public side), one to the managed switch (trusted, trunk port carrying every VLAN). Runs the
iptablesmodule to provide inter-zone firewalling. If you choose to also run a perimeter firewall locally (option A in Perimeter ingress), the same box can do both jobs — or you can split them onto separate hosts. - Managed switch. Any switch that supports 802.1q. Trunk port to the firewall, trunk port to each Proxmox host, access port to anything else (mgmt box, NAS, wired endpoints).
- Proxmox hosts. One or more. Each connects to the switch over a single trunk port;
vmbr0is configured VLAN-aware, and containers attach with atag=Nmatching their zone. - Mgmt box. Whatever machine you run
celilofrom. Usually on an access port carrying theinternalVLAN untagged.
Recommended: managed switch with 802.1q
This is the layout above. Three things to configure:
Pick your numbers
Celilo ships with these defaults, which match the diagrams above:
| Zone | VLAN | Default subnet | Notes |
|---|---|---|---|
dmz | 10 | 10.0.10.0/24 | Public-facing workloads (Caddy) |
app | 20 | 10.0.20.0/24 | Internal services (Authentik, internal APIs) |
secure | 30 | 10.0.30.0/24 | Databases, secret stores |
internal | 192 | 192.168.1.0/24 | Operator LAN, mgmt |
external | — | — | Off-LAN; lives on the public side of the firewall |
If you have to use different VLAN numbers (existing network, conflicts with a vendor default), you can override them later — see Telling Celilo about it.
Switch configuration
Vendor specifics vary; the shape is the same:
- Create VLANs 10, 20, 30, 192 in the switch's admin UI.
- The port to the firewall machine: tagged trunk — all four VLANs tagged. (Some vendors call this "general" or "hybrid"; the point is the firewall sees the tag.)
- Each port to a Proxmox host: tagged trunk — same four VLANs tagged.
- Port to the mgmt box: access port, untagged on VLAN 192.
- Ports for wired endpoints (printers, NAS, anything not running zone-aware software): access port, untagged on whichever VLAN that device should live on.
For step-by-step admin-UI walkthroughs, the vendor's docs are the source of truth — UniFi, MikroTik, TP-Link/Omada, Cisco, and Netgear all have working guides. The Celilo side doesn't care which switch you use, only that the tags match the numbers above.
Firewall machine
Celilo's iptables module configures the firewall rules for you.
What you have to set up before running Celilo is the VLAN sub-interfaces
themselves, so the kernel has somewhere to apply rules. On Debian/Ubuntu,
/etc/network/interfaces looks like:
auto eth0
iface eth0 inet manual
# Trunk to the switch — no IP on the trunk itself
auto eth0.10
iface eth0.10 inet static
address 10.0.10.1/24
auto eth0.20
iface eth0.20 inet static
address 10.0.20.1/24
auto eth0.30
iface eth0.30 inet static
address 10.0.30.1/24
auto eth0.192
iface eth0.192 inet static
address 192.168.1.1/24
# eth1 is the public side facing the ISP router
auto eth1
iface eth1 inet dhcp
Each eth0.N sub-interface is the gateway for its zone — that's the
IP Celilo's auto-derived gateway field will produce when a module
asks for one. Restart networking, confirm ip -br addr shows the
sub-interfaces up, then celilo machine add the firewall and run
module deploy iptables to lay down the rules.
Proxmox host
Each Proxmox host needs a single VLAN-aware bridge. /etc/network/interfaces:
auto vmbr0
iface vmbr0 inet static
address 192.168.1.10/24
gateway 192.168.1.1
bridge-ports eno1
bridge-stp off
bridge-fd 0
bridge-vlan-aware yes
bridge-vids 2-4094
Two important bits: bridge-vlan-aware yes tells the bridge to
respect 802.1q tags, and bridge-vids 2-4094 permits the full tag
range on the bridge (Proxmox restricts it by default). The Proxmox host itself
is reachable on the untagged segment of the bridge — most setups put that on
internal so you can SSH in from your mgmt box.
Once the bridge is up, container creation is hands-off. Celilo's Terraform
templates emit a network block with tag = $self:vlan,
and Celilo auto-derives vlan from the zone the module requested
— see Telling Celilo about it.
Minimal: firewall ↔ Proxmox direct, no switch
If you don't have a managed switch (yet), you can still get zone enforcement. The trick is to skip the switch entirely and run a trunk straight from the firewall to the Proxmox host. Two viable shapes:
Shape A: single cable, sub-interfaces on both ends
One Ethernet cable from the firewall's "trusted" NIC to a Proxmox NIC. Both
ends speak 802.1q. Configuration is identical to the recommended setup above —
eth0.10/20/30/192 on the firewall, VLAN-aware vmbr0
on Proxmox — you just don't have a switch in between.
What you give up: there's nowhere to plug a non-Proxmox device into a specific
zone. Want a NAS in secure? It has to be a Proxmox container, or
you have to add a switch.
Shape B: multi-NIC firewall, untagged per-zone subnets
If your firewall machine has several NICs, you can give each zone its own
untagged NIC and crossover-cable each one to a Proxmox NIC. No 802.1q anywhere.
Simpler to debug, but it requires Proxmox bridges per zone (vmbr10,
vmbr20, etc.) and the same NIC-port-shortage problem as
per-zone NICs below.
Per-zone NICs (no VLANs)
Each zone gets its own physical NIC on the firewall and on each Proxmox host. Cleanest L2 isolation, no tagging to misconfigure — but you run out of NIC ports fast. Five zones means five NICs per Proxmox host, and most home-lab boxes ship with one or two onboard.
Worth knowing exists; in practice 802.1q is what most home labs use because it makes "add another zone" a config change rather than a hardware change. Vendor docs (Proxmox networking, Linux bridge) cover the per-NIC setup.
Perimeter ingress
The diagrams show a Linux box at the perimeter doing public NAT and inbound DNAT for ports 80/443. That's one option, but it's not the only one. Three patterns work today; pick whichever fits your ISP situation and tolerance for outsourced trust:
A. Port-forwarding on your ISP router
The simplest setup. Configure port-forward rules in your ISP router's admin
UI to send TCP 80/443 to your DMZ host (typically the Caddy container — its
IP is what module deploy caddy prints on success). No extra
hardware, no public IP beyond what the ISP gives you. Most consumer routers
support this.
Recommended starting point. A planned isp-router Celilo module
will give you a checklist of port-forward rules to enter manually and confirm
when done; until then, do it by hand once and forget about it.
B. Cloudflare tunnel
No port-forward, no public IP needed. Cloudflare Tunnel runs a daemon inside your network that dials out to Cloudflare's edge; Cloudflare proxies inbound HTTPS through that tunnel back to your DMZ. Works behind CG-NAT, works on residential connections that block inbound traffic.
Trade-offs: HTTPS terminates at Cloudflare (you trust them with TLS for the domains you tunnel), and you're tied to their pricing/policy. Not yet wrapped in a Celilo module; configure it manually, then point the tunnel's local endpoint at Caddy.
C. VPS + WireGuard
Rent a small public-IP VPS (Digital Ocean, Hetzner, Vultr — $4–$6/month),
run a WireGuard tunnel between it and a host on your dmz, and
forward inbound 80/443 on the VPS through the tunnel into your network.
The VPS becomes your "public IP" for ingress purposes; TLS can terminate
either at the VPS or at home, your call.
Most flexible, most setup. Useful if you're behind CG-NAT, want a static public IP, or want geographically-distant ingress. Not yet wrapped as a Celilo module either; manual today.
firewall capability
operates inside the perimeter (DMZ container ↔ app ↔ secure). The
perimeter-fw box in the topology diagram is a stand-in for
whichever of these you choose.
Wireless
Wifi isn't part of the Celilo zone model. Connect to your ISP router's wifi
the way you always have — that's where laptops, phones, and IoT devices
live, and it's how you reach the orchestrator on internal.
Celilo's protected zones (dmz, app,
secure) are wired-only by design; there's no reason to want
clients on them over wifi.
Telling Celilo about it
Two pieces of state Celilo cares about: subnets and VLAN tags.
Subnets
celilo system init prompts for each zone's subnet during initial
setup. Defaults match the table above. You can also set them non-interactively:
celilo system config set network.dmz.subnet 10.0.10.0/24
celilo system config set network.app.subnet 10.0.20.0/24
celilo system config set network.secure.subnet 10.0.30.0/24
celilo system config set network.internal.subnet 192.168.1.0/24
Gateway IPs aren't prompted for. When you celilo machine add the
firewall machine, Celilo SSHes in and inspects its interfaces; the IP it
finds on each VLAN sub-interface (eth0.10, eth0.20,
and so on) becomes the canonical gateway for that zone.
VLAN tags
Celilo ships with VLAN tag defaults that match the diagrams above (dmz=10,
app=20, secure=30, internal=192). If your
switch uses different numbers — for example, a vendor default that conflicts —
override them:
celilo system config set network.dmz.vlan 110
celilo system config set network.app.vlan 120
celilo system config set network.secure.vlan 130
celilo system config set network.internal.vlan 1
Whatever numbers you choose here have to match the tags on your switch and the
sub-interface names on the firewall (eth0.110 instead of
eth0.10, etc.). They're the canonical record; modules pick up these
values automatically.
What modules see
When a module requests an LXC container or a machine in a zone (e.g.
requires.machine.zone: app in its manifest), Celilo auto-injects
the matching vlan, gateway, subnet, and
bridge values into that module's $self: namespace.
The module's Terraform template
then drops them straight into the Proxmox container spec:
resource "proxmox_lxc" "my_app" {
network {
name = "eth0"
bridge = "$self:bridge" # → vmbr0
tag = $self:vlan # → 20 (for the 'app' zone)
ip = "$self:target_ip"
gw = "$self:gateway" # → 10.0.20.1
}
}
You don't set per-module VLAN tags by hand. Declaring the zone is enough.
See apps/celilo/src/variables/context.ts for the auto-derivation
logic if you want to dig in.
celilo system init prompts for
subnets but not VLAN tags. The defaults are usually right, but if you need
custom numbers you have to set them with system config set
afterwards. Promoting VLAN tag prompts into the interactive init flow is a
likely improvement.
Once your network is wired and Celilo knows the numbers, you're done with this page. Head back to step 2 of the main walkthrough to add your Proxmox service.