
Inside an OVN VPC, instances talk to each other freely by default. For a serious application you want the opposite: the web tier reaching the app tier, the app tier reaching the database, and the web tier never touching the database. Incus network ACLs play exactly the role of Security Groups: rules that reference one another and apply per instance. This guide shows how to segment a three-tier application, and takes stock of the default reject, long caught out and fixed since Incus 6.22. Tested on an Incus OS cluster. For people hardening a private cloud.
What you will learn
- What an Incus network ACL is and how it relates to Security Groups.
- Write rules that reference other ACLs (role-based segmentation).
- Apply an ACL to an instance.
- The real state of the default reject, depending on your Incus version.
Prerequisites
- A working OVN network: see OVN in Incus. ACLs only apply to OVN networks.
- Instances already running on that network (for example
web1,api1,db1).
An Incus ACL is a Security Group
A network ACL is a set of ingress (incoming traffic) and egress rules that you attach to an instance. Its strength: a rule can name another ACL as its source or destination. Every instance carrying that ACL then forms a logical group, exactly like a Security Group referencing another one.
You create one group per tier:
incus network acl create tier-webincus network acl create tier-appincus network acl create tier-dbWriting the segmentation rules
The app tier only accepts the web tier, the database only accepts the app tier. You express that with source pointing at the ACL of the upstream tier:
incus network acl rule add tier-app ingress action=allow protocol=tcp source=tier-web destination_port=8080incus network acl rule add tier-db ingress action=allow protocol=tcp source=tier-app destination_port=5432The rule "allow port 8080 from tier-web" means: any instance carrying the tier-app ACL accepts port 8080 only from instances carrying tier-web. That is the Security Group mechanism.
Applying ACLs to instances
An ACL only takes effect once attached to the NIC of an instance. Since the eth0 device already exists (created by --network), you modify it with set rather than override:
incus config device set web1 eth0 security.acls=tier-web \ security.acls.default.ingress.action=drop \ security.acls.default.egress.action=allowRepeat for every instance: tier-web on the web tier, tier-app on the app tier, tier-db on the database. The security.acls.default.ingress.action=drop key sets a restrictive default policy: anything not explicitly allowed is blocked.
The default reject: a fixed bug, and what to keep from it
That default.ingress.action=drop should be enough to block the web tier from reaching the database. For a while it was not: on versions earlier than Incus 6.22, the implicit reject was not applied to east-west traffic, and the web tier reached the database when it should have been blocked. The cause was not in your rules but in the OVN programming of the default: the default egress action was set to allow, meaning without connection tracking, while any custom allow rule was itself translated into allow-related.
The defect has been fixed (lxc/incus#2851, PR #2985) by switching the default egress action to allow-related. The fix ships in Incus 6.22.0 and was backported to the 6.0 branch, so it is present in 6.0.6 LTS. Every supported version since March 2026, including 7.0 LTS, enforces the default reject correctly.
That said, the explicit drop rule keeps a value that depends on no bug at all. It writes the intent into the configuration instead of leaving it in an implicit default, it survives a change to default.ingress.action, and it reads back during an audit. This is the same logic as an explicit deny rule in a Security Group.
incus network acl rule add tier-db ingress action=drop source=tier-webAn explicit rule applies immediately, with no instance restart.
Checking the segmentation
You test from each tier towards a target port (with a service listening, or /dev/tcp for a plain connection test):
# from web1: web -> app must pass, web -> db must be blockedincus exec web1 -- bash -c 'echo > /dev/tcp/10.151.218.4/8080' && echo OPEN || echo BLOCKEDincus exec web1 -- bash -c 'echo > /dev/tcp/10.151.218.6/5432' && echo OPEN || echo BLOCKED# from api1: app -> db must passincus exec api1 -- bash -c 'echo > /dev/tcp/10.151.218.6/5432' && echo OPEN || echo BLOCKEDweb1 -> app:8080 OPENweb1 -> db:5432 BLOCKEDapi1 -> db:5432 OPENThe web tier reaches the app tier, the app tier reaches the database, but the web tier never touches the database. The three-tier application is segmented, the way it would be on a public cloud.
Key points
- Incus network ACLs are the Security Groups of the OVN VPC; they work only on OVN.
- A rule can reference another ACL as its
source: that is the role-based group (web, app, db). - An ACL is applied by setting it on the NIC (
security.aclsthroughconfig device set). - The default reject has been fixed since Incus 6.22 (backported to 6.0.6 LTS); below that, add explicit
droprules, which read better in any case. - Rules apply immediately, with no instance restart.