mirror of
https://github.com/QubesOS/qubes-doc.git
synced 2024-12-18 04:04:39 -05:00
519 lines
19 KiB
ReStructuredText
519 lines
19 KiB
ReStructuredText
===========================================
|
||
Qrexec: secure communication across domains
|
||
===========================================
|
||
|
||
|
||
(*This page is about qrexec v3. For qrexec v2, see*
|
||
:doc:`here </developer/services/qrexec2>` *.*)
|
||
|
||
The **qrexec framework** is used by core Qubes components to implement
|
||
communication between domains. Qubes domains are strictly isolated by
|
||
design. However, the OS needs a mechanism to allow the administrative
|
||
domain (dom0) to force command execution in another domain (VM). For
|
||
instance, when a user selects an application from the KDE menu, it
|
||
should start in the selected VM. Also, it is often useful to be able to
|
||
pass stdin/stdout/stderr from an application running in a VM to dom0
|
||
(and the other way around). (For example, so that a VM can notify dom0
|
||
that there are updates available for it). By default, Qubes allows VMs
|
||
to initiate such communications in specific circumstances. The qrexec
|
||
framework generalizes this process by providing a remote procedure call
|
||
(RPC) for the Qubes architecture. It allows users and developers to use
|
||
and design secure inter-VM tools.
|
||
|
||
Qrexec basics: architecture and examples
|
||
----------------------------------------
|
||
|
||
|
||
Qrexec is built on top of *vchan*, a Xen library providing data links
|
||
between VMs. During domain startup, a process named ``qrexec-daemon`` is
|
||
started in dom0, and a process named ``qrexec-agent`` is started in the
|
||
VM. They are connected over a **vchan** channel. ``qrexec-daemon``
|
||
listens for connections from a dom0 utility named ``qrexec-client``.
|
||
Let’s say we want to start a process (call it ``VMprocess``) in a VM
|
||
(``someVM``). Typically, the first thing that a ``qrexec-client``
|
||
instance does is to send a request to the ``qrexec-daemon``, which in
|
||
turn relays it to ``qrexec-agent`` running in ``someVM``.
|
||
``qrexec-daemon`` assigns unique vchan connection details and sends them
|
||
to both ``qrexec-client`` (in dom0) and ``qrexec-agent`` (in
|
||
``someVM``). ``qrexec-client`` starts a vchan server, which
|
||
``qrexec-agent`` then connects to. Once this channel is established,
|
||
stdin/stdout/stderr from the VMprocess is passed between
|
||
``qrexec-agent`` and the ``qrexec-client`` process.
|
||
|
||
.. figure:: /attachment/doc/qrexec3-basics.png
|
||
:alt: qrexec basics diagram
|
||
|
||
qrexec basics diagram
|
||
|
||
The ``qrexec-client`` command is used to make connections to VMs from
|
||
dom0. For example, the following command creates an empty file called
|
||
``hello-world.txt`` in the home folder of ``someVM``:
|
||
|
||
.. code:: bash
|
||
|
||
$ qrexec-client -e -d someVM user:'touch hello-world.txt'
|
||
|
||
|
||
|
||
The string before the colon specifies which user will run the command.
|
||
The ``-e`` flag tells ``qrexec-client`` to exit immediately after
|
||
sending the execution request and receiving a status code from
|
||
``qrexec-agent`` (if the process creation succeeded). With this option,
|
||
no further data is passed between the domains. The following command
|
||
demonstrates an open channel between dom0 and someVM (in this case, a
|
||
remote shell):
|
||
|
||
.. code:: bash
|
||
|
||
$ qrexec-client -d someVM user:bash
|
||
|
||
|
||
|
||
The ``qvm-run`` command is heavily based on ``qrexec-client``. It also
|
||
handles additional activities, e.g. starting the domain if the domain is
|
||
not up yet and starting the GUI daemon. It is usually more convenient to
|
||
use ``qvm-run``.
|
||
|
||
There can be an almost arbitrary number of ``qrexec-client`` processes
|
||
for a given domain. The limiting factor is the number of available vchan
|
||
channels, which depends on the underlying hypervisor, as well the
|
||
domain’s OS.
|
||
|
||
For more details on the qrexec framework and protocol, see “:doc:`Qubes RPC internals </developer/services/qrexec-internals>`.”
|
||
|
||
Qubes RPC services
|
||
------------------
|
||
|
||
|
||
Some common tasks (like copying files between VMs) have an RPC-like
|
||
structure: a process in one VM (say, the file sender) needs to invoke
|
||
and send/receive data to some process in other VM (say, the file
|
||
receiver). The Qubes RPC framework was created to securely facilitate a
|
||
range of such actions.
|
||
|
||
Inter-VM communication must be tightly controlled to prevent one VM from
|
||
taking control of another, possibly more privileged, VM. The design
|
||
decision was made to pass all control communication via dom0 which can
|
||
enforce proper authorization. It is therefore natural to reuse the
|
||
already-existing qrexec framework.
|
||
|
||
Note that bare qrexec provides ``VM <-> dom0`` connectivity, but the
|
||
command execution is always initiated by dom0. There are cases when a VM
|
||
needs to invoke and send data to a command in dom0 (e.g. to pass
|
||
information on newly installed ``.desktop`` files). This framework
|
||
allows dom0 to be the RPC target as well.
|
||
|
||
Thanks to the framework, RPC programs are very simple – both RPC client
|
||
and server just use their stdin/stdout to pass data. The framework does
|
||
all the inner work to connect the processes to eachother via
|
||
``qrexec-daemon`` and ``qrexec-agent``. Disposable VMs are tightly
|
||
integrated – RPC to a DisposableVM is identical to RPC to an AppVM or
|
||
StandaloneVM: all one needs is to pass ``@dispvm`` as the remote domain
|
||
name.
|
||
|
||
Qubes RPC administration
|
||
------------------------
|
||
|
||
|
||
Policy files
|
||
^^^^^^^^^^^^
|
||
|
||
| The dom0 directory ``/etc/qubes/policy.d/`` contains files that set
|
||
policy for each available RPC action that a VM might call. For
|
||
example, ``/etc/qubes/policy.d/90-default.policy`` contains the
|
||
default policy settings.
|
||
| When making changes to existing policies it is recommended that you
|
||
create a *new* policy file starting with a lower number, like
|
||
``/etc/qubes/policy.d/30-user.policy``.
|
||
| You may keep your custom policies in one file like
|
||
``/etc/qubes/policy.d/30-user.policy``, or you may choose to have
|
||
multiple files, like ``/etc/qubes/policy.d/10-copy.policy``,
|
||
``/etc/qubes/policy.d/10-open.policy``.
|
||
| Together the contents of these files make up the RPC access policy
|
||
database: the files are merged, with policies in lower number files
|
||
overriding policies in higher numbered files.
|
||
|
||
|
||
Policies are defined in lines with the following format:
|
||
|
||
.. code:: bash
|
||
|
||
service-name|* +argument|* source destination action [options]
|
||
|
||
|
||
|
||
You can specify the source and destination by name or by one of the
|
||
reserved keywords such as ``*``, ``@dispvm``, or ``dom0``. (Of these
|
||
three, only ``*`` keyword makes sense in the source field. Service calls
|
||
from dom0 are currently always allowed, and ``@dispvm`` means “new VM
|
||
created for this particular request,” so it is never a source of
|
||
request.) Other methods using *tags* and *types* are also available (and
|
||
discussed below).
|
||
|
||
Whenever a RPC request for an action is received, the domain checks the
|
||
first matching line of the files in ``/etc/qubes/policy.d/`` to
|
||
determine access: whether to allow the request, what VM to redirect the
|
||
execution to, and what user account the program should run under. Note
|
||
that if the request is redirected (``target=`` parameter), policy action
|
||
remains the same – even if there is another rule which would otherwise
|
||
deny such request. If no policy rule is matched, the action is denied.
|
||
|
||
In the target VM, a file in either of the following locations must
|
||
exist, containing the file name of the program that will be invoked, or
|
||
being that program itself – in which case it must have executable
|
||
permission set (``chmod +x``): - ``/etc/qubes-rpc/RPC_ACTION_NAME`` when
|
||
you make it in the template qube; -
|
||
``/usr/local/etc/qubes-rpc/RPC_ACTION_NAME`` for making it only in an
|
||
app qube.
|
||
|
||
Making an RPC call
|
||
^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
From outside of dom0, RPC calls take the following form:
|
||
|
||
.. code:: bash
|
||
|
||
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME rpc_client_path client arguments
|
||
|
||
|
||
|
||
For example:
|
||
|
||
.. code:: bash
|
||
|
||
$ qrexec-client-vm work qubes.StartApp+firefox
|
||
|
||
|
||
|
||
Note that only stdin/stdout is passed between RPC server and client –
|
||
notably, no command line arguments are passed. By default, stderr of
|
||
client and server is logged in the syslog/journald of the VM where the
|
||
process is running.
|
||
|
||
It is also possible to call service without specific client program – in
|
||
which case server stdin/out will be connected with the terminal:
|
||
|
||
.. code:: bash
|
||
|
||
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME
|
||
|
||
|
||
|
||
Specifying VMs: tags, types, targets, etc.
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
There are severals methods for specifying source/target VMs in RPC
|
||
policies.
|
||
|
||
- ``@tag:some-tag`` - meaning a VM with tag ``some-tag``
|
||
|
||
- ``@type:type`` - meaning a VM of ``type`` (like ``AppVM``,
|
||
``TemplateVM`` etc)
|
||
|
||
|
||
|
||
Target VM can be also specified as ``@default``, which matches the case
|
||
when calling VM didn’t specified any particular target (either by using
|
||
``@default`` target, or empty target). For DisposableVMs,
|
||
``@dispvm:DISP_VM`` is very similar to ``@dispvm`` but forces using a
|
||
particular VM (``DISP_VM``) as a base VM to be started as DisposableVM.
|
||
For example:
|
||
|
||
.. code:: bash
|
||
|
||
* * anon-whonix @dispvm:anon-whonix-dvm allow
|
||
|
||
|
||
|
||
Adding such policy itself will not force usage of this particular
|
||
``DISP_VM`` - it will only allow it when specified by the caller. But
|
||
``@dispvm:DISP_VM`` can also be used as target in request redirection,
|
||
so *it is possible* to force particular ``DISP_VM`` usage, when caller
|
||
didn’t specify it:
|
||
|
||
.. code:: bash
|
||
|
||
* * anon-whonix @dispvm allow target=@dispvm:anon-whonix-dvm
|
||
|
||
|
||
|
||
Note that without redirection, this rule would allow using default
|
||
Disposable VM (``default_dispvm`` VM property, which itself defaults to
|
||
global ``default_dispvm`` property). Also note that the request will be
|
||
allowed (``allow`` action) even if there is no second rule allowing
|
||
calls to ``@dispvm:anon-whonix-dvm``, or even if there is a rule
|
||
explicitly denying it. This is because the redirection happens *after*
|
||
considering the action.
|
||
|
||
The policy confirmation dialog (``ask`` action) allows the user to
|
||
specify target VM. User can choose from VMs that, according to policy,
|
||
would lead to ``ask`` or ``allow`` actions. It is not possible to select
|
||
VM that policy would deny. By default no VM is selected, even if the
|
||
caller provided some, but policy can specify default value using
|
||
``default_target=`` parameter. For example:
|
||
|
||
.. code:: bash
|
||
|
||
* * work-mail work-archive allow
|
||
* * work-mail @tag:work ask default_target=work-files
|
||
* * work-mail @default ask default_target=work-files
|
||
|
||
|
||
|
||
The first rule allows calls from ``work-mail`` to ``work-archive``,
|
||
without any confirmation. The second rule will ask the user about calls
|
||
from ``work-mail`` VM to any VM with tag ``work``. And the confirmation
|
||
dialog will have ``work-files`` VM chosen by default, regardless of the
|
||
VM specified by the caller (``work-mail`` VM). The third rule allows the
|
||
caller to not specify target VM at all and let the user choose, still -
|
||
from VMs with tag ``work`` (and ``work-archive``, regardless of tag),
|
||
and with ``work-files`` as default.
|
||
|
||
RPC services and security
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
Be very careful when coding and adding a new RPC service. Unless the
|
||
offered functionality equals full control over the target (it is the
|
||
case with e.g. ``qubes.VMShell`` action), any vulnerability in an RPC
|
||
server can be fatal to Qubes security. On the other hand, this mechanism
|
||
allows to delegate processing of untrusted input to less privileged (or
|
||
disposable) AppVMs, thus wise usage of it increases security.
|
||
|
||
For example, this command will run the ``firefox`` command in a
|
||
DisposableVM based on ``work``:
|
||
|
||
.. code:: bash
|
||
|
||
$ qvm-run --dispvm=work firefox
|
||
|
||
|
||
|
||
By contrast, consider this command:
|
||
|
||
.. code:: bash
|
||
|
||
$ qvm-run --dispvm=work --service qubes.StartApp+firefox
|
||
|
||
|
||
|
||
This will look for a ``firefox.desktop`` file in a standard location in
|
||
a DisposableVM based on ``work``, then launch the application described
|
||
by that file. The practical difference is that the bare ``qvm-run``
|
||
command uses the ``qubes.VMShell`` service, which allows you to run an
|
||
arbitrary command with arbitrary arguments, essentially providing full
|
||
control over the target VM. By contrast, the ``qubes.StartApp`` service
|
||
allows you to run only applications that are advertised in
|
||
``/usr/share/applications`` (or other standard locations) *without*
|
||
control over the arguments, so giving a VM access to ``qubes.StartApp``
|
||
is much safer. While there isn’t much practical difference between the
|
||
two commands above when starting an application from dom0 in Qubes 4.0,
|
||
there is a significant security risk when launching applications from a
|
||
domU (e.g., from a separate GUI domain). This is why ``qubes.StartApp``
|
||
uses our standard ``qrexec`` argument grammar to strictly filter the
|
||
permissible grammar of the ``Exec=`` lines in ``.desktop`` files that
|
||
are passed from untrusted domUs to dom0, thereby protecting dom0 from
|
||
command injection by maliciously-crafted ``.desktop`` files.
|
||
|
||
Service policies with arguments
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
Sometimes a service name alone isn’t enough to make reasonable qrexec
|
||
policy. One example of such a situation is :doc:`qrexec-based USB passthrough </user/how-to-guides/how-to-use-usb-devices>`. Using just a service name
|
||
would make it difficult to express the policy “allow access to devices X
|
||
and Y, but deny to all others.” It isn’t feasible to create a separate
|
||
service for every device: we would need to change the code in multiple
|
||
files any time we wanted to update the service.
|
||
|
||
For this reason it is possible to specify a service argument, which will
|
||
be subject to a policy. A service argument can make service policies
|
||
more fine-grained. With arguments, it is easier to write more precise
|
||
policies using the “allow” and “deny” actions, instead of relying on the
|
||
“ask” method. (Writing too many “ask” policies offloads additional
|
||
decisions to the user. Generally, the fewer choices the user must make,
|
||
the lower the chance to make a mistake.)
|
||
|
||
The argument is specified in the second column of the policy line, as
|
||
+ARGUMENT. If the policy uses “*” as an argument, then it will match
|
||
any argument (including no argument). As rules are processed in order,
|
||
any lines with a specific argument below the line with the wildcard
|
||
argument will be ignored. So for instance, we might have policies which
|
||
are different depending on the argument:
|
||
|
||
.. code:: bash
|
||
|
||
Device +device1 * * allow
|
||
Device +device2 * * deny
|
||
Device * * * ask
|
||
|
||
|
||
|
||
When calling a service that takes an argument, just add the argument to
|
||
the service name separated with ``+``.
|
||
|
||
.. code:: bash
|
||
|
||
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME+ARGUMENT
|
||
|
||
|
||
|
||
The script will receive ``ARGUMENT`` as its argument. The argument will
|
||
also become available as the ``QREXEC_SERVICE_ARGUMENT`` environment
|
||
variable. This means it is possible to install a different script for a
|
||
particular service argument.
|
||
|
||
See `below <#rpc-service-with-argument-file-reader>`__ for an example of
|
||
an RPC service using an argument.
|
||
<!-- TODO document "Yes to All" authorization if it is reintroduced -->
|
||
|
||
Qubes RPC examples
|
||
------------------
|
||
|
||
|
||
To demonstrate some of the possibilities afforded by the qrexec
|
||
framework, here are two examples of custom RPC services.
|
||
|
||
Simple RPC service (addition)
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
We can create an RPC service that adds two integers in a target domain
|
||
(the server, call it “anotherVM”) and returns back the result to the
|
||
invoker (the client, “someVM”). In someVM, create a file with the
|
||
following contents and save it with the path
|
||
``/usr/bin/our_test_add_client``:
|
||
|
||
.. code:: bash
|
||
|
||
#!/bin/sh
|
||
echo $1 $2 # pass data to RPC server
|
||
exec cat >&$SAVED_FD_1 # print result to the original stdout, not to the other RPC endpoint
|
||
|
||
|
||
|
||
Our server will be anotherVM at ``/usr/bin/our_test_add_server``. The
|
||
code for this file is:
|
||
|
||
.. code:: bash
|
||
|
||
#!/bin/sh
|
||
read arg1 arg2 # read from stdin, which is received from the RPC client
|
||
echo $(($arg1+$arg2)) # print to stdout, which is passed to the RPC client
|
||
|
||
|
||
|
||
We’ll need to create a service called ``test.Add`` with its own
|
||
definition and policy file in dom0. Now we need to define what the
|
||
service does. In this case, it should call our addition script. We
|
||
define the service with a symlink at ``/etc/qubes-rpc/test.Add``
|
||
pointing to our server script (the script can be also placed directly in
|
||
``/etc/qubes-rpc/test.Add`` - make sure the file has executable bit
|
||
set!):
|
||
|
||
.. code:: bash
|
||
|
||
ln -s /usr/bin/our_test_add_server /etc/qubes-rpc/test.Add
|
||
|
||
|
||
|
||
The administrative domain will direct traffic based on the current RPC
|
||
policies. In dom0, create a file at
|
||
``/etc/qubes/policy.d/30-test.policy`` containing the following:
|
||
|
||
.. code:: bash
|
||
|
||
test.Add * * * ask
|
||
|
||
|
||
|
||
This will allow our client and server to communicate.
|
||
|
||
Before we make the call, ensure that the client and server scripts have
|
||
executable permissions. Finally, invoke the RPC service.
|
||
|
||
.. code:: bash
|
||
|
||
$ qrexec-client-vm anotherVM test.Add /usr/bin/our_test_add_client 1 2
|
||
|
||
|
||
|
||
We should get “3” as answer. (dom0 will ask for confirmation first.)
|
||
|
||
**Note:** For a real world example of writing a qrexec service, see this
|
||
`blog post <https://blog.invisiblethings.org/2013/02/21/converting-untrusted-pdfs-into-trusted.html>`__.
|
||
|
||
RPC service with argument (file reader)
|
||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
|
||
Here we create an RPC call that reads a specific file from a predefined
|
||
directory on the target. This example uses an
|
||
`argument <#service-policies-with-arguments>`__ to the policy. In this
|
||
example a simplified workflow will be used. The service code is placed
|
||
directly in the service definition file on the target VM. No separate
|
||
client script will be needed.
|
||
|
||
First, on your target VM, create two files in the home directory:
|
||
``testfile1`` and ``testfile2``. Have them contain two different “Hello
|
||
world!” lines.
|
||
|
||
Next, we define the RPC service. On the target VM, place the code below
|
||
at ``/etc/qubes-rpc/test.File``:
|
||
|
||
.. code:: bash
|
||
|
||
#!/bin/sh
|
||
argument="$1" # service argument, also available as $QREXEC_SERVICE_ARGUMENT
|
||
if [ -z "$argument" ]; then
|
||
echo "ERROR: No argument given!"
|
||
exit 1
|
||
fi
|
||
cat "/home/user/$argument"
|
||
|
||
|
||
|
||
Make sure the file is executable! (The service argument is already
|
||
sanitized by qrexec framework. It is guaranteed to not contain any
|
||
spaces or slashes, so there should be no need for additional path
|
||
sanitization.)
|
||
|
||
Now we create the policy file in dom0, at
|
||
``/etc/qubes/policy.d/30-test.policy``. The contents of the file are
|
||
below. Replace “source_vm1” and others with the names of your own chosen
|
||
domains.
|
||
|
||
.. code:: bash
|
||
|
||
test.File +testfile1 source_vm1 target_vm allow
|
||
test.File +testfile2 source_vm2 target_vm allow
|
||
test.File * * * deny
|
||
|
||
|
||
|
||
With this done, we can run some tests. Invoke RPC from ``source_vm1``
|
||
via
|
||
|
||
.. code:: bash
|
||
|
||
[user@source_vm1] $ qrexec-client-vm target_vm test.File+testfile1
|
||
|
||
|
||
|
||
We should get the contents of ``/home/user/testfile1`` printed to the
|
||
terminal. Invoking the service from ``source_vm2`` should result in a
|
||
denial, but ``testfile2`` should work.
|
||
|
||
.. code:: bash
|
||
|
||
[user@source_vm2] $ qrexec-client-vm target_vm test.File+testfile1
|
||
Request refused
|
||
[user@source_vm2] $ qrexec-client-vm target_vm test.File+testfile2
|
||
|
||
|
||
|
||
And when invoked with other arguments or from a different VM, it should
|
||
also be denied.
|