mirror of
https://github.com/Rudd-O/ansible-qubes.git
synced 2025-03-01 14:22:33 +01:00
Compare commits
No commits in common. "master" and "v0.0.13" have entirely different histories.
14
README.md
14
README.md
@ -89,21 +89,21 @@ Enabling bombshell-client access to dom0
|
||||
----------------------------------------
|
||||
|
||||
`dom0` needs its `qubes.VMShell` service activated. As `root` in `dom0`,
|
||||
create a file `/etc/qubes-rpc/qubes.VMshell` with mode `0755` and make
|
||||
create a file `/etc/qubes-rpc/qubes.VMshell` with mode `0644` and make
|
||||
sure its contents say `/bin/bash`.
|
||||
|
||||
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
|
||||
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
|
||||
line towards the top of the file:
|
||||
|
||||
```
|
||||
qubes.VMShell * controller * allow
|
||||
yourvm dom0 ask
|
||||
```
|
||||
|
||||
Where `controller` represents the name of the VM you will be executing
|
||||
`bombshell-client` against `dom0` from.
|
||||
Where `yourvm` 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.
|
||||
|
||||
|
@ -96,8 +96,6 @@ def inject_qubes(inject):
|
||||
pass
|
||||
elif vmtype == "ProxyVM":
|
||||
add(flags, "proxy")
|
||||
elif vmtype == "DispVM":
|
||||
pass
|
||||
elif vmtype == "TemplateVM":
|
||||
try:
|
||||
qubes["source"] = qubes["template"]
|
||||
|
@ -8,24 +8,15 @@ 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']]
|
||||
@ -99,6 +90,7 @@ def generate_datastructure(vms, task_vars):
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class ActionModule(template):
|
||||
|
||||
TRANSFERS_FILES = True
|
||||
@ -107,7 +99,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.encode())
|
||||
x.write(contents)
|
||||
x.flush()
|
||||
self._task.args['src'] = x.name
|
||||
retval = template.run(self, tmp, task_vars)
|
||||
@ -115,7 +107,7 @@ class ActionModule(template):
|
||||
return retval
|
||||
|
||||
with tempfile.NamedTemporaryFile() as y:
|
||||
y.write(topcontents.encode())
|
||||
y.write(topcontents)
|
||||
y.flush()
|
||||
|
||||
# Create new tmp path -- the other was blown away.
|
||||
|
@ -3,7 +3,7 @@
|
||||
%define mybuildnumber %{?build_number}%{?!build_number:1}
|
||||
|
||||
Name: ansible-qubes
|
||||
Version: 0.0.21
|
||||
Version: 0.0.13
|
||||
Release: %{mybuildnumber}%{?dist}
|
||||
Summary: Inter-VM program execution for Qubes OS AppVMs and StandaloneVMs
|
||||
BuildArch: noarch
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import base64
|
||||
import pickle
|
||||
import contextlib
|
||||
import errno
|
||||
import fcntl
|
||||
import os
|
||||
@ -42,6 +43,18 @@ 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)
|
||||
@ -321,18 +334,12 @@ 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", quotedargs_ellipsized(sys.argv[1:]))
|
||||
logging.info("Started with arguments: %s", sys.argv[1:])
|
||||
|
||||
global debug_enabled
|
||||
args = sys.argv[1:]
|
||||
@ -345,7 +352,7 @@ def main_master():
|
||||
assert remote_command
|
||||
|
||||
def anypython(exe):
|
||||
return "` test -x %s && echo %s || echo python3`" % (
|
||||
return "` test -x %s && echo %s || echo python`" % (
|
||||
quote(exe),
|
||||
quote(exe),
|
||||
)
|
||||
@ -363,27 +370,28 @@ def main_master():
|
||||
|
||||
saved_stderr = openfdforappend(os.dup(sys.stderr.fileno()))
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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,
|
||||
@ -425,7 +433,7 @@ def main_remote():
|
||||
global logging
|
||||
logging = LoggingEmu("remote")
|
||||
|
||||
logging.info("Started with arguments: %s", quotedargs_ellipsized(sys.argv[1:]))
|
||||
logging.info("Started with arguments: %s", sys.argv[1:])
|
||||
|
||||
global debug_enabled
|
||||
if "-d" in sys.argv[1:]:
|
||||
@ -474,11 +482,10 @@ def main_remote():
|
||||
muxer.name = "remote multiplexer"
|
||||
muxer.start()
|
||||
|
||||
nicecmd_ellipsized = quotedargs_ellipsized(cmd)
|
||||
logging.info("Started %s", nicecmd_ellipsized)
|
||||
logging.info("Started %s", nicecmd)
|
||||
|
||||
retval = p.wait()
|
||||
logging.info("Return code %s for %s", retval, nicecmd_ellipsized)
|
||||
logging.info("Return code %s for %s", retval, nicecmd)
|
||||
muxer.join()
|
||||
logging.info("Ending bombshell")
|
||||
return retval
|
||||
|
@ -1 +1 @@
|
||||
["RELEASE": "q4.2 38 39"]
|
||||
["RELEASE": "q4.1 36"]
|
||||
|
@ -63,20 +63,14 @@ class x(object):
|
||||
display = x()
|
||||
|
||||
|
||||
BUFSIZE = 64*1024 # any bigger and it causes issues because we don't read multiple chunks until completion
|
||||
BUFSIZE = 128*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":
|
||||
@ -85,7 +79,6 @@ 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)
|
||||
@ -114,7 +107,6 @@ 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,
|
||||
@ -132,11 +124,9 @@ 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')
|
||||
@ -146,25 +136,18 @@ def put(out_path):
|
||||
return
|
||||
while True:
|
||||
chunksize = int(sys.stdin.readline(16))
|
||||
if not chunksize:
|
||||
debug("looks like we have no more to read")
|
||||
if chunksize == 0:
|
||||
break
|
||||
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()
|
||||
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
|
||||
try:
|
||||
f.flush()
|
||||
except (IOError, OSError) as e:
|
||||
@ -172,12 +155,10 @@ 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:
|
||||
@ -225,7 +206,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 (debug, encode_exception, popen, put, fetch)
|
||||
for x in (encode_exception, popen, put, fetch)
|
||||
) + \
|
||||
b'''
|
||||
|
||||
@ -274,7 +255,7 @@ class Connection(ConnectionBase):
|
||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||
super(Connection, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||
# FIXME HORRIBLE WORKAROUND FIXME
|
||||
if task_keys and task_keys['delegate_to'] and self._options and 'management_proxy' in self._options:
|
||||
if task_keys['delegate_to'] and 'management_proxy' in self._options:
|
||||
self._options['management_proxy'] = ''
|
||||
|
||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||
@ -285,6 +266,7 @@ class Connection(ConnectionBase):
|
||||
self.transport_cmd = kwargs['transport_cmd']
|
||||
return
|
||||
self.transport_cmd = distutils.spawn.find_executable('qrun')
|
||||
self.transport_cmd = None
|
||||
if not self.transport_cmd:
|
||||
self.transport_cmd = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
@ -313,7 +295,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',
|
||||
'python3', '-u', '-i', '-c', preamble
|
||||
'python', '-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 ""
|
||||
@ -375,18 +357,16 @@ class Connection(ConnectionBase):
|
||||
cmd = shlex.split(cmd)
|
||||
display.vvvv("EXEC %s" % cmd, host=self._play_context.remote_addr)
|
||||
try:
|
||||
payload = ('popen(%r, %r)\n\n' % (cmd, in_data)).encode("utf-8")
|
||||
payload = ('popen(%r, %r)\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:
|
||||
@ -423,7 +403,6 @@ 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.'''
|
||||
@ -445,7 +424,6 @@ 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()
|
||||
@ -465,15 +443,9 @@ 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)
|
||||
|
@ -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/policy.d/80-ansible-qubes.policy`
|
||||
Edit (as `root`) the file `/etc/qubes-rpc/policy/qubes.VMShell`
|
||||
located on the file system of your `dom0`.
|
||||
|
||||
At the top of the file, add the following two lines:
|
||||
|
||||
```
|
||||
qubes.VMShell * managevm * allow
|
||||
managevm $anyvm allow
|
||||
```
|
||||
|
||||
This first line lets `managevm` execute any commands on any VM on your
|
||||
@ -41,21 +41,25 @@ 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`
|
||||
|
||||
The next step is to add the RPC service proper to dom0. Edit the file
|
||||
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
|
||||
`/etc/qubes-rpc/qubes.VMShell` to have a single line that contains:
|
||||
|
||||
```
|
||||
exec bash
|
||||
```
|
||||
|
||||
Make the file executable.
|
||||
|
||||
That is it. `dom0` should work now. Note you do this at your own risk.
|
||||
That is it. `dom0` should work now.
|
||||
|
||||
|
||||
## Test `qrun` works
|
||||
|
@ -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/policy.d/80-ansible-qubes.policy` to add,
|
||||
In `dom0` of your Qubes server, edit `/etc/qubes-rpc/policy/qubes.VMShell` to add,
|
||||
at the top of the file, a policy that looks like this:
|
||||
|
||||
```
|
||||
qubes.VMShell * managevm * allow
|
||||
exp-manager $anyvm 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 `*` 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:
|
||||
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:
|
||||
|
||||
```
|
||||
qubes.VMShell * exp-manager exp-net allow
|
||||
qubes.VMShell * exp-manager @anyvm deny
|
||||
exp-manager exp-net allow
|
||||
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 `0755`. Doing this will enable you
|
||||
with the contents `/bin/bash` and permission mode `0644`. 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. You have been warned.
|
||||
literally anything on your Qubes OS server.
|
||||
|
||||
## Integrate your Ansible setup
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user