Merge branch 'qrexec-rewrite-phase-2' of https://github.com/pierwill/qubes-doc into pierwill-qrexec-rewrite-phase-2

This commit is contained in:
Andrew David Wong 2019-09-15 16:23:28 -05:00
commit 2de99d2bab
No known key found for this signature in database
GPG Key ID: 8CE137352A019A17
2 changed files with 120 additions and 78 deletions

View File

@ -73,5 +73,5 @@ the instructions above. This will be time consuming process.
[upgrade-r3.1]: /doc/releases/3.1/release-notes/#upgrading
[backup]: /doc/backup-restore/
[qrexec-argument]: https://github.com/QubesOS/qubes-issues/issues/1876
[qrexec-doc]: /doc/qrexec/#service-argument-in-policy
[qrexec-doc]: /doc/qrexec/#service-policies-with-arguments
[github-release-notes]: https://github.com/QubesOS/qubes-issues/issues?q=is%3Aissue+sort%3Aupdated-desc+milestone%3A%22Release+3.2%22+label%3Arelease-notes+is%3Aclosed

View File

@ -44,7 +44,9 @@ Once this channel is established, stdin/stdout/stderr from the VMprocess is pass
The `qrexec-client` command is used to make connections to VMs from dom0.
For example, the following command
$ qrexec-client -e -d someVM user:'touch hello-world.txt'
```
$ qrexec-client -e -d someVM user:'touch hello-world.txt'
```
creates an empty file called `hello-world.txt` in the home folder of `someVM`.
@ -53,7 +55,9 @@ The `-e` flag tells `qrexec-client` to exit immediately after sending the execut
With this option, no further data is passed between the domains.
By contrast, the following command demonstrates an open channel between dom0 and someVM (in this case, a remote shell):
$ qrexec-client -d someVM user:bash
```
$ qrexec-client -d someVM user:bash
```
The `qvm-run` command is heavily based on `qrexec-client`.
It also takes care of additional activities, e.g. starting the domain if it is not up yet and starting the GUI daemon.
@ -81,15 +85,15 @@ Additionally, disposable VMs are tightly integrated -- RPC to a DisposableVM is
## Qubes RPC administration
<!-- (*TODO: fix for non-linux dom0*) -->
### Policy files
The dom0 directory `/etc/qubes-rpc/policy/` contains a file for each available RPC action that a VM might call.
Together the contents of these files make up the RPC access policy database.
Policies are defined in lines with the following format:
srcvm destvm (allow|deny|ask[,default_target=default_target_VM])[,user=user_to_run_as][,target=VM_to_redirect_to]
```
srcvm destvm (allow|deny|ask[,default_target=default_target_VM])[,user=user_to_run_as][,target=VM_to_redirect_to]
```
You can specify srcvm and destvm by name or by one of the reserved keywords such as `@anyvm`, `@dispvm`, or `dom0`.
(Of these three, only `@anyvm` keyword makes sense in the srcvm field.
@ -109,36 +113,46 @@ In the target VM, the file `/etc/qubes-rpc/RPC_ACTION_NAME` must exist, containi
From outside of dom0, RPC calls take the following form:
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME rpc_client_path client arguments
```
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME rpc_client_path client arguments
```
For example:
$ qrexec-client-vm work qubes.StartApp+firefox
```
$ 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:
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME
```
$ 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)
- `@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:
anon-whonix @dispvm:anon-whonix-dvm allow
```
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:
anon-whonix @dispvm allow,target=@dispvm:anon-whonix-dvm
```
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.
@ -150,9 +164,11 @@ 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:
work-mail work-archive allow
work-mail @tag:work ask,default_target=work-files
work-mail @default ask,default_target=work-files
```
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 allow call 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`.
@ -183,116 +199,142 @@ By contrast, the `qubes.StartApp` service allows you to run only applications th
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
### Service argument in policy
Sometimes a service name alone isn't enough to make reasonable qrexec policy.
One example of such a situation is [qrexec-based USB passthrough](https://www.qubes-os.org/doc/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.
Sometimes just service name isn't enough to make reasonable qrexec policy.
One example of such a situation is [qrexec-based USB passthrough](https://github.com/qubesos/qubes-issues/issues/531) - using just service name isn't possible to express the policy "allow access to device X and deny to others".
It also isn't feasible to create a separate service for every device...
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.)
For this reason, starting with Qubes 3.2, it is possible to specify a service argument, which will be subject to policy.
Besides the above example of USB passthrough, a service argument can make many service policies more fine-grained and easier to write precise policy with "allow" and "deny" actions, instead of "ask" (offloading additional decisions to the user).
And generally the less choices the user must make, the lower the chance to make a mistake.
Each specific argument that we want to use needs its own policy in dom0 at a path like `/etc/qubes-rpc/policy/RPC_ACTION_NAME+ARGUMENT`.
So for instance, we might have policies called `test.Device`, `test.Device+device1` and `test.Device+device2`.
If the policy for the specific argument is not set (that is, if no file exists for `RPC_ACTION_NAME+ARGUMENT`), then dom0 uses the default policy with no argument for this service.
The syntax is simple: when calling a service, add an argument to the service name separated with `+` sign, for example:
When calling a service that takes an argument, just add the argument to the service name separated with `+`.
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME+ARGUMENT
```
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME+ARGUMENT
```
Then create a policy as usual, including the argument (`/etc/qubes-rpc/policy/RPC_ACTION_NAME+ARGUMENT`).
If the policy for the specific argument is not set (file does not exist), then the default policy for this service is loaded (`/etc/qubes-rpc/policy/RPC_ACTION_NAME`).
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.
In target VM (when the call is allowed) the service file will searched as:
- `/etc/qubes-rpc/RPC_ACTION_NAME+ARGUMENT`
- `/etc/qubes-rpc/RPC_ACTION_NAME`
In any case, the script will receive `ARGUMENT` as its argument and additionally as `QREXEC_SERVICE_ARGUMENT` environment variable.
This means it is also possible to install a different script for a particular service argument.
See below for an example service using an 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 example
## Qubes RPC examples
As a demonstration, 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").
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`:
#!/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
```
#!/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:
#!/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
```
#!/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 another one-line file, `/etc/qubes-rpc/test.Add`:
/usr/bin/our_test_add_server
```
/usr/bin/our_test_add_server
```
The administrative domain will direct traffic based on the current RPC policies.
In dom0, create a file at `/etc/qubes-rpc/policy/test.Add` containing the following:
@anyvm @anyvm ask
```
@anyvm @anyvm 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.
$ qrexec-client-vm anotherVM test.Add /usr/bin/our_test_add_client 1 2
```
$ 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).
### Qubes RPC example - with argument usage
### RPC service with argument (file reader)
We will show the necessary files to create an RPC call that reads a specific file from a predefined directory on the target.
Besides really naive storage, it may be a very simple password manager.
Additionally, in this example a simplified workflow will be used - server code placed directly in the service definition file (in `/etc/qubes-rpc` directory).
And no separate client script will be used.
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 placed is placed directly in the service definition file on the target VM.
No separate client script will be needed.
* RPC server code (*/etc/qubes-rpc/test.File*)
First, on your target VM, create two files in the home directory: `testfile1` and `testfile2`.
Have them contain two different "Hello world!" lines.
#!/bin/sh
argument="$1" # service argument, also available as $QREXEC_SERVICE_ARGUMENT
if [ -z "$argument" ]; then
echo "ERROR: No argument given!"
exit 1
fi
# service argument is already sanitized by qrexec framework and it is
# guaranteed to not contain any space or /, so no need for additional path
# sanitization
cat "/home/user/rpc-file-storage/$argument"
Next, we define the RPC service.
On the target VM, place the code below at `/etc/qubes-rpc/test.File`:
* specific policy file in dom0 (*/etc/qubes-rpc/policy/test.File+testfile1* )
```
#!/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"
```
source_vm1 target_vm allow
(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.)
* another specific policy file in dom0 (*/etc/qubes-rpc/policy/test.File+testfile2* )
Now we create three policy files in dom0.
See the table below for details.
Replace "source_vm1" and others with the names of your own chosen domains.
source_vm2 target_vm allow
|------------------------------------------------------------------------|
| Path to file in dom0 | Policy contents |
|-------------------------------------------+----------------------------|
| /etc/qubes-rpc/policy/test.File | @anyvm @anyvm deny |
| /etc/qubes-rpc/policy/test.File+testfile1 | source_vm1 target_vm allow |
| /etc/qubes-rpc/policy/test.File+testfile2 | source_vm2 target_vm allow |
|------------------------------------------------------------------------|
* default policy file in dom0 (*/etc/qubes-rpc/policy/test.File* )
With this done, we can run some tests.
Invoke RPC from `source_vm1` via
@anyvm @anyvm deny
```
[user@source_vm1] $ qrexec-client-vm target_vm test.File+testfile1
```
* invoke RPC from `source_vm1` via
We should get the contents of `/home/user/testfile1` printed to the terminal.
Invoking the service from `source_vm2` should work the same, and `testfile2` should also work.
/usr/lib/qubes/qrexec-client-vm target_vm test.File+testfile1
```
[user@source_vm2] $ qrexec-client-vm target_vm test.File+testfile1
[user@source_vm2] $ qrexec-client-vm target_vm test.File+testfile2
```
and we should get content of `/home/user/rpc-file-storage/testfile1` as answer.
* also possible to invoke RPC from `source_vm2` via
/usr/lib/qubes/qrexec-client-vm target_vm test.File+testfile2
But when invoked with other argument or from different VM, it should be denied.
But when invoked with other arguments or from a different VM, it should be denied.