A VLAN (virtual LAN) splits one physical network into several isolated broadcast domains. Each Ethernet frame carries an 802.1Q tag with a VLAN ID, and the switch only delivers it to ports that belong to that VLAN. Putting management, production and development systems in separate VLANs means a compromised development host cannot reach your database directly: traffic between segments has to pass through a router, where you decide what is allowed. In this tutorial you will create VLAN interfaces on Ubuntu 24.04 with Netplan, turn a Linux server into the gateway between three VLANs, and use UFW to allow only the traffic each segment needs.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS with a non-root user that has sudo privileges. This server will be the gateway between VLANs.
  • A network interface on that server connected to a switch port configured as an 802.1Q trunk that carries VLANs 100, 200 and 300. This tutorial uses eth1 as the interface name; replace it with yours (for example enp2s0).
  • A separate interface for your SSH session (here eth0), or console access. Changing the network configuration of the interface you are connected through can cut you off.
  • At least one host in each VLAN to test with, connected to an access port of the matching VLAN.

VLAN tagging needs control over the switch and a physical or passed-through network card, so this setup is meant for dedicated servers and on-premises networks. On a typical virtual server the hypervisor does not pass tagged frames to the guest.

The addressing plan used throughout the guide:

VLAN IDPurposeSubnetGateway (this server)
100Management10.100.0.0/2410.100.0.1
200Production10.200.0.0/2410.200.0.1
300Development10.30.0.0/2410.30.0.1

Step 1 - Checking the interface and the 802.1Q module

List the network interfaces and identify the one connected to the trunk port:

ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0             UP             52:54:00:12:34:56 <BROADCAST,MULTICAST,UP,LOWER_UP>
eth1             UP             52:54:00:ab:cd:ef <BROADCAST,MULTICAST,UP,LOWER_UP>

Linux handles VLAN tags with the 8021q kernel module, which is loaded automatically when you create the first VLAN interface. You can confirm it is available:

sudo modprobe 8021q
lsmod | grep 8021q
8021q                  45056  0
garp                   20480  1 8021q
mrp                    20480  1 8021q

Before writing a permanent configuration, create a temporary VLAN interface to confirm that the switch trunk delivers tagged frames. The ip command creates an interface named eth1.100 that sends and receives frames tagged with VLAN 100 on eth1:

sudo ip link add link eth1 name eth1.100 type vlan id 100
sudo ip addr add 10.100.0.1/24 dev eth1.100
sudo ip link set dev eth1 up
sudo ip link set dev eth1.100 up

Show the details of the new interface. The -d flag prints the VLAN protocol and ID:

ip -d link show eth1.100
5: eth1.100@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:ab:cd:ef brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 0 maxmtu 65535
    vlan protocol 802.1Q id 100 <REORDER_HDR> addrgenmode eui64 numtxqueues 1 numrxqueues 1

Ping a host that is in VLAN 100, for example 10.100.0.10:

ping -c 3 10.100.0.10

If the ping works, the trunk is configured correctly. If not, check the switch port configuration before continuing. Remove the test interface, because Netplan will create the permanent ones:

sudo ip link delete eth1.100

Interfaces created with ip link disappear at reboot, which makes them useful for testing but not for production.

Step 3 - Creating persistent VLANs with Netplan

Ubuntu 24.04 configures networking with Netplan, which writes the configuration for systemd-networkd on servers. Create a separate Netplan file for the VLANs so you do not have to touch the file that configures eth0:

sudo nano /etc/netplan/60-vlans.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    eth1:
      dhcp4: false
      dhcp6: false
  vlans:
    vlan100:
      id: 100
      link: eth1
      addresses: [10.100.0.1/24]
    vlan200:
      id: 200
      link: eth1
      addresses: [10.200.0.1/24]
    vlan300:
      id: 300
      link: eth1
      addresses: [10.30.0.1/24]

The parent interface eth1 has no address of its own; it only carries the tagged traffic. Each entry under vlans defines the VLAN ID, the parent interface (link) and the address of the gateway in that VLAN. No default route is set on the VLAN interfaces, because this server reaches the internet through eth0.

Netplan warns when its files are readable by other users, since they can contain secrets. Restrict the permissions:

sudo chmod 600 /etc/netplan/60-vlans.yaml

Apply the configuration with netplan try. It applies the change and rolls it back automatically after 120 seconds unless you press Enter to confirm, so a mistake cannot lock you out:

sudo netplan try
Do you want to keep these settings?

Press ENTER before the timeout to accept the new configuration

Changes will revert in 118 seconds
Configuration accepted.

Verify that the three VLAN interfaces are up with their addresses:

ip -br addr show type vlan
vlan100@eth1     UP             10.100.0.1/24 fe80::5054:ff:feab:cdef/64
vlan200@eth1     UP             10.200.0.1/24 fe80::5054:ff:feab:cdef/64
vlan300@eth1     UP             10.30.0.1/24 fe80::5054:ff:feab:cdef/64

On Rocky Linux 9 or other NetworkManager systems

Distributions that use NetworkManager create the same VLAN interfaces with nmcli:

sudo nmcli connection add type vlan con-name vlan100 ifname vlan100 dev eth1 id 100 ipv4.method manual ipv4.addresses 10.100.0.1/24
sudo nmcli connection up vlan100

Repeat the commands for VLANs 200 and 300 with their IDs and addresses.

Step 4 - Enabling routing between VLANs

At this point the server has an address in each VLAN, but it does not forward packets from one VLAN to another. Enable IPv4 forwarding with a sysctl file so it persists across reboots:

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/60-ip-forward.conf
sudo sysctl --system

Check the value:

sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

On each host in the VLANs, set the server's address in that VLAN as the default gateway (for example 10.200.0.1 on production hosts). With forwarding enabled and no firewall yet, every VLAN can now reach every other VLAN. The next step closes that.

Step 5 - Restricting inter-VLAN traffic with UFW

The point of segmentation is to allow only the flows you need. In this example:

  • Management (VLAN 100) may reach production (VLAN 200) on SSH (22) and MySQL (3306).
  • Production (VLAN 200) may reach development (VLAN 300) on HTTP and HTTPS (80, 443).
  • Everything else between VLANs is denied.

UFW controls forwarded traffic with route rules. First make sure you do not lock yourself out: allow SSH on the management interface of the gateway itself before enabling UFW, if it is not already active:

sudo ufw allow in on eth0 to any port 22 proto tcp
sudo ufw allow in on vlan100 to any port 22 proto tcp

Set the default policy for routed traffic to deny:

sudo ufw default deny routed

Add the allowed flows. Each rule names the inbound and outbound VLAN interfaces, the destination subnet and the ports:

sudo ufw route allow in on vlan100 out on vlan200 to 10.200.0.0/24 port 22 proto tcp
sudo ufw route allow in on vlan100 out on vlan200 to 10.200.0.0/24 port 3306 proto tcp
sudo ufw route allow in on vlan200 out on vlan300 to 10.30.0.0/24 port 80,443 proto tcp

UFW is stateful, so reply packets of an allowed connection are accepted automatically; you do not need rules for the return direction.

If UFW is not active yet, enable it:

sudo ufw enable

If it was already active, reload it instead so the new default policy and rules take effect:

sudo ufw reload

List the rules:

sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ----        ----
22/tcp on eth0             ALLOW IN    Anywhere
22/tcp on vlan100          ALLOW IN    Anywhere

10.200.0.0/24 22/tcp on vlan200 ALLOW FWD Anywhere on vlan100
10.200.0.0/24 3306/tcp on vlan200 ALLOW FWD Anywhere on vlan100
10.30.0.0/24 80,443/tcp on vlan300 ALLOW FWD Anywhere on vlan200

Step 6 - Verifying the segmentation

Test from a management host in VLAN 100. Connections to the production database port are allowed:

nc -zv -w 3 10.200.0.10 3306
Connection to 10.200.0.10 3306 port [tcp/mysql] succeeded!

Connections from management to development are not in the allowed list and time out:

nc -zv -w 3 10.30.0.10 80
nc: connect to 10.30.0.10 port 80 (tcp) timed out: Operation now in progress

From a development host in VLAN 300, nothing in production is reachable:

nc -zv -w 3 10.200.0.10 22
nc: connect to 10.200.0.10 port 22 (tcp) timed out: Operation now in progress

On the gateway, you can watch the tagged frames on the trunk interface. The -e flag prints the link-level header, including the VLAN tag:

sudo tcpdump -i eth1 -e -n -c 5 vlan
10:42:01.118420 52:54:00:11:22:33 > 52:54:00:ab:cd:ef, ethertype 802.1Q (0x8100), length 78: vlan 100, p 0, ethertype IPv4 (0x0800), 10.100.0.10.51514 > 10.200.0.10.3306: Flags [S], seq 3021, win 64240, length 0

Blocked attempts are logged by UFW with the prefix [UFW BLOCK]. Follow them to see which flows are being denied:

sudo journalctl -k -f | grep 'UFW BLOCK'

Troubleshooting

The VLAN interface is up but hosts in the VLAN do not answer. Check that the switch port of the server is a trunk that carries the VLAN ID as tagged, and that the host's port is an access port in the same VLAN. sudo tcpdump -i eth1 -e -n vlan 100 shows whether tagged frames arrive at all.

Traffic between VLANs does not pass even with a route rule. Confirm net.ipv4.ip_forward = 1, and check that the hosts use this server as their gateway with ip route on each host. Without the right gateway, replies go elsewhere and connections fail.

Large packets are lost but pings work. The 4-byte VLAN tag can push frames over the MTU of some switches. Make sure the switch accepts at least 1522-byte frames, or lower the MTU with mtu: 1496 under the VLAN in Netplan.

netplan try reverts the configuration or reports an error. Run sudo netplan generate to see YAML errors, and check indentation: Netplan requires spaces, not tabs.

Conclusion

You created persistent 802.1Q VLAN interfaces with Netplan, turned the server into a router between three segments, and limited inter-VLAN traffic to the flows each segment needs. This gives you a default-deny boundary between management, production and development. As next steps, add a DHCP server per VLAN (for example with dnsmasq or Kea), enable IPv6 on the VLANs with the same route rules, and send the UFW logs to your central logging system to spot unexpected connection attempts between segments.