Add ability to fallback to chgrp remote_tmp and its files. (#68627)
* Add ability to fallback to chgrp remote_tmp and its files. Signed-off-by: Rick Elrod <rick@elrod.me>
This commit is contained in:
parent
b9e38e8b55
commit
91aea92c62
14 changed files with 470 additions and 29 deletions
|
@ -72,7 +72,7 @@ Become connection variables
|
|||
You can define different ``become`` options for each managed node or group. You can define these variables in inventory or use them as normal variables.
|
||||
|
||||
ansible_become
|
||||
equivalent of the become directive, decides if privilege escalation is used or not.
|
||||
overrides the ``become`` directive, decides if privilege escalation is used or not.
|
||||
|
||||
ansible_become_method
|
||||
which privilege escalation method should be used
|
||||
|
@ -83,6 +83,9 @@ ansible_become_user
|
|||
ansible_become_password
|
||||
set the privilege escalation password. See :ref:`playbooks_vault` for details on how to avoid having secrets in plain text
|
||||
|
||||
ansible_common_remote_group
|
||||
determines if Ansible should try to ``chgrp`` its temporary files to a group if ``setfacl`` and ``chown`` both fail. See `Risks of becoming an unprivileged user`_ for more information. Added in version 2.10.
|
||||
|
||||
For example, if you want to run all tasks as ``root`` on a server named ``webserver``, but you can only connect as the ``manager`` user, you could use an inventory entry like this:
|
||||
|
||||
.. code-block:: text
|
||||
|
@ -125,20 +128,57 @@ and finally executing it there.
|
|||
|
||||
Everything is fine if the module file is executed without using ``become``,
|
||||
when the ``become_user`` is root, or when the connection to the remote machine
|
||||
is made as root. In these cases Ansible creates the module file with permissions
|
||||
that only allow reading by the user and root, or only allow reading by the unprivileged
|
||||
user being switched to.
|
||||
is made as root. In these cases Ansible creates the module file with
|
||||
permissions that only allow reading by the user and root, or only allow reading
|
||||
by the unprivileged user being switched to.
|
||||
|
||||
However, when both the connection user and the ``become_user`` are unprivileged,
|
||||
the module file is written as the user that Ansible connects as, but the file needs to
|
||||
be readable by the user Ansible is set to ``become``. In this case, Ansible makes
|
||||
the module file world-readable for the duration of the Ansible module execution.
|
||||
the module file is written as the user that Ansible connects as (the
|
||||
``remote_user``), but the file needs to be readable by the user Ansible is set
|
||||
to ``become``. The details of how Ansible solves this can vary based on platform.
|
||||
However, on POSIX systems, Ansible solves this problem in the following way:
|
||||
|
||||
First, if :command:`setfacl` is installed and available in the remote ``PATH``,
|
||||
and the temporary directory on the remote host is mounted with POSIX.1e
|
||||
filesystem ACL support, Ansible will use POSIX ACLs to share the module file
|
||||
with the second unprivileged user.
|
||||
|
||||
Next, if POSIX ACLs are **not** available or :command:`setfacl` could not be
|
||||
run, Ansible will attempt to change ownership of the module file using
|
||||
:command:`chown` for systems which support doing so as an unprivileged user.
|
||||
|
||||
New in Ansible 2.10, if the :command:`chown` fails, Ansible will then check the
|
||||
value of the configuration setting ``ansible_common_remote_group``. Many
|
||||
systems will allow a given user to change the group ownership of a file to a
|
||||
group the user is in. As a result, if the second unprivileged user (the
|
||||
``become_user``) has a UNIX group in common with the user Ansible is connected
|
||||
as (the ``remote_user``), and if ``ansible_common_remote_group`` is defined to
|
||||
be that group, Ansible can try to change the group ownership of the module file
|
||||
to that group by using :command:`chgrp`, thereby likely making it readable to
|
||||
the ``become_user``.
|
||||
|
||||
At this point, if ``ansible_common_remote_group`` was defined and a
|
||||
:command:`chgrp` was attempted and returned successfully, Ansible assumes (but,
|
||||
importantly, does not check) that the new group ownership is enough and does not
|
||||
fall back further. That is, Ansible **does not check** that the ``become_user``
|
||||
does in fact share a group with the ``remote_user``; so long as the command
|
||||
exits successfully, Ansible considers the result successful and does not proceed
|
||||
to check ``allow_world_readable_tmpfiles`` per below.
|
||||
|
||||
If ``ansible_common_remote_group`` is **not** set and the chown above it failed,
|
||||
or if ``ansible_common_remote_group`` *is* set but the :command:`chgrp` (or
|
||||
following group-permissions :command:`chmod`) returned a non-successful exit
|
||||
code, Ansible will lastly check the value of
|
||||
``allow_world_readable_tmpfiles``. If this is set, Ansible will place the module
|
||||
file in a world-readable temporary directory, with world-readable permissions to
|
||||
allow the ``become_user`` (and incidentally any other user on the system) to
|
||||
read the contents of the file. **If any of the parameters passed to the module
|
||||
are sensitive in nature, and you do not trust the remote machines, then this is
|
||||
a potential security risk.**
|
||||
|
||||
Once the module is done executing, Ansible deletes the temporary file.
|
||||
|
||||
If any of the parameters passed to the module are sensitive in nature, and you do
|
||||
not trust the client machines, then this is a potential danger.
|
||||
|
||||
Ways to resolve this include:
|
||||
Several ways exist to avoid the above logic flow entirely:
|
||||
|
||||
* Use `pipelining`. When pipelining is enabled, Ansible does not save the
|
||||
module to a temporary file on the client. Instead it pipes the module to
|
||||
|
@ -146,12 +186,6 @@ Ways to resolve this include:
|
|||
python modules involving file transfer (for example: :ref:`copy <copy_module>`,
|
||||
:ref:`fetch <fetch_module>`, :ref:`template <template_module>`), or for non-python modules.
|
||||
|
||||
* Install POSIX.1e filesystem acl support on the
|
||||
managed host. If the temporary directory on the remote host is mounted with
|
||||
POSIX acls enabled and the :command:`setfacl` tool is in the remote ``PATH``
|
||||
then Ansible will use POSIX acls to share the module file with the second
|
||||
unprivileged user instead of having to make the file readable by everyone.
|
||||
|
||||
* Avoid becoming an unprivileged
|
||||
user. Temporary files are protected by UNIX file permissions when you
|
||||
``become`` root or do not use ``become``. In Ansible 2.1 and above, UNIX
|
||||
|
@ -167,14 +201,32 @@ Ways to resolve this include:
|
|||
|
||||
Ansible makes it hard to unknowingly use ``become`` insecurely. Starting in Ansible 2.1,
|
||||
Ansible defaults to issuing an error if it cannot execute securely with ``become``.
|
||||
If you cannot use pipelining or POSIX ACLs, you must connect as an unprivileged user,
|
||||
you must use ``become`` to execute as a different unprivileged user,
|
||||
and you decide that your managed nodes are secure enough for the
|
||||
If you cannot use pipelining or POSIX ACLs, must connect as an unprivileged user,
|
||||
must use ``become`` to execute as a different unprivileged user,
|
||||
and decide that your managed nodes are secure enough for the
|
||||
modules you want to run there to be world readable, you can turn on
|
||||
``allow_world_readable_tmpfiles`` in the :file:`ansible.cfg` file. Setting
|
||||
``allow_world_readable_tmpfiles`` will change this from an error into
|
||||
a warning and allow the task to run as it did prior to 2.1.
|
||||
|
||||
.. versionchanged:: 2.10
|
||||
|
||||
Ansible 2.10 introduces the above-mentioned ``ansible_common_remote_group``
|
||||
fallback. As mentioned above, if enabled, it is used when ``remote_user`` and
|
||||
``become_user`` are both unprivileged users. Refer to the text above for details
|
||||
on when this fallback happens.
|
||||
|
||||
.. warning:: As mentioned above, if ``ansible_common_remote_group`` and
|
||||
``allow_world_readable_tmpfiles`` are both enabled, it is unlikely that the
|
||||
world-readable fallback will ever trigger, and yet Ansible might still be
|
||||
unable to access the module file. This is because after the group ownership
|
||||
change is successful, Ansible does not fall back any further, and also does
|
||||
not do any check to ensure that the ``become_user`` is actually a member of
|
||||
the "common group". This is a design decision made by the fact that doing
|
||||
such a check would require another round-trip connection to the remote
|
||||
machine, which is a time-expensive operation. Ansible does, however, emit a
|
||||
warning in this case.
|
||||
|
||||
Not supported by all connection plugins
|
||||
---------------------------------------
|
||||
|
||||
|
|
|
@ -510,11 +510,24 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
file with chown which only works in case the remote_user is
|
||||
privileged or the remote systems allows chown calls by unprivileged
|
||||
users (e.g. HP-UX)
|
||||
* If the chown fails we can set the file to be world readable so that
|
||||
* If the chown fails, we check if ansible_common_remote_group is set.
|
||||
If it is, we attempt to chgrp the file to its value. This is useful
|
||||
if the remote_user has a group in common with the become_user. As the
|
||||
remote_user, we can chgrp the file to that group and allow the
|
||||
become_user to read it.
|
||||
* If (the chown fails AND ansible_common_remote_group is not set) OR
|
||||
(ansible_common_remote_group is set AND the chgrp (or following chmod)
|
||||
returned non-zero), we can set the file to be world readable so that
|
||||
the second unprivileged user can read the file.
|
||||
Since this could allow other users to get access to private
|
||||
information we only do this if ansible is configured with
|
||||
"allow_world_readable_tmpfiles" in the ansible.cfg
|
||||
"allow_world_readable_tmpfiles" in the ansible.cfg. Also note that
|
||||
when ansible_common_remote_group is set this final fallback is very
|
||||
unlikely to ever be triggered, so long as chgrp was successful. But
|
||||
just because the chgrp was successful, does not mean Ansible can
|
||||
necessarily access the files (if, for example, the variable was set
|
||||
to a group that remote_user is in, and can chgrp to, but does not have
|
||||
in common with become_user).
|
||||
"""
|
||||
if remote_user is None:
|
||||
remote_user = self._get_remote_user()
|
||||
|
@ -528,6 +541,8 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
# Unprivileged user that's different than the ssh user. Let's get
|
||||
# to work!
|
||||
|
||||
become_user = self.get_become_option('become_user')
|
||||
|
||||
# Try to use file system acls to make the files readable for sudo'd
|
||||
# user
|
||||
if execute:
|
||||
|
@ -540,7 +555,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
# start to we'll have to fix this.
|
||||
setfacl_mode = 'r-X'
|
||||
|
||||
res = self._remote_set_user_facl(remote_paths, self.get_become_option('become_user'), setfacl_mode)
|
||||
res = self._remote_set_user_facl(remote_paths, become_user, setfacl_mode)
|
||||
if res['rc'] != 0:
|
||||
# File system acls failed; let's try to use chown next
|
||||
# Set executable bit first as on some systems an
|
||||
|
@ -550,12 +565,48 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
if res['rc'] != 0:
|
||||
raise AnsibleError('Failed to set file mode on remote temporary files (rc: {0}, err: {1})'.format(res['rc'], to_native(res['stderr'])))
|
||||
|
||||
res = self._remote_chown(remote_paths, self.get_become_option('become_user'))
|
||||
if res['rc'] != 0 and remote_user in self._get_admin_users():
|
||||
# chown failed even if remote_user is administrator/root
|
||||
raise AnsibleError('Failed to change ownership of the temporary files Ansible needs to create despite connecting as a privileged user. '
|
||||
'Unprivileged become user would be unable to read the file.')
|
||||
elif res['rc'] != 0:
|
||||
res = self._remote_chown(remote_paths, become_user)
|
||||
if res['rc'] != 0:
|
||||
# First check if we are an admin/root user. If we are
|
||||
# and failed here, something weird has happened.
|
||||
if remote_user in self._get_admin_users():
|
||||
# chown failed even if remote_user is administrator/root
|
||||
raise AnsibleError('Failed to change ownership of the temporary files Ansible needs to create despite connecting as a privileged user. '
|
||||
'Unprivileged become user would be unable to read the file.')
|
||||
|
||||
# Otherwise, we're a normal user. We failed to chown the
|
||||
# paths to the unprivileged user, but if we have a common
|
||||
# group with them, we should be able to chown it to that.
|
||||
#
|
||||
# Note that we have no way of knowing if this will actually
|
||||
# work... just because chgrp exits successfully does not
|
||||
# mean that Ansible will work. We could check if the become
|
||||
# user is in the group, but this would create an extra
|
||||
# round trip.
|
||||
#
|
||||
# Also note that due to the above, this can prevent the
|
||||
# ALLOW_WORLD_READABLE_TMPFILES logic below from ever
|
||||
# getting called. We leave this up to the user to rectify
|
||||
# if they have both of these features enabled.
|
||||
group = self.get_shell_option('common_remote_group')
|
||||
if group is not None:
|
||||
res = self._remote_chgrp(remote_paths, group)
|
||||
if res['rc'] == 0:
|
||||
# If ALLOW_WORLD_READABLE_TMPFILES is set, we should warn the user
|
||||
# that something might go weirdly here.
|
||||
if C.ALLOW_WORLD_READABLE_TMPFILES:
|
||||
display.warning('Both common_remote_group and allow_world_readable_tmpfiles are set. chgrp was successful, but there is no '
|
||||
'guarantee that Ansible will be able to read the files after this operation, particularly if '
|
||||
'common_remote_group was set to a group of which the unprivileged become user is not a member. In this '
|
||||
'situation, allow_world_readable_tmpfiles is a no-op. See the "Risks of becoming an unprivileged user" section '
|
||||
'of the "Understanding privilege escalation: become" user guide documentation for more information')
|
||||
if execute:
|
||||
group_mode = 'g+rwx'
|
||||
else:
|
||||
group_mode = 'g+rw'
|
||||
res = self._remote_chmod(remote_paths, group_mode)
|
||||
|
||||
if res['rc'] != 0:
|
||||
if self.get_shell_option('world_readable_temp', C.ALLOW_WORLD_READABLE_TMPFILES):
|
||||
# chown and fs acls failed -- do things this insecure
|
||||
# way only if the user opted in in the config file
|
||||
|
@ -595,6 +646,14 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
|||
res = self._low_level_execute_command(cmd, sudoable=sudoable)
|
||||
return res
|
||||
|
||||
def _remote_chgrp(self, paths, group, sudoable=False):
|
||||
'''
|
||||
Issue a remote chgrp command
|
||||
'''
|
||||
cmd = self._connection._shell.chgrp(paths, group)
|
||||
res = self._low_level_execute_command(cmd, sudoable=sudoable)
|
||||
return res
|
||||
|
||||
def _remote_set_user_facl(self, paths, user, mode, sudoable=False):
|
||||
'''
|
||||
Issue a remote call to setfacl
|
||||
|
|
|
@ -19,6 +19,19 @@ options:
|
|||
key: remote_tmp
|
||||
vars:
|
||||
- name: ansible_remote_tmp
|
||||
common_remote_group:
|
||||
name: Enables changing the group ownership of temporary files and directories
|
||||
default: null
|
||||
description:
|
||||
- Checked when Ansible needs to execute a module as a different user.
|
||||
- If setfacl and chown both fail and do not let the different user access the module's files, they will be chgrp'd to this group.
|
||||
- In order for this to work, the remote_user and become_user must share a common group and this setting must be set to that group.
|
||||
env: [{name: ANSIBLE_COMMON_REMOTE_GROUP}]
|
||||
vars:
|
||||
- name: ansible_common_remote_group
|
||||
ini:
|
||||
- {key: common_remote_group, section: defaults}
|
||||
version_added: "2.10"
|
||||
system_tmpdirs:
|
||||
description:
|
||||
- "List of valid system temporary directories for Ansible to choose when it cannot use
|
||||
|
|
|
@ -107,6 +107,13 @@ class ShellBase(AnsiblePlugin):
|
|||
|
||||
return ' '.join(cmd)
|
||||
|
||||
def chgrp(self, paths, group):
|
||||
cmd = ['chgrp', group]
|
||||
cmd.extend(paths)
|
||||
cmd = [shlex_quote(c) for c in cmd]
|
||||
|
||||
return ' '.join(cmd)
|
||||
|
||||
def set_user_facl(self, paths, user, mode):
|
||||
"""Only sets acls for users as that's really all we need"""
|
||||
cmd = ['setfacl', '-m', 'u:%s:%s' % (user, mode)]
|
||||
|
|
4
test/integration/targets/become_unprivileged/aliases
Normal file
4
test/integration/targets/become_unprivileged/aliases
Normal file
|
@ -0,0 +1,4 @@
|
|||
destructive
|
||||
shippable/posix/group1
|
||||
skip/aix
|
||||
needs/ssh
|
8
test/integration/targets/become_unprivileged/cleanup.yml
Normal file
8
test/integration/targets/become_unprivileged/cleanup.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- name: Clean up host
|
||||
hosts: ssh
|
||||
gather_facts: yes
|
||||
|
||||
# Default, just noted here to be explicit about what is happening:
|
||||
remote_user: root
|
||||
roles:
|
||||
- cleanup_become_unprivileged
|
7
test/integration/targets/become_unprivileged/inventory
Normal file
7
test/integration/targets/become_unprivileged/inventory
Normal file
|
@ -0,0 +1,7 @@
|
|||
[ssh]
|
||||
ssh-pipelining ansible_ssh_pipelining=true
|
||||
#ssh-no-pipelining ansible_ssh_pipelining=false
|
||||
[ssh:vars]
|
||||
ansible_host=localhost
|
||||
ansible_connection=ssh
|
||||
ansible_python_interpreter="{{ ansible_playbook_python }}"
|
|
@ -0,0 +1 @@
|
|||
hello I was copied
|
|
@ -0,0 +1,58 @@
|
|||
- name: Run a command
|
||||
shell: whoami
|
||||
register: whoami
|
||||
|
||||
# TODO: We ignore_errors here because atomic_move has some really weird edge
|
||||
# cases and gives different behavior based on whether the tmpdir we are copying
|
||||
# from is on the same partition as the target or not, among other things. There
|
||||
# is probably work to be done there to either unify the behavior if possible, or
|
||||
# if not, document/add a warning.
|
||||
#
|
||||
# In what follows, unpriv1 is remote_user and unpriv2 is become_user. Both
|
||||
# users are unprivileged.
|
||||
#
|
||||
# In particular, given a system (FreeBSD in my testing, but probably any *nix)
|
||||
# with a single partition, when we connect (as unpriv1) and become unpriv2,
|
||||
# the file ends up being unpriv1:commongroup. We can't chown it after that
|
||||
# since we are become_user, so the file remains owned by unpriv1.
|
||||
#
|
||||
# But when we have multiple partitions, os.rename() in atomic_move fails, and
|
||||
# we end up falling back to a whole new bunch of logic. In the end the file
|
||||
# ends up being creted as unpriv2 and is unpriv2:unpriv2_login_group.
|
||||
#
|
||||
# This creates a bunch of inconsistency and really should be documented better
|
||||
# but the relevant part for *this* test is that in the single-partition case,
|
||||
# we cannot chmod in the `if creating` branch of atomic_move since we do not
|
||||
# own the file. That will generate an error.
|
||||
- name: Copy a file
|
||||
copy:
|
||||
src: baz.txt
|
||||
dest: ~/uh-oh
|
||||
owner: unpriv2
|
||||
group: notcoolenoughforroot
|
||||
mode: 0644
|
||||
ignore_errors: yes
|
||||
|
||||
- name: See if the file exists
|
||||
stat:
|
||||
path: ~/uh-oh
|
||||
register: uh_oh_stat
|
||||
|
||||
#- name: Get files in /var/tmp
|
||||
# find:
|
||||
# paths: "/var/tmp/"
|
||||
# patterns: 'ansible*'
|
||||
# file_type: directory
|
||||
# register: found
|
||||
#
|
||||
#- name: Get latest ansible tmp dir
|
||||
# set_fact:
|
||||
# tmpdir: "{{ found.files | sort(attribute='mtime') | last }}"
|
||||
#
|
||||
#- debug: var=tmpdir
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- whoami.stdout == 'unpriv2'
|
||||
- uh_oh_stat.stat.exists
|
||||
#- tmpdir.gr_name == 'notcoolenoughforroot'
|
|
@ -0,0 +1,74 @@
|
|||
# Do this first so we can use tilde notation while the user still exists
|
||||
- name: Delete homedirs
|
||||
file:
|
||||
path: '~{{ item }}'
|
||||
state: absent
|
||||
with_items:
|
||||
- unpriv1
|
||||
- unpriv2
|
||||
|
||||
- name: Delete users
|
||||
user:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
force: yes # I think this is needed in case pipelining is used and the session remains open
|
||||
with_items:
|
||||
- unpriv1
|
||||
- unpriv2
|
||||
|
||||
- name: Delete groups
|
||||
group:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
with_items:
|
||||
- notcoolenoughforroot
|
||||
- unpriv1
|
||||
- unpriv2
|
||||
|
||||
- name: Fix sudoers.d path for FreeBSD
|
||||
set_fact:
|
||||
sudoers_etc: /usr/local/etc
|
||||
when: ansible_distribution == 'FreeBSD'
|
||||
|
||||
- name: Fix sudoers.d path for everything else
|
||||
set_fact:
|
||||
sudoers_etc: /etc
|
||||
when: ansible_distribution != 'FreeBSD'
|
||||
|
||||
- name: Undo OpenSUSE
|
||||
lineinfile:
|
||||
path: "{{ sudoers_etc }}/sudoers"
|
||||
regexp: '^### Defaults targetpw'
|
||||
line: 'Defaults targetpw'
|
||||
backrefs: yes
|
||||
|
||||
- name: Nuke custom sudoers file
|
||||
file:
|
||||
path: "{{ sudoers_etc }}/sudoers.d/unpriv1"
|
||||
state: absent
|
||||
|
||||
- name: Check if /usr/bin/setfacl exists
|
||||
stat:
|
||||
path: /usr/bin/setfacl
|
||||
register: usr_bin_setfacl
|
||||
|
||||
- name: Check if the /bin/setfacl exists
|
||||
stat:
|
||||
path: /bin/setfacl
|
||||
register: bin_setfacl
|
||||
|
||||
- name: Set path to setfacl
|
||||
set_fact:
|
||||
setfacl_path: /usr/bin/setfacl
|
||||
when: usr_bin_setfacl.stat.exists
|
||||
|
||||
- name: Set path to setfacl
|
||||
set_fact:
|
||||
setfacl_path: /bin/setfacl
|
||||
when: bin_setfacl.stat.exists
|
||||
|
||||
- name: chmod +x setfacl
|
||||
file:
|
||||
path: "{{ setfacl_path }}"
|
||||
mode: +x
|
||||
when: setfacl_path is defined
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
####################################################################
|
||||
# NOTE! Any destructive changes you make here... Undo them in
|
||||
# cleanup_become_unprivileged so that they don't affect other tests.
|
||||
####################################################################
|
||||
|
||||
- name: Create groups for unprivileged users
|
||||
group:
|
||||
name: "{{ item }}"
|
||||
with_items:
|
||||
- notcoolenoughforroot
|
||||
- unpriv1
|
||||
- unpriv2
|
||||
|
||||
# MacOS requires unencrypted password
|
||||
- name: Set password for unpriv1 (MacOSX)
|
||||
set_fact:
|
||||
password: 'iWishIWereCoolEnoughForRoot!'
|
||||
when: ansible_distribution == 'MacOSX'
|
||||
|
||||
- name: Set password for unpriv1 (everything else)
|
||||
set_fact:
|
||||
password: $6$CRuKRUfAoVwibjUI$1IEOISMFAE/a0VG73K9QsD0uruXNPLNkZ6xWg4Sk3kZIXwv6.YJLECzfNjn6pu8ay6XlVcj2dUvycLetL5Lgx1
|
||||
when: ansible_distribution != 'MacOSX'
|
||||
|
||||
# This user is special. It gets a password so we can sudo as it
|
||||
# (we set the sudo password in runme.sh) and it gets wheel so it can
|
||||
# `become` unpriv2 without an overly complex sudoers file.
|
||||
- name: Create first unprivileged user
|
||||
user:
|
||||
name: unpriv1
|
||||
groups: unpriv1,notcoolenoughforroot
|
||||
append: yes
|
||||
password: "{{ password }}"
|
||||
|
||||
- name: Create second unprivileged user
|
||||
user:
|
||||
name: unpriv2
|
||||
groups: unpriv2,notcoolenoughforroot
|
||||
append: yes
|
||||
|
||||
- name: Create .ssh for unpriv1
|
||||
file:
|
||||
path: ~unpriv1/.ssh
|
||||
state: directory
|
||||
owner: unpriv1
|
||||
group: unpriv1
|
||||
mode: 0700
|
||||
|
||||
- name: Set authorized key for unpriv1
|
||||
copy:
|
||||
src: ~root/.ssh/authorized_keys
|
||||
dest: ~unpriv1/.ssh/authorized_keys
|
||||
remote_src: yes
|
||||
owner: unpriv1
|
||||
group: unpriv1
|
||||
mode: 0600
|
||||
|
||||
# Without this we get:
|
||||
# "Failed to connect to the host via ssh: "System is booting up. Unprivileged
|
||||
# users are not permitted to log in yet. Please come back later."
|
||||
- name: Nuke /run/nologin
|
||||
file:
|
||||
path: /run/nologin
|
||||
state: absent
|
||||
|
||||
- name: Fix sudoers.d path for FreeBSD
|
||||
set_fact:
|
||||
sudoers_etc: /usr/local/etc
|
||||
when: ansible_distribution == 'FreeBSD'
|
||||
|
||||
- name: Fix sudoers.d path for everything else
|
||||
set_fact:
|
||||
sudoers_etc: /etc
|
||||
when: sudoers_etc is not defined
|
||||
|
||||
- name: Chown group for bsd and osx
|
||||
set_fact:
|
||||
chowngroup: wheel
|
||||
when: ansible_distribution in ('FreeBSD', 'MacOSX')
|
||||
|
||||
- name: Chown group for everything else
|
||||
set_fact:
|
||||
chowngroup: root
|
||||
when: chowngroup is not defined
|
||||
|
||||
- name: Make it so unpriv1 can sudo (Chapter 1)
|
||||
copy:
|
||||
dest: "{{ sudoers_etc }}/sudoers.d/unpriv1"
|
||||
content: unpriv1 ALL=(ALL) ALL
|
||||
owner: root
|
||||
group: "{{ chowngroup }}"
|
||||
mode: 0644
|
||||
|
||||
# OpenSUSE has a weird sudo default here and requires the root pw
|
||||
# instead of the user pw. Undo that setting, we can clean it up later.
|
||||
- name: Make it so unpriv1 can sudo (Chapter 2 - The Return Of the OpenSUSE)
|
||||
lineinfile:
|
||||
dest: "{{ sudoers_etc }}/sudoers"
|
||||
regexp: '^Defaults targetpw'
|
||||
line: '### Defaults targetpw'
|
||||
backrefs: yes
|
||||
|
||||
- name: Check if /usr/bin/setfacl exists
|
||||
stat:
|
||||
path: /usr/bin/setfacl
|
||||
register: usr_bin_setfacl
|
||||
|
||||
- name: Check if the /bin/setfacl exists
|
||||
stat:
|
||||
path: /bin/setfacl
|
||||
register: bin_setfacl
|
||||
|
||||
- name: Set path to setfacl
|
||||
set_fact:
|
||||
setfacl_path: /usr/bin/setfacl
|
||||
when: usr_bin_setfacl.stat.exists
|
||||
|
||||
- name: Set path to setfacl
|
||||
set_fact:
|
||||
setfacl_path: /bin/setfacl
|
||||
when: bin_setfacl.stat.exists
|
||||
|
||||
- name: chmod -x setfacl
|
||||
file:
|
||||
path: "{{ setfacl_path }}"
|
||||
mode: -x
|
||||
when: setfacl_path is defined
|
14
test/integration/targets/become_unprivileged/runme.sh
Executable file
14
test/integration/targets/become_unprivileged/runme.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -ux
|
||||
|
||||
ansible-playbook setup.yml -i inventory -v "$@"
|
||||
|
||||
export ANSIBLE_KEEP_REMOTE_FILES=True
|
||||
export ANSIBLE_COMMON_REMOTE_GROUP=notcoolenoughforroot
|
||||
export ANSIBLE_BECOME_PASS='iWishIWereCoolEnoughForRoot!'
|
||||
|
||||
ansible-playbook test.yml -i inventory -v "$@"
|
||||
|
||||
# Do a few cleanup tasks (nuke users, groups, and homedirs, undo config changes)
|
||||
ansible-playbook cleanup.yml -i inventory -v "$@"
|
8
test/integration/targets/become_unprivileged/setup.yml
Normal file
8
test/integration/targets/become_unprivileged/setup.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- name: Set up host
|
||||
hosts: ssh
|
||||
gather_facts: yes
|
||||
|
||||
# Default, just noted here to be explicit about what is happening:
|
||||
remote_user: root
|
||||
roles:
|
||||
- setup_become_unprivileged
|
8
test/integration/targets/become_unprivileged/test.yml
Normal file
8
test/integration/targets/become_unprivileged/test.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
- name: Run the test
|
||||
hosts: ssh
|
||||
gather_facts: yes
|
||||
remote_user: unpriv1
|
||||
become: yes
|
||||
become_user: unpriv2
|
||||
roles:
|
||||
- become_unprivileged
|
Loading…
Reference in a new issue