Compare commits

..

35 Commits

Author SHA1 Message Date
Manuel Amador (Rudd-O)
033d86035c Tag 0.0.21. 2024-02-20 12:16:23 +00:00
Manuel Amador (Rudd-O)
d1af78c00b F39 and Qubes 4.2, no longer F37 and Qubes 4.1. 2024-02-20 12:16:11 +00:00
Manuel Amador (Rudd-O)
6014c6f190 qrun do not use pipes. 2023-08-11 22:21:19 +00:00
Manuel Amador (Rudd-O)
037e5af9bd Add Fedora 38. 2023-08-06 11:30:11 +00:00
Manuel Amador (Rudd-O)
84b7c6b0eb Fix bug in ellipsized. 2023-03-13 15:25:45 +00:00
Manuel Amador (Rudd-O)
3b1ae61238 Fix quote generator. 2023-03-13 15:14:47 +00:00
Manuel Amador (Rudd-O)
c85b35867d Nicely ellipsize logged commands. 2023-03-13 13:06:47 +00:00
Manuel Amador (Rudd-O)
782c557cb6 Update documentation to catch up with Qubes 4.1 policy changes. 2023-02-25 18:24:58 +00:00
Manuel Amador (Rudd-O)
f6dc498036 New build parameters. 2023-02-21 22:33:37 +00:00
Manuel Amador (Rudd-O)
50b3deddd2 Tag 0.0.17. 2023-02-21 22:27:41 +00:00
Manuel Amador (Rudd-O)
8675eaa547 Eliminate the lock. 2023-02-21 22:27:10 +00:00
Rudd-O
3a60f0ee4b
Merge pull request #19 from ProfessorManhattan/master
Update qubesformation.py
2022-10-24 12:40:44 +00:00
Brian Zalewski
f750054efa
Update qubesformation.py 2022-10-23 04:05:03 -04:00
Brian Zalewski
45cd87d984
Update qubesformation.py 2022-10-23 02:47:08 -04:00
Brian Zalewski
feff9f41a7
Update qubesformation.py 2022-10-23 02:40:10 -04:00
Brian Zalewski
f1db77fb05
Update qubesformation.py 2022-10-23 01:38:17 -04:00
Brian Zalewski
e5aef5be64
Update qubesformation.py 2022-10-18 12:03:20 -04:00
Brian Zalewski
77015a49ac
Update qubesformation.py 2022-10-18 11:49:48 -04:00
Brian Zalewski
9043a3a736
Update qubesformation.py 2022-10-18 11:31:17 -04:00
Brian Zalewski
8ad65b7e27
Update qubesformation.py 2022-10-18 11:18:52 -04:00
Brian Zalewski
a41aa775d2
Update qubesformation.py 2022-10-18 10:46:21 -04:00
Brian Zalewski
6984a541c6
Update qubesformation.py 2022-10-18 10:29:21 -04:00
Brian Zalewski
a6384ab40f
Update qubesformation.py 2022-10-18 05:08:21 -04:00
Brian Zalewski
bbed07547c
Update qubesformation.py 2022-10-04 00:06:42 -04:00
Brian Zalewski
6ae2ae87c0
Update qubesformation.py 2022-10-03 22:52:41 -04:00
Rudd-O
c4029694fb
Merge pull request #18 from ProfessorManhattan/master
Missing encoding for qubesformation
2022-10-01 15:01:25 +00:00
Brian Zalewski
9a592548e2
Update qubesformation.py 2022-10-01 01:05:16 -04:00
Brian Zalewski
aa712c35e0
Update qubesformation.py 2022-09-30 21:10:38 -04:00
Brian Zalewski
6eba5edf1f
Update commonlib.py 2022-09-27 16:32:15 -04:00
Brian Zalewski
e3d1084c92
Update qubes.py 2022-09-26 23:40:59 -04:00
Brian Zalewski
17303a9f92
Update bombshell-client 2022-09-26 22:09:50 -04:00
Brian Zalewski
ce843d49f7
Merge pull request #1 from ProfessorManhattan/ProfessorManhattan-patch-1
Update qubesformation.py
2022-09-18 23:32:37 -04:00
Brian Zalewski
8a850692f8
Update qubesformation.py 2022-09-18 23:30:29 -04:00
Manuel Amador (Rudd-O)
2b8f4e3a90 Tag new. 2022-09-07 02:44:25 +00:00
Manuel Amador (Rudd-O)
4966b9e814 Fix put protocol to work correctly. 2022-09-07 02:42:28 +00:00
9 changed files with 116 additions and 88 deletions

View File

@ -92,18 +92,18 @@ Enabling bombshell-client access to dom0
create a file `/etc/qubes-rpc/qubes.VMshell` with mode `0755` and make
sure its contents say `/bin/bash`.
You will then create a file `/etc/qubes-rpc/policy/qubes.VMShell` with
mode 0664, owned by your login user, and group `qubes`. Add a policy
You will then create a file `/etc/qubes/policy.d/80-ansible-qubes.policy`
with mode 0664, owned by `root` and group `qubes`. Add a policy
line towards the top of the file:
```
yourvm dom0 ask
qubes.VMShell * controller * allow
```
Where `yourvm` represents the name of the VM you will be executing
`bombshell-client` against dom0 from.
Where `controller` represents the name of the VM you will be executing
`bombshell-client` against `dom0` from.
That's it -- `bombshell-client` should work against dom0 now. Of course,
That's it -- `bombshell-client` should work against `dom0` now. Of course,
you can adjust the policy to have it not ask — do the security math
on what that implies.

View File

@ -96,6 +96,8 @@ def inject_qubes(inject):
pass
elif vmtype == "ProxyVM":
add(flags, "proxy")
elif vmtype == "DispVM":
pass
elif vmtype == "TemplateVM":
try:
qubes["source"] = qubes["template"]

View File

@ -8,15 +8,24 @@ from ansible.plugins.action.template import ActionModule as template
sys.path.insert(0, os.path.dirname(__file__))
import commonlib
contents = """{{ vms | to_nice_yaml }}"""
topcontents = "{{ saltenv }}:\n '*':\n - {{ recipename }}\n"
def generate_datastructure(vms, task_vars):
dc = collections.OrderedDict
d = dc()
for n, data in vms.items():
# This block will skip any VMs that are not in the groups defined in the 'formation_vm_groups' variable
# This allows you to deploy in multiple stages which is useful in cases
# where you want to create a template after another template is already provisioned.
if 'formation_vm_groups' in task_vars:
continueLoop = True
for group in task_vars['formation_vm_groups']:
if n in task_vars['hostvars'][n]['groups'][group]:
continueLoop = False
if continueLoop:
continue
qubes = data['qubes']
d[task_vars['hostvars'][n]['inventory_hostname_short']] = dc(qvm=['vm'])
vm = d[task_vars['hostvars'][n]['inventory_hostname_short']]
@ -90,7 +99,6 @@ def generate_datastructure(vms, task_vars):
return d
class ActionModule(template):
TRANSFERS_FILES = True
@ -99,7 +107,7 @@ class ActionModule(template):
qubesdata = commonlib.inject_qubes(task_vars)
task_vars["vms"] = generate_datastructure(qubesdata, task_vars)
with tempfile.NamedTemporaryFile() as x:
x.write(contents)
x.write(contents.encode())
x.flush()
self._task.args['src'] = x.name
retval = template.run(self, tmp, task_vars)
@ -107,7 +115,7 @@ class ActionModule(template):
return retval
with tempfile.NamedTemporaryFile() as y:
y.write(topcontents)
y.write(topcontents.encode())
y.flush()
# Create new tmp path -- the other was blown away.

View File

@ -3,7 +3,7 @@
%define mybuildnumber %{?build_number}%{?!build_number:1}
Name: ansible-qubes
Version: 0.0.15
Version: 0.0.21
Release: %{mybuildnumber}%{?dist}
Summary: Inter-VM program execution for Qubes OS AppVMs and StandaloneVMs
BuildArch: noarch

View File

@ -2,7 +2,6 @@
import base64
import pickle
import contextlib
import errno
import fcntl
import os
@ -43,18 +42,6 @@ def set_proc_name(newname):
libc.prctl(15, byref(buff), 0, 0, 0)
@contextlib.contextmanager
def mutexfile(filepath):
oldumask = os.umask(0o077)
try:
f = open(filepath, "a")
finally:
os.umask(oldumask)
fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
yield
f.close()
def unset_cloexec(fd):
old = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, old & ~fcntl.FD_CLOEXEC)
@ -334,12 +321,18 @@ def quotedargs():
return " ".join(quote(x) for x in sys.argv[1:])
def quotedargs_ellipsized(cmdlist):
text = " ".join(quote(x) for x in cmdlist)
if len(text) > 80:
text = text[:77] + "..."
return text
def main_master():
set_proc_name("bombshell-client (master) %s" % quotedargs())
global logging
logging = LoggingEmu("master")
logging.info("Started with arguments: %s", sys.argv[1:])
logging.info("Started with arguments: %s", quotedargs_ellipsized(sys.argv[1:]))
global debug_enabled
args = sys.argv[1:]
@ -352,7 +345,7 @@ def main_master():
assert remote_command
def anypython(exe):
return "` test -x %s && echo %s || echo python`" % (
return "` test -x %s && echo %s || echo python3`" % (
quote(exe),
quote(exe),
)
@ -370,28 +363,27 @@ def main_master():
saved_stderr = openfdforappend(os.dup(sys.stderr.fileno()))
with mutexfile(os.path.expanduser("~/.bombshell-lock")):
try:
p = subprocess.Popen(
["qrexec-client-vm", remote_vm, "qubes.VMShell"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
close_fds=True,
preexec_fn=os.setpgrp,
bufsize=0,
)
except OSError as e:
logging.error("cannot launch qrexec-client-vm: %s", e)
return 127
try:
p = subprocess.Popen(
["qrexec-client-vm", remote_vm, "qubes.VMShell"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
close_fds=True,
preexec_fn=os.setpgrp,
bufsize=0,
)
except OSError as e:
logging.error("cannot launch qrexec-client-vm: %s", e)
return 127
logging.debug("Writing the helper text into the other side")
p.stdin.write(remote_helper_text)
p.stdin.flush()
logging.debug("Writing the helper text into the other side")
p.stdin.write(remote_helper_text)
p.stdin.flush()
confirmation, errmsg = recv_confirmation(p.stdout)
if confirmation != 0:
logging.error("remote: %s", errmsg)
return confirmation
confirmation, errmsg = recv_confirmation(p.stdout)
if confirmation != 0:
logging.error("remote: %s", errmsg)
return confirmation
handled_signals = (
signal.SIGINT,
@ -433,7 +425,7 @@ def main_remote():
global logging
logging = LoggingEmu("remote")
logging.info("Started with arguments: %s", sys.argv[1:])
logging.info("Started with arguments: %s", quotedargs_ellipsized(sys.argv[1:]))
global debug_enabled
if "-d" in sys.argv[1:]:
@ -482,10 +474,11 @@ def main_remote():
muxer.name = "remote multiplexer"
muxer.start()
logging.info("Started %s", nicecmd)
nicecmd_ellipsized = quotedargs_ellipsized(cmd)
logging.info("Started %s", nicecmd_ellipsized)
retval = p.wait()
logging.info("Return code %s for %s", retval, nicecmd)
logging.info("Return code %s for %s", retval, nicecmd_ellipsized)
muxer.join()
logging.info("Ending bombshell")
return retval

View File

@ -1 +1 @@
["RELEASE": "q4.1 36"]
["RELEASE": "q4.2 38 39"]

View File

@ -63,14 +63,20 @@ class x(object):
display = x()
BUFSIZE = 128*1024 # any bigger and it causes issues because we don't read multiple chunks until completion
BUFSIZE = 64*1024 # any bigger and it causes issues because we don't read multiple chunks until completion
CONNECTION_TRANSPORT = "qubes"
CONNECTION_OPTIONS = {
'management_proxy': '--management-proxy',
}
def debug(text):
return
print(text, file=sys.stderr)
def encode_exception(exc, stream):
debug("encoding exception")
stream.write('{}\n'.format(len(exc.__class__.__name__)).encode('ascii'))
stream.write('{}'.format(exc.__class__.__name__).encode('ascii'))
for attr in "errno", "filename", "message", "strerror":
@ -79,6 +85,7 @@ def encode_exception(exc, stream):
def decode_exception(stream):
debug("decoding exception")
name_len = stream.readline(16)
name_len = int(name_len)
name = stream.read(name_len)
@ -107,6 +114,7 @@ def decode_exception(stream):
def popen(cmd, in_data, outf=sys.stdout):
debug("popening on remote %s" % type(in_data))
try:
p = subprocess.Popen(
cmd, shell=False, stdin=subprocess.PIPE,
@ -124,9 +132,11 @@ def popen(cmd, in_data, outf=sys.stdout):
outf.write('{}\n'.format(len(err)).encode('ascii'))
outf.write(err)
outf.flush()
debug("finished popening")
def put(out_path):
debug("dest writing %s" % out_path)
try:
f = open(out_path, "wb")
sys.stdout.write(b'Y\n')
@ -136,18 +146,25 @@ def put(out_path):
return
while True:
chunksize = int(sys.stdin.readline(16))
if chunksize == 0:
if not chunksize:
debug("looks like we have no more to read")
break
chunk = sys.stdin.read(chunksize)
assert len(chunk) == chunksize, ("Mismatch in chunk length", len(chunk), chunksize)
try:
f.write(chunk)
sys.stdout.write(b'Y\n')
except (IOError, OSError) as e:
sys.stdout.write(b'N\n')
encode_exception(e, sys.stdout)
f.close()
return
while chunksize:
debug(type(chunksize))
chunk = sys.stdin.read(chunksize)
assert chunk
debug("dest writing %s" % len(chunk))
try:
f.write(chunk)
except (IOError, OSError) as e:
sys.stdout.write(b'N\n')
encode_exception(e, sys.stdout)
f.close()
return
chunksize = chunksize - len(chunk)
debug("remaining %s" % chunksize)
sys.stdout.write(b'Y\n')
sys.stdout.flush()
try:
f.flush()
except (IOError, OSError) as e:
@ -155,10 +172,12 @@ def put(out_path):
encode_exception(e, sys.stdout)
return
finally:
debug("finished writing dest")
f.close()
def fetch(in_path, bufsize):
debug("Fetching from remote %s" % in_path)
try:
f = open(in_path, "rb")
except (IOError, OSError) as e:
@ -206,7 +225,7 @@ sys.stdout = sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout
'''
payload = b'\n\n'.join(
inspect.getsource(x).encode("utf-8")
for x in (encode_exception, popen, put, fetch)
for x in (debug, encode_exception, popen, put, fetch)
) + \
b'''
@ -294,7 +313,7 @@ class Connection(ConnectionBase):
if not self._connected:
remote_cmd = [to_bytes(x, errors='surrogate_or_strict') for x in [
# 'strace', '-s', '2048', '-o', '/tmp/log',
'python', '-u', '-i', '-c', preamble
'python3', '-u', '-i', '-c', preamble
]]
addr = self._play_context.remote_addr
proxy = to_bytes(self.get_option("management_proxy")) if self.get_option("management_proxy") else ""
@ -356,16 +375,18 @@ class Connection(ConnectionBase):
cmd = shlex.split(cmd)
display.vvvv("EXEC %s" % cmd, host=self._play_context.remote_addr)
try:
payload = ('popen(%r, %r)\n' % (cmd, in_data)).encode("utf-8")
payload = ('popen(%r, %r)\n\n' % (cmd, in_data)).encode("utf-8")
self._transport.stdin.write(payload)
self._transport.stdin.flush()
yesno = self._transport.stdout.readline(2)
debug("Reading yesno")
except Exception:
self._abort_transport()
raise
if yesno == "Y\n" or yesno == b"Y\n":
try:
retcode = self._transport.stdout.readline(16)
debug("Reading retcode")
try:
retcode = int(retcode)
except Exception:
@ -402,6 +423,7 @@ class Connection(ConnectionBase):
else:
self._abort_transport()
raise errors.AnsibleError("pass/fail from remote end is unexpected: %r" % yesno)
debug("finished popening on master")
def put_file(self, in_path, out_path):
'''Transfer a file from local to VM.'''
@ -423,6 +445,7 @@ class Connection(ConnectionBase):
with open(in_path, 'rb') as in_file:
while True:
chunk = in_file.read(BUFSIZE)
debug("source writing %s bytes" % len(chunk))
try:
self._transport.stdin.write(("%s\n" % len(chunk)).encode("utf-8"))
self._transport.stdin.flush()
@ -442,9 +465,15 @@ class Connection(ConnectionBase):
else:
self._abort_transport()
raise errors.AnsibleError("pass/fail from remote end is unexpected: %r" % yesno)
debug("on this side it's all good")
self._transport.stdin.write(("%s\n" % 0).encode("utf-8"))
self._transport.stdin.flush()
debug("finished writing source")
def fetch_file(self, in_path, out_path):
'''Fetch a file from VM to local.'''
debug("fetching to local")
super(Connection, self).fetch_file(in_path, out_path)
display.vvvv("FETCH %s to %s" % (in_path, out_path), host=self._play_context.remote_addr)
in_path = _prefix_login_path(in_path)

View File

@ -24,13 +24,13 @@ Integrate this software into your Ansible setup (within your `managevm`) VM) by:
## Set up the policy file for `qubes.VMShell`
Edit (as `root`) the file `/etc/qubes-rpc/policy/qubes.VMShell`
Edit (as `root`) the file `/etc/qubes/policy.d/80-ansible-qubes.policy`
located on the file system of your `dom0`.
At the top of the file, add the following two lines:
```
managevm $anyvm allow
qubes.VMShell * managevm * allow
```
This first line lets `managevm` execute any commands on any VM on your
@ -41,25 +41,21 @@ security prompt to allow `qubes.VMShell` on the target VM you're managing.
Now save that file, and exit your editor.
If your dom0 has a file `/etc/qubes-rpc/policy/qubes.VMShell`,
you can delete it now. It is obsolete.
### Optional: allow `managevm` to manage `dom0`
Before the line you added in the previous step, add this line:
```
managevm dom0 allow
```
This line lets `managevm` execute any commands in `dom0`. Be sure you
understand the security implications of such a thing.
The next step is to add the RPC service proper. Edit the file
The next step is to add the RPC service proper to dom0. Edit the file
`/etc/qubes-rpc/qubes.VMShell` to have a single line that contains:
```
exec bash
```
That is it. `dom0` should work now.
Make the file executable.
That is it. `dom0` should work now. Note you do this at your own risk.
## Test `qrun` works

View File

@ -13,11 +13,11 @@ to set up a policy that allows us to remotely execute commands on any VM of the
network server, without having to be physically present to click any dialogs authorizing
the execution of those commands.
In `dom0` of your Qubes server, edit `/etc/qubes-rpc/policy/qubes.VMShell` to add,
In `dom0` of your Qubes server, edit `/etc/qubes/policy.d/80-ansible-qubes.policy` to add,
at the top of the file, a policy that looks like this:
```
exp-manager $anyvm allow
qubes.VMShell * managevm * allow
```
This tells Qubes OS that `exp-manager` is now authorized to run any command in any of the VMs.
@ -25,13 +25,13 @@ This tells Qubes OS that `exp-manager` is now authorized to run any command in a
**Security note**: this does mean that anyone with access to `exp-manager` can do
literally anything on any of your VMs in your Qubes OS server.
If that is not what you want, then replace `$anyvm` with the name of the VMs you would like
to manage. For example: if you would like `exp-manager` to be authorized to run commands
*only* on `exp-net`, then you can use the following policy:
If that is not what you want, then replace `*` after `managevm` with the name of the VMs you
would like to manage. For example: if you would like `exp-manager` to be authorized to run
commands *only* on `exp-net`, then you can use the following policy:
```
exp-manager exp-net allow
exp-manager $anyvm deny
qubes.VMShell * exp-manager exp-net allow
qubes.VMShell * exp-manager @anyvm deny
```
Try it out now. SSH from your manager machine into `exp-manager` and run:
@ -47,7 +47,7 @@ You should see `yes` followed by `exp-net` on the output side.
If you expect that you will need to run commands in `dom0` from your manager machine
(say, to create, stop, start and modify VMs in the Qubes OS server),
then you will have to create a file `/etc/qubes-rpc/qubes.VMShell` as `root` in `dom0`,
with the contents `/bin/bash` and permission mode `0644`. Doing this will enable you
with the contents `/bin/bash` and permission mode `0755`. Doing this will enable you
to run commands on `dom0` which you can subsequently test in `exp-manager` by running command:
```
@ -57,7 +57,7 @@ qvm-run dom0 'echo yes ; hostname'
like you did before.
**Security note**: this does mean that anyone with access to `exp-manager` can do
literally anything on your Qubes OS server.
*literally anything* on your Qubes OS server. You have been warned.
## Integrate your Ansible setup