Polkit access control

Libvirt's client access control framework allows administrators to setup fine grained permission rules across client users, managed objects and API operations. This allows client connections to be locked down to a minimal set of privileges. The polkit driver provides a simple implementation of the access control framework.

Introduction

A default install of libvirt will typically use polkit to authenticate the initial user connection to libvirtd. This is a very coarse grained check though, either allowing full read-write access to all APIs, or just read-only access. The polkit access control driver in libvirt builds on this capability to allow for fine grained control over the operations a user may perform on an object.

Permission names

The libvirt object names and permission names are mapped onto polkit action names using the simple pattern:

org.libvirt.api.$object.$permission

The only caveat is that any underscore characters in the object or permission names are converted to hyphens. So, for example, the search_storage_vols permission on the storage_pool object maps to the polkit action:

org.libvirt.api.storage-pool.search-storage-vols

The default policy for any permission which corresponds to a "read only" operation, is to allow access. All other permissions default to deny access.

Object identity attributes

To allow polkit authorization rules to be written to match against individual object instances, libvirt provides a number of authorization detail attributes when performing a permission check. The set of attributes varies according to the type of object being checked

virConnectPtr

AttributeDescription
connect_driverName of the libvirt connection driver

virDomainPtr

AttributeDescription
connect_driverName of the libvirt connection driver
domain_nameName of the domain, unique to the local host
domain_uuidUUID of the domain, globally unique

virInterfacePtr

AttributeDescription
connect_driverName of the libvirt connection driver
interface_nameName of the network interface, unique to the local host
interface_macaddrMAC address of the network interface, not unique

virNetworkPtr

AttributeDescription
connect_driverName of the libvirt connection driver
network_nameName of the network, unique to the local host
network_uuidUUID of the network, globally unique

virNodeDevicePtr

AttributeDescription
connect_driverName of the libvirt connection driver
node_device_nameName of the node device, unique to the local host

virNWFilterPtr

AttributeDescription
connect_driverName of the libvirt connection driver
nwfilter_nameName of the network filter, unique to the local host
nwfilter_uuidUUID of the network filter, globally unique

virSecretPtr

AttributeDescription
connect_driverName of the libvirt connection driver
secret_uuidUUID of the secret, globally unique
secret_usage_volumeName of the associated volume, if any
secret_usage_cephName of the associated Ceph server, if any
secret_usage_targetName of the associated iSCSI target, if any

virStoragePoolPtr

AttributeDescription
connect_driverName of the libvirt connection driver
pool_nameName of the storage pool, unique to the local host
pool_uuidUUID of the storage pool, globally unique

virStorageVolPtr

AttributeDescription
connect_driverName of the libvirt connection driver
pool_nameName of the storage pool, unique to the local host
pool_uuidUUID of the storage pool, globally unique
vol_nameName of the storage volume, unique to the pool
vol_keyKey of the storage volume, globally unique

User identity attributes

At this point in time, the only attribute provided by libvirt to identify the user invoking the operation is the PID of the client program. This means that the polkit access control driver is only useful if connections to libvirt are restricted to its UNIX domain socket. If connections are being made to a TCP socket, no identifying information is available and access will be denied. Also note that if the client is connecting via an SSH tunnel, it is the local SSH user that will be identified. In future versions, it is expected that more information about the client user will be provided, including the SASL / Kerberos username and/or x509 distinguished name obtained from the authentication provider in use.

Writing access control policies

If using versions of polkit prior to 0.106 then it is only possible to validate (user, permission) pairs via the .pkla files. Fully validation of the (user, permission, object) triple requires the new JavaScript .rules support that was introduced in version 0.106. The latter is what will be described here.

Libvirt does not ship any rules files by default. It merely provides a definition of the default behaviour for each action (permission). As noted earlier, permissions which correspond to read-only operations in libvirt will be allowed to all users by default; everything else is denied by default. Defining custom rules requires creation of a file in the /etc/polkit-1/rules.d directory with a name chosen by the administrator (100-libvirt-acl.rules would be a reasonable choice). See the polkit(8) manual page for a description of how to write these files in general. The key idea is to create a file containing something like

      polkit.addRule(function(action, subject) {
        ....logic to check 'action' and 'subject'...
      });
    

In this code snippet above, the action object instance will represent the libvirt permission being checked along with identifying attributes for the object it is being applied to. The subject meanwhile will identify the libvirt client app (with the caveat above about it only dealing with local clients connected via the UNIX socket). On the action object, the permission name is accessible via the id attribute, while the object identifying attributes are exposed via the lookup method.

Example: restricting ability to connect to drivers

Consider a local user berrange who has been granted permission to connect to libvirt in full read-write mode. The goal is to only allow them to use the QEMU driver and not the Xen or LXC drivers which are also available in libvirtd. To achieve this we need to write a rule which checks whether the connect_driver attribute is QEMU, and match on an action name of org.libvirt.api.connect.getattr. Using the javascript rules format, this ends up written as

polkit.addRule(function(action, subject) {
    if (action.id == "org.libvirt.api.connect.getattr" &&
        subject.user == "berrange") {
          if (action.lookup("connect_driver") == 'QEMU') {
            return polkit.Result.YES;
          } else {
            return polkit.Result.NO;
          }
    }
});
    

Example: restricting access to a single domain

Consider a local user berrange who has been granted permission to connect to libvirt in full read-write mode. The goal is to only allow them to see the domain called demo on the LXC driver. To achieve this we need to write a rule which checks whether the connect_driver attribute is LXC and the domain_name attribute is demo, and match on a action name of org.libvirt.api.domain.getattr. Using the javascript rules format, this ends up written as

polkit.addRule(function(action, subject) {
    if (action.id == "org.libvirt.api.domain.getattr" &&
        subject.user == "berrange") {
          if (action.lookup("connect_driver") == 'LXC' &&
              action.lookup("domain_name") == 'demo') {
            return polkit.Result.YES;
          } else {
            return polkit.Result.NO;
          }
    }
});