Proxmox OCI LXC Traffic Lockdown: Firewall Checklist

Lock down Proxmox OCI LXC containers with a dedicated bridge, pinned internal DNS, default-deny firewall rules, and hardened SSH to safely limit exposure.

11 min read
A glowing server container behind a hexagonal firewall barrier with red light specks stopped and a blue pin attached.

Locking down Proxmox VE 9.1 OCI LXC traffic means putting each container on a purpose-built bridge, forcing DNS through one internal resolver, and applying a default-deny Proxmox firewall before anything reaches the LAN or WAN. This checklist takes you from a fresh 9.1 node to a container that only answers to the ports and source ranges you explicitly allow, with SSH and fail2ban in place before you expose it.

Key Takeaways

  • Segment first: Put OCI LXC containers on a dedicated VLAN bridge, not the default Proxmox management bridge.
  • Default deny: Use default-deny inbound in the container firewall and allow only the source ranges and ports that need access.
  • Pin DNS: Set the container nameserver to an internal resolver and block direct public DNS in the outbound firewall rules.
  • Guard SSH: Harden host SSH, enable fail2ban, and limit container SSH to the admin VLAN or a bastion IP.
  • Test before expose: Verify reachability from allowed and blocked source IPs before opening the service to users.

How to segment OCI LXC traffic with Linux bridges

Start with the Proxmox VE 9.1 network audit if you have not already mapped which VLANs carry management, containers, and exposed services. The goal is simple: a container should not reach the Proxmox host, backup server, or other containers unless you explicitly allow it. If you are starting from a fresh node, run the Proxmox VE 9.1 homelab setup checklist before you add VLANs.

Create a management bridge and a container VLAN

Use a separate bridge for containers. The example below keeps Proxmox management on an untagged bridge and puts OCI LXC containers on VLAN 20. Adjust the interface name and addresses to match your switch.

# /etc/network/interfaces
auto lo
iface lo inet loopback

auto enp1s0
iface enp1s0 inet manual

auto vmbr0
iface vmbr0 inet static
    address 192.168.10.10/24
    gateway 192.168.10.1
    bridge-ports enp1s0
    bridge-stp off
    bridge-fd 0

auto enp1s0.20
iface enp1s0.20 inet manual

auto vmbr20
iface vmbr20 inet static
    address 192.168.20.10/24
    gateway 192.168.20.1
    bridge-ports enp1s0.20
    bridge-stp off
    bridge-fd 0

Reload the network configuration and confirm the bridge exists. If your console is on the same bridge, do this from IPMI or the Proxmox web console.

ifreload -a
ip -br link show

This takes about 15 minutes per container after the VLANs exist. The slow part is usually verifying switch trunks, not editing the Proxmox config.

Attach the OCI LXC to the VLAN bridge

Set the container's network interface to the new bridge. The firewall option on the interface enables the per-container firewall for that interface; the firewall file defines the rules.

# /etc/pve/lxc/201.conf
net0: name=eth0, bridge=vmbr20, firewall=1, hwaddr=BC:24:11:00:00:01
ipconfig0: ip=192.168.20.21/24,gw=192.168.20.1
nameserver: 192.168.20.2

You can apply the same settings from the command line. A fixed MAC address makes firewall and fail2ban logs easier to correlate.

pct config 201 --net0 name=eth0,bridge=vmbr20,firewall=1,hwaddr=BC:24:11:00:00:01
pct config 201 --ipconfig0 ip=192.168.20.21/24,gw=192.168.20.1
pct config 201 --nameserver 192.168.20.2
pct config 201 --firewall 1
pct stop 201
pct start 201

Do not leave the container on the default bridge just because it is already attached. The default bridge is convenient, but it is also the easiest way to give a new container accidental access to your management network.

How to pin DNS before the container talks to the world

DNS is the quiet leak in most container rollouts. An application can use the resolver you configured, or it can hard-code a public resolver, or the image can ship with its own resolver settings. Pin the resolver at the Proxmox level, then verify what the container actually uses.

Set the container resolver

Point the container at an internal resolver you control. A static search domain is optional, but it helps when your services use short names.

pct config 201 --nameserver 192.168.20.2
pct config 201 --searchdomain lan.example
pct stop 201
pct start 201

Verify what the container actually sees. Some OCI images run their own resolver, so the Proxmox setting is only the starting point.

pct exec 201 -- cat /etc/resolv.conf
pct exec 201 -- getent hosts lan.example

If the container shows a public nameserver, check whether the image ships a static resolver file or a local stub resolver. In that case, fix the image configuration or use a base image that honors the Proxmox-provided resolver.

Block direct public DNS from the firewall

Even if the container's resolver file is correct, an application can hard-code a public DNS address. Add outbound rules that drop direct public DNS and allow only your internal resolver.

# /etc/pve/firewall/201.fw
[OUT]
1: DROP udp dport 53 dst 8.8.8.8
2: DROP tcp dport 53 dst 8.8.8.8
3: ACCEPT udp dport 53 dst 192.168.20.2
4: ACCEPT tcp dport 53 dst 192.168.20.2

Reload the firewall after editing the file.

pve-firewall --reload

This does not stop DNS over HTTPS. If the container can reach port 443 on a public DNS-over-HTTPS endpoint, it can bypass plain DNS pinning. For most homelab and small production setups, pinning plain DNS and controlling egress is enough. For stricter setups, allow only the resolver, NTP, and specific upstream hosts.

How to write Proxmox firewall rules that actually lock the container down

The container firewall file is where the policy lives. Treat it like a mini access-control list: default deny inbound, explicit allow, and outbound rules only where you need to pin behavior.

Use default-deny inbound and explicit sources

Use default-deny inbound and explicit source ranges. The established-state rules let the container answer outbound connections without opening the door to every reply from the internet.

# /etc/pve/firewall/201.fw
[OPTIONS]
ENABLED: 1
POLICY_IN: DROP
POLICY_OUT: ACCEPT

[IN]
1: ACCEPT tcp state ESTABLISHED
2: ACCEPT udp state ESTABLISHED
3: ACCEPT tcp dport 22 src 192.168.10.0/24
4: ACCEPT tcp dport 22 src 192.168.20.10/32
5: ACCEPT tcp dport 80 src 192.168.20.0/24
6: ACCEPT tcp dport 443 src 192.168.20.0/24
7: ACCEPT icmp src 192.168.10.0/24

[OUT]
1: DROP udp dport 53 dst 8.8.8.8
2: DROP tcp dport 53 dst 8.8.8.8
3: ACCEPT udp dport 53 dst 192.168.20.2
4: ACCEPT tcp dport 53 dst 192.168.20.2

Enable the firewall and apply the rules.

pct config 201 --firewall 1
pve-firewall --reload
pct stop 201
pct start 201

Rule numbers matter. Lower numbers are evaluated first. If you put a broad accept before a specific drop, the drop will never run. Number your rules so the most specific exceptions come first.

Choose an egress posture

Posture Default outbound behavior What you must manage When to use
Default accept Any destination unless a drop rule matches Known bad destinations and noisy services Most OCI LXC containers that need updates or API calls
Pinned egress Only explicitly allowed destinations Every upstream dependency before production Batch workers, internal-only services, containers with secrets
NAT gateway Outbound filtered upstream of the container One gateway IP, centralized logging, and egress rules Multi-node setups where you want a single control point

For most pre-production containers, start with default outbound accept and add drop rules for known bad destinations. If the container stores secrets, talks to a payment API, or runs a batch worker, switch to pinned egress and allow only the resolver, NTP, and the specific upstream hosts.

How to harden SSH without breaking Proxmox recovery

SSH is the one port you will probably leave open on a container for operations. That makes it the highest-value target.

Host SSH drop-in

Restrict host SSH to key-based login and a small set of users. Run the test in a second terminal before you close the first one.

# /etc/ssh/sshd_config.d/10-proxmox-hardening.conf
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
LoginGraceTime 30
MaxAuthTries 3
AllowUsers root

Validate the config and reload the service.

sshd -t
systemctl reload ssh
ssh -o BatchMode=yes [email protected]

If you use local users other than root for operations, add them to the allow list before you reload. The recovery trap is not the firewall; it is the SSH drop-in that removes your only login method.

fail2ban on the Proxmox host

Install fail2ban on the Proxmox host and ban repeat offenders after a short window. This bans a source after 3 failed attempts within 60 seconds and holds the ban for one hour.

apt-get update
apt-get install -y fail2ban
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 22
backend = systemd
maxretry = 3
findtime = 60
bantime = 3600
systemctl enable --now fail2ban
fail2ban-client status sshd

You can ban and unban manually while testing.

fail2ban-client set sshd banip 203.0.113.10
fail2ban-client set sshd unbanip 203.0.113.10

fail2ban on the host protects node SSH. For container SSH, keep the Proxmox firewall source restriction and, if the container runs its own sshd, install fail2ban inside the container as well.

How to prove isolation before you expose the container

Do not expose the container until you have watched it fail from the wrong source IP. A firewall rule that looks correct in the file can still be hidden by a lower-numbered rule, a missing source range, or a bridge that is not where you think it is.

Run the verification matrix

Check Source Expected result
SSH from admin VLAN 192.168.10.0/24 Port 22 open
SSH from another container VLAN 192.168.20.30 Port 22 closed or filtered
HTTP from allowed LAN 192.168.20.0/24 Port 80 open
Direct public DNS from container 8.8.8.8 Blocked by outbound drop

Run the port checks from a workstation in the allowed VLAN, then repeat them from a blocked VLAN.

timeout 3 bash -c 'echo > /dev/tcp/192.168.20.21/22' && echo 'SSH open' || echo 'SSH closed or filtered'
timeout 3 bash -c 'echo > /dev/tcp/192.168.20.21/80' && echo 'HTTP open' || echo 'HTTP closed or filtered'

Check the container's resolver and the firewall file on the Proxmox host.

pct exec 201 -- cat /etc/resolv.conf
pct exec 201 -- getent hosts lan.example
cat /etc/pve/firewall/201.fw
pct config 201 | grep -E '^(net0|firewall)'

Once the container is locked down, the Proxmox OCI LXC edge stack article shows how to put Caddy or Nginx in front of it without opening extra ports.

Common gotchas and tradeoffs

The most common mistake is putting the OCI LXC on the default bridge because it is already there. Proxmox firewall will still filter the interface, but the container is now in the same broadcast domain as your Proxmox host, NAS, and lab devices. I have lost a console session when I redefined the management bridge from a workstation on that same bridge; use IPMI or the web console for that change.

Firewall rules are evaluated by number, not by section. A broad accept with a lower number will hide a specific drop that comes after it. Number your rules so the most specific exceptions come first.

The firewall file is usually under 1 KB, but a single missing source range can lock you out of SSH. Keep a known-good admin source in the allow list until you have a second recovery path.

Default outbound accept keeps unexpected API calls working and avoids breaking established replies. The cost is that the container can initiate connections to any destination unless you add drop rules. If you switch to default outbound drop, you get a tighter egress boundary, but you must test every upstream dependency before production.

Proxmox firewall does not inspect DNS payloads. It controls which resolver address the container can reach, not what the application asks for. If you need payload-level control, put a proxy or NAT gateway in front of the container.

Conclusion

Once the bridge, DNS, firewall, and SSH rules are in place, your OCI LXC container is no longer a default-accept appliance; it is a scoped service that only talks to the networks you named. The next step is to run the verification matrix from a second workstation and only then expose the service through your edge stack or reverse proxy.

Share
Proxmox Pulse

Written by

Proxmox Pulse

Sysadmin-driven guides for getting the most out of Proxmox VE in production and homelab environments.

Related Articles

View all →