How do you use libvirt rootless?

I know I’ve done this but I forgot how and I haven’t used it in many years now.

But there is a way to use libvirt and virt-manager to create VMs and networks without requiring root.

I have of course added myself to the libvirt group, and rebooted to verify that with id my-user.

I have also edited /etc/libvirt/libvirtd.conf.

$ sudo grep -v '^#' /etc/libvirt/libvirtd.conf | grep -v '^$'
unix_sock_group = "libvirt"
unix_sock_ro_perms = "0777"
unix_sock_rw_perms = "0770"
auth_unix_ro = "polkit"
auth_unix_rw = "polkit"

And I can define a network but the problem is that I can’t start it, make it active. If I connect to qemu:///session in virt-manager and try to start it I get asked for my password, and get a permission denied exception.

Error starting network 'default': error creating bridge interface virbr0: Operation not permitted

Traceback (most recent call last):
  File "/usr/share/virt-manager/virtManager/asyncjob.py", line 71, in cb_wrapper
    callback(asyncjob, *args, **kwargs)
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/share/virt-manager/virtManager/asyncjob.py", line 107, in tmpcb
    callback(*args, **kwargs)
    ~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/share/virt-manager/virtManager/object/libvirtobject.py", line 57, in newfn
    ret = fn(self, *args, **kwargs)
  File "/usr/share/virt-manager/virtManager/object/network.py", line 69, in start
    self._backend.create()
    ~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.13/site-packages/libvirt.py", line 3569, in create
    raise libvirtError('virNetworkCreate() failed')
libvirt.libvirtError: error creating bridge interface virbr0: Operation not permitted

On my old system many years ago I used to just have this virbr0 on at boot and could freely create VMs without root after that. But if I define it in qemu:///system then my session VMs won’t find it when I specify the network name. Should I perhaps specify the bridge name instead?

Just trying to remember all the tricks I went through to get this working in the past. I’m pretty sure setting up the initial bridge did require root, but then it was just there and ready to be used.

My user has wheel & libvirt group membership.
I have installed libvirt, qemu, and virt-manager

With those choices, when I launch VMM (virt manager) it takes a few seconds to connect to the libvirt daemon but once connected I have full control with the gui to create/delete/run VMs on my system.

Launching VMM automatically creates the virbr0 bridge interface without manual intervention. Then when I create a VM I tell it to use the bridge interface (default is NAT) and it just works.

The only part of that which required root was the installation of the supporting packages. The rest is done as my regular user.

I did not alter the file /etc/libvirt/libvirtd.conf from the default. Every line in that file is either commented out with # or is a blank line so libvirtd runs purely with default settings.

You can also define a system mode NAT network and session mode bridge with the same interface name, then starting with F43 virt-manager allows selecting it session mode:
virt-manager doesn't list User Session virtual networks when creating new User Session VM · Issue #863 · virt-manager/virt-manager · GitHub

I think that is what I ended up doing. Creating a system-wide virbr0 using root, then simply specifying the bridge name instead of the network name when I create VMs in session.

But there were some steps involved just to get my libvirt to a working state, this is the Ansible playbook I had to write.

---
- name: Bootstrap libvirtd
  hosts: localhost
  become: true
  tasks:
    - name: Enable the main VM management socket
      systemd:
        name: virtqemud.socket
        enabled: true
        state: started

    - name: Enable the network management socket
      systemd:
        name: virtnetworkd.socket
        enabled: true
        state: started

    - name: Enable the storage management socket
      systemd:
        name: virtstoraged.socket
        enabled: true
        state: started

    - name: Define the default libvirt network
      community.libvirt.virt_net:
        command: define
        name: default
        uri: "qemu:///system"
        xml: "{{ lookup('template', 'net.xml') }}"

    - name: Ensure the default libvirt network is active
      community.libvirt.virt_net:
        name: default
        uri: "qemu:///system"
        state: active

    - name: Allow unprivileged users to use virtualization
      ansible.posix.seboolean:
        name: unprivuser_use_svirt
        state: true
        persistent: true

    - name: Clear cached facts to prevent cache poisoning
      meta: clear_facts

I think maybe I should have set the sebool staff_use_svirtinstead, because I am in wheel and libvirt groups after all.