Add new role, 'debops.system_groups'

parent 23868736
...@@ -1223,6 +1223,14 @@ stages: ...@@ -1223,6 +1223,14 @@ stages:
JANE_DIFF_PATTERN: '.*/debops.sudo/.*' JANE_DIFF_PATTERN: '.*/debops.sudo/.*'
JANE_LOG_PATTERN: '\[debops\.sudo\]' JANE_LOG_PATTERN: '\[debops\.sudo\]'
'system_groups role':
<<: *test_role_1st_deps
variables:
JANE_TEST_PLAY: '${DEBOPS_PLAYBOOKS}/service/system_groups.yml'
JANE_INVENTORY_GROUPS: 'debops_service_system_groups'
JANE_DIFF_PATTERN: '.*/debops.system_groups/.*'
JANE_LOG_PATTERN: '\[debops\.system_groups\]'
'swapfile role': 'swapfile role':
<<: *test_role_no_deps <<: *test_role_no_deps
variables: variables:
......
...@@ -29,6 +29,9 @@ Added ...@@ -29,6 +29,9 @@ Added
- :ref:`debops.sudo`: install and manage :command:`sudo` configuration on - :ref:`debops.sudo`: install and manage :command:`sudo` configuration on
a host. The role is included in the ``common.yml`` playbook. a host. The role is included in the ``common.yml`` playbook.
- :ref:`debops.system_groups`: configure UNIX system groups used on DebOps
hosts.
- [debops.users] Selected UNIX accounts can now be configured to linger when - [debops.users] Selected UNIX accounts can now be configured to linger when
not logged in via the ``item.linger`` parameter. This allows these accounts not logged in via the ``item.linger`` parameter. This allows these accounts
to maintain long-running services when not logged in via their own private to maintain long-running services when not logged in via their own private
......
---
- name: Configure UNIX system groups
hosts: [ 'debops_all_hosts', 'debops_service_system_groups' ]
become: True
environment: '{{ inventory__environment | d({})
| combine(inventory__group_environment | d({}))
| combine(inventory__host_environment | d({})) }}'
roles:
- role: debops.sudo
tags: [ 'role::sudo' ]
- role: debops.system_groups
tags: [ 'role::system_groups' ]
debops.system_groups - Manage UNIX system groups with Ansible
Copyright (C) 2018 Maciej Delmanowski <drybjed@gmail.com>
Copyright (C) 2018 DebOps https://debops.org/
This Ansible role is part of DebOps.
DebOps is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3, as
published by the Free Software Foundation.
DebOps is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DebOps. If not, see https://www.gnu.org/licenses/.
---
# .. vim: foldmarker=[[[,]]]:foldmethod=marker
# debops.system_groups default variables
# ======================================
# .. contents:: Sections
# :local:
# General configuration [[[
# -------------------------
# .. envvar:: system_groups__enabled [[[
#
# Enable or disable support for managing UNIX system groups.
system_groups__enabled: True
# ]]]
# .. envvar:: system_groups__admins_sudo_nopasswd [[[
#
# If enabled, the role will add the ``NOPASSWD:`` tag in the ``sudoers``
# configuration of the ``admins`` and ``wheel`` UNIX groups. This allows
# execution of :command:`sudo` commands without password authentication.
# See :man:`sudoers(5)` for more details.
#
# You can disable this and configure the ``ansible_become_pass`` variable in
# the Ansible inventory for each affected host to provide password
# authentication. You can use the Ansible Vault functionality to encrypt the
# password in inventory variables, or store the password in the :file:`secret/`
# directory and use the ``lookup('file')`` module to retrieve it.
# See :ref:`debops.secret` documentation for details.
system_groups__admins_sudo_nopasswd: True
# ]]]
# ]]]
# UNIX system groups [[[
# ----------------------
# These lists define what UNIX system groups should be present on
# DebOps-managed hosts and configure additional facilities like :command:`sudo`
# access. See :ref:`system_groups__ref_list` for more details.
# .. envvar:: system_groups__default_list [[[
#
# List of UNIX system groups defined by default by the role.
system_groups__default_list:
# This is the current default UNIX group which grants unrestricted 'root'
# shell access via the `sudo` command.
#
# Users in the 'admins' UNIX group are allowed to connect to the host via SSH
# service and gain shell access on the host. They can also use the `sudo`
# command to execute commands as any UNIX account or gain superuser ('root')
# access.
- name: 'admins'
sudoers: |
# This might be required to allow Ansible pipelining connections
Defaults: %admins !requiretty
# This variable is used to configure access by Ansible Controller hosts
Defaults: %admins env_check += "SSH_CLIENT"
# Allow execution of any command as any user on the system.
# This is required for Ansible operation.
%admins ALL = (ALL:ALL) {{ 'NOPASSWD: ' if system_groups__admins_sudo_nopasswd|bool else '' }}ALL
members: '{{ ansible_local.core.admin_users
if (ansible_local|d() and ansible_local.core|d() and
ansible_local.core.admin_users|d())
else [] }}'
access: [ 'root', 'sshd' ]
# This might be a new future UNIX system group that grants admin access, it
# is not currently created on the hosts.
# See https://en.wikipedia.org/wiki/Wheel_(Unix_term) for rationale.
#
# Users in the 'wheel' UNIX group are allowed to connect to the host via SSH
# service and gain shell access on the host. They can also use the `sudo`
# command to execute commands as any UNIX account or gain superuser ('root')
# access.
- name: 'wheel'
sudoers: |
# This might be required to allow Ansible pipelining connections
Defaults: %wheel !requiretty
# This variable is used to configure access by Ansible Controller hosts
Defaults: %wheel env_check += "SSH_CLIENT"
# Allow execution of any command as any user on the system.
# This is required for Ansible operation.
%wheel ALL = (ALL:ALL) {{ 'NOPASSWD: ' if system_groups__admins_sudo_nopasswd|bool else '' }}ALL
members: '{{ ansible_local.core.admin_users
if (ansible_local|d() and ansible_local.core|d() and
ansible_local.core.admin_users|d())
else [] }}'
access: [ 'root', 'sshd' ]
state: 'init'
# This group is present on Debian installations by default.
#
# Users in the 'adm' UNIX group have read-only access to various log files in
# the '/var/log/' directory as well as firewall configuration in the
# '/etc/ferm/' directory.
- name: 'adm'
members: '{{ ansible_local.core.admin_users
if (ansible_local|d() and ansible_local.core|d() and
ansible_local.core.admin_users|d())
else [] }}'
# This group is present on Debian installations by default.
#
# Users in the 'staff' UNIX group have write access to the '/usr/local/' and
# '/var/local/' directories and can manage content inside of them.
- name: 'staff'
members: '{{ ansible_local.core.admin_users
if (ansible_local|d() and ansible_local.core|d() and
ansible_local.core.admin_users|d())
else [] }}'
# Users in the 'sshusers' UNIX group are allowed to connect to the host via
# SSH service and gain shell access on the host. See the 'debops.sshd' role
# for more details.
- name: 'sshusers'
access: [ 'sshd' ]
# Users in the 'sftponly' UNIX group have access to chrooted SFTP service,
# without full shell access. They cannot use SSH public keys in the
# '~/.ssh/authorized_keys' file, only keys in the
# '/etc/ssh/authorized_keys.d/<user>' file are allowed.
# See the 'debops.sshd' and 'debops.authorized_keys' roles for more details.
- name: 'sftponly'
access: [ 'sshd' ]
# This is a UNIX group used in multiple DebOps roles. Its configuration will
# be conditional in the future so that it's not created on DebOps hosts that
# don't provide webserver services.
#
# Users in the 'webadmins' UNIX group can reload webserver services using
# specific `sudo` commands. See the 'debops.nginx' or 'debops.php' roles for
# more details.
- name: 'webadmins'
access: [ 'webserver' ]
# ]]]
# .. envvar:: system_groups__list [[[
#
# List of UNIX system groups that should be present on all hosts in the Ansible
# inventory.
system_groups__list: []
# ]]]
# .. envvar:: system_groups__group_list [[[
#
# List of UNIX system groups that should be present on hosts in a specific
# Ansible inventory group.
system_groups__group_list: []
# ]]]
# .. envvar:: system_groups__host_list [[[
#
# List of UNIX system groups that should be present on specific hosts in the
# Ansible inventory.
system_groups__host_list: []
# ]]]
# .. envvar:: system_groups__dependent_list [[[
#
# List of UNIX system groups that are defined by other Ansible roles via role
# dependent variables.
system_groups__dependent_list: []
# ]]]
# .. envvar:: system_groups__combined_list [[[
#
# List which combines all of the other UNIX group lists and is used in the role
# tasks.
system_groups__combined_list: '{{ system_groups__default_list
+ system_groups__dependent_list
+ system_groups__list
+ system_groups__group_list
+ system_groups__host_list }}'
# ]]]
# ]]]
---
dependencies:
- role: debops.ansible_plugins
galaxy_info:
author: 'Maciej Delmanowski'
description: 'Manage UNIX system groups'
company: 'DebOps'
license: 'GPL-3.0'
min_ansible_version: '2.4.0'
platforms:
- name: Ubuntu
versions:
- precise
- trusty
- xenial
- bionic
- name: Debian
versions:
- wheezy
- jessie
- stretch
- buster
categories:
- system
- access
- authz
- acl
- sudo
- ssh
---
- name: Ensure that requested UNIX system groups exist
group:
name: '{{ item.name }}'
gid: '{{ item.gid | d(omit) }}'
state: 'present'
system: '{{ item.system | d(True) }}'
with_items: '{{ system_groups__combined_list | parse_kv_items }}'
when: system_groups__enabled|bool and
item.name|d() and item.state|d('present') not in [ 'init', 'absent', 'ignore' ]
- name: Get list of existing UNIX accounts
getent:
database: 'passwd'
when: system_groups__enabled|bool
- name: Add specified UNIX accounts to system groups if present
user:
name: '{{ item.name }}'
append: '{{ item.append }}'
groups: '{{ item.groups }}'
with_items: '{{ lookup("template", "lookup/system_groups_members.j2") | from_yaml }}'
when: system_groups__enabled|bool
- name: Remove sudo configuration if not specified
file:
path: '/etc/sudoers.d/{{ item.sudoers_filename | d("system_groups-" + item.name) }}'
state: 'absent'
with_items: '{{ system_groups__combined_list | parse_kv_items }}'
when: system_groups__enabled|bool and
item.name|d() and item.state|d('present') not in [ 'init', 'absent', 'ignore' ] and
not item.sudoers|d()
- name: Configure sudo for UNIX system groups
template:
src: 'etc/sudoers.d/system_groups.j2'
dest: '/etc/sudoers.d/{{ item.sudoers_filename | d("system_groups-" + item.name) }}'
owner: 'root'
group: 'root'
mode: '0440'
validate: '/usr/sbin/visudo -cf %s'
with_items: '{{ system_groups__combined_list | parse_kv_items }}'
when: system_groups__enabled|bool and
item.name|d() and item.state|d('present') not in [ 'init', 'absent', 'ignore' ] and
item.sudoers|d()
- name: Remove tmpfiles configuration if not specified
file:
path: '/etc/tmpfiles.d/{{ item.tmpfiles_filename | d("system_groups-" + item.name + ".conf") }}'
state: 'absent'
with_items: '{{ system_groups__combined_list | parse_kv_items }}'
when: system_groups__enabled|bool and ansible_service_mgr == 'systemd' and
item.name|d() and item.state|d('present') not in [ 'init', 'absent', 'ignore' ] and
not item.tmpfiles|d()
- name: Generate tmpfiles configuration for UNIX system groups
template:
src: 'etc/tmpfiles.d/system_groups.conf.j2'
dest: '/etc/tmpfiles.d/{{ item.tmpfiles_filename | d("system_groups-" + item.name + ".conf") }}'
owner: 'root'
group: 'root'
mode: '0644'
with_items: '{{ system_groups__combined_list | parse_kv_items }}'
register: system_groups__register_tmpfiles
when: system_groups__enabled|bool and ansible_service_mgr == 'systemd' and
item.name|d() and item.state|d('present') not in [ 'init', 'absent', 'ignore' ] and
item.tmpfiles|d()
- name: Ensure that missing tmpfiles are present
command: systemd-tmpfiles --create
when: system_groups__enabled|bool and ansible_service_mgr == 'systemd' and
system_groups__register_tmpfiles is changed
- name: Make sure that Ansible local facts directory exists
file:
path: '/etc/ansible/facts.d'
state: 'directory'
owner: 'root'
group: 'root'
mode: '0755'
- name: Save system groups local facts
template:
src: 'etc/ansible/facts.d/system_groups.fact.j2'
dest: '/etc/ansible/facts.d/system_groups.fact'
owner: 'root'
group: 'root'
mode: '0755'
register: system_groups__register_facts
- name: Update Ansible facts if they were modified
action: setup
when: system_groups__register_facts is changed
#!/usr/bin/env python
# {{ ansible_managed }}
from __future__ import print_function
from json import loads, dumps
from sys import exit
# A workaround for Jinja templates in Python scripts
"""
{% set system_groups__tpl_acl = {} %}
{% set system_groups__tpl_access = (
ansible_local.system_groups.access
if (ansible_local|d() and ansible_local.system_groups|d() and
ansible_local.system_groups.access|d()) else {}) %}
{% for element in (system_groups__combined_list | parse_kv_items) %}
{% if (element.name|d() and element.state|d('present') not in
[ 'init', 'absent', 'ignore' ]) %}
{% if element.access|d() %}
{% for token in ([ element.access ]
if element.access is string
else element.access) %}
{% set _ = system_groups__tpl_access.update(
{token:
(((system_groups__tpl_access[token]|d([]))
+ [ element.name ]) | unique)}) %}
{% endfor %}
{% endif %}
{% if element.allow|d() %}
{% for token in ([ element.allow ]
if element.allow is string
else element.allow) %}
{% set _ = system_groups__tpl_access.update(
{token:
(((system_groups__tpl_access[token]|d([]))
+ [ element.name ]) | unique)}) %}
{% endfor %}
{% endif %}
{% if element.deny|d() %}
{% for token in ([ element.deny ]
if element.deny is string
else element.deny) %}
{% if element.name in (system_groups__tpl_access[token]|d([])) %}
{% set _ = system_groups__tpl_access[token].remove(element.name) %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{% for element, group_list in system_groups__tpl_access.items() %}
{% if group_list %}
{% set _ = system_groups__tpl_acl.update({element: group_list}) %}
{% endif %}
{% endfor %}
"""
output = loads("""{{ {'configured': True,
'enabled': system_groups__enabled|bool,
'access': system_groups__tpl_acl
} | to_nice_json }}""")
print(dumps(output, sort_keys=True, indent=4))
# {{ ansible_managed }}
# Configuration managed by the 'debops.system_groups' Ansible role
{{ item.sudoers }}
# {{ ansible_managed }}
# Configuration managed by the 'debops.system_groups' Ansible role
#Type Path Mode UID GID Age Argument
{{ item.tmpfiles }}
{% set system_groups__tpl_items = [] %}
{% for account in getent_passwd.keys() %}
{% set system_groups__tpl_append_groups = [] %}
{% for element in (system_groups__combined_list | parse_kv_items) %}
{% if element.name|d() and element.state|d('present') not in [ 'init', 'absent', 'ignore' ] %}
{% if (element.members|d() and account in ([ element.members ] if element.members is string else element.members)) %}
{% set _ = system_groups__tpl_append_groups.append(element.name) %}
{% endif %}
{% endif %}
{% endfor %}
{% if system_groups__tpl_append_groups %}
{% set _ = system_groups__tpl_items.append({
"name": account,
"append": True,
"groups": (system_groups__tpl_append_groups | join(','))
}) %}
{% endif %}
{% endfor %}
{{ system_groups__tpl_items | to_yaml }}
...@@ -231,6 +231,7 @@ Security ...@@ -231,6 +231,7 @@ Security
- :ref:`debops.proc_hidepid` - :ref:`debops.proc_hidepid`
- :ref:`debops.sshd` - :ref:`debops.sshd`
- :ref:`debops.sudo` - :ref:`debops.sudo`
- :ref:`debops.system_groups`
- :ref:`debops.tcpwrappers` - :ref:`debops.tcpwrappers`
- ``debops-contrib.apparmor`` - ``debops-contrib.apparmor``
- ``debops-contrib.firejail`` - ``debops-contrib.firejail``
...@@ -257,6 +258,7 @@ System configuration ...@@ -257,6 +258,7 @@ System configuration
- :ref:`debops.sysctl` - :ref:`debops.sysctl`
- :ref:`debops.sysfs` - :ref:`debops.sysfs`
- :ref:`debops.sysnews` - :ref:`debops.sysnews`
- :ref:`debops.system_groups`
- :ref:`debops.users` - :ref:`debops.users`
- ``debops.console`` - ``debops.console``
- ``debops.gitusers`` - ``debops.gitusers``
......
Default variable details
========================
Some of ``debops.system_groups`` default variables have more extensive
configuration than simple strings or lists, here you can find documentation and
examples for them.
.. contents::
:local:
:depth: 1
.. _system_groups__ref_list:
system_groups__list
-------------------
The ``system_groups__*_list`` variables define what UNIX system groups should
be present on a given host, and can optionally create or modify configuration
of selected services. The variables are list of YAML dictionaries, each
dictionary defining one UNIX group.
The lists are combined together in the order specified by the
:envvar:`system_groups__combined_list` variable and the entries with the same
``name`` parameters will be merged. This can be used to change the
configuration of existing entries via Ansible inventory.
Each entry can specify a set of parameters:
``name``
Required. The name of the UNIX group to manage. This should be an
alphanumeric string, you can check the :man:`groupadd(8)` manpage for allowed
characters. This parameter is used as a key for merging multiple
configuration entries together in order of appearance.
``gid``
Optional. Specify the group ID (GID) of a given UNIX group. If not specified,
it will be selected automatically.
``system``
Optional, boolean. If ``True`` (default), the created UNIX group will be
a "system" group with GID < 1000.
``state``
Optional. If ``present``, the specified UNIX group will be created and its
configuration in different services will be set. If ``absent``, the UNIX
group will not be created, but existing configuration will be left in place.
If ``init``, the configuration for a given UNIX group will be prepared but it
will not be active - this can be done conditionally in a later configuration
entry. If ``ignore``, a given configuration entry will be ignored by the role
and its parameters will not affect a given UNIX group.
``members``
Optional. List of UNIX accounts that should be the members of a given UNIX
group. Only existing UNIX accounts will be added by the role.
``sudoers``
Optional. A string or YAML text block which specifies the :command:`sudo`
configuration for a given UNIX group. It will be saved as
:file:`/etc/sudoers.d/system_groups-<group>` configuration file.
If the value is ``False``, or the parameter is not specified, the
:command:`sudo` configuration file will be removed.
See :man:`sudoers(5)` manual page for information about the configuration
syntax. The role does not ensure that the configuration is related to the
specified UNIX group, you should ensure that independently using the
:command:`sudo` configuration options.
``sudoers_filename``
Optional. Override the filename of the :command:`sudo` configuration file in
the :file:`/etc/sudoers.d/` directory. This might be useful if you need to
change the order of the :command:`sudo` configuration options. You shouldn't
change the filename of existing configuration, because the role will lose
track of it.
``tmpfiles``
Optional. A string or YAML text block which specifies the configuration of
temporary files and directories maintained by the :command:`system-tmpfiles`
command. It will be saved as
:file:`/etc/tmpfiles.d/system_groups-<group>.conf` configuration file.
If the value is ``False``, or the parameter is not specified, the
:command:`systemd-tmpfiles` configuration file will be removed.
See :man:`tmpfiles.d(5)` manual page for information about the configuration
syntax. The role does not ensure that the configuration is related to the
specified UNIX group, you should ensure that independently using the
:command:`systemd-tmpfiles` configuration options.
``tmpfiles_filename``
Optional. Override the filename of the :command:`systemd-tmpfiles`
configuration file in the :file:`/etc/tmpfiles.d/` directory. This might be
useful if you need to change the order of the :command:`systemd-tmpfiles`
configuration options. You shouldn't change the filename of existing
configuration, because the role will lose track of it. The filename should
contain the ``.conf`` suffix, otherwise it will be ignored by
:command:`systemd-tmpfiles` command.
The role maintains a simple :ref:`Access Control List <system_groups__ref_acl>`
using Ansible local facts which can be used by other Ansible roles to augment
their configuration. The parameters below control the ACL configuration.
``access``
Optional. A string or a list of resources which correspond to Access Control
List entries. A given UNIX group will be added to all of the ACL entries with
corresponding resources.
The ``access`` parameter should be used in default or initial configuration,
using it in the inventory will override the default list of resources of
a given UNIX group.
``allow``
Optional. A string or a list of resources which correspond to Access Control
List entries. A given UNIX group will be added to all of the ACL entries with
corresponding resources.
The ``allow`` parameter should be used in additional configuration entries to
augment an existing ACL entries. Currently the configuration of ACL from
multiple entries is not merged automatically, but existing ACL entries are
preserved.
``deny``
Optional. A string or a list of resources which corresdpond to Access Control
List entries. A given UNIX group will be removed from all of the ACL entries
specified here.
The ``deny`` parameter should be used in additional configuration entries to
augment an existing ACL entries. Currently the configuration of ACL from
multiple entries is not merged automatically, but existing ACL entries are
preserved.
Examples
~~~~~~~~
Create a system UNIX group for an application that is composed of multiple UNIX
accounts for better access control. The group will use a temporary directory as
a shared communication channel and will allow its members to reload system
services via :command:`sudo` commands. Members of the group will be allowed to
connect to the host via SSH.
.. code-block:: yaml
system_groups__list:
- name: 'application'
members: [ 'app-core', 'app-webui', 'app-admin1', 'app-admin2' ]
sudoers: |
User_Alias APP_ADMINS = app-admin1, app-admin2
Runas_Alias APP_SERVICES = app-core, app-webui
Cmnd_Alias APP_RELOAD = /bin/systemctl reload app-core.service,\
/bin/systemctl reload app-webui.service
Cmnd_Alias APP_RESTART = /bin/systemctl restart app-core.service,\
/bin/systemctl restart app-webui.service
Cmnd_Alias APP_STATUS = /bin/systemctl status app-core.service,\
/bin/systemctl status app-webui.service
# Allow service reloads for all members, even services
%application ALL = (root) NOPASSWD: APP_RELOAD
# Allow more control over services for application administrators
APP_ADMINS ALL = (root) NOPASSWD: APP_RESTART, APP_STATUS
# Allow administrators to switch to the service UNIX accounts and run
# commands on their behalf, after authentication
APP_ADMINS ALL = (APP_SERVICES) ALL
tmpfiles: |
# Temporary directory for UNIX sockets
d /run/application 2771 root application - -
access: [ 'sshd' ]
You might need to add the individual accounts to the UNIX group in your role if
they don't exist before the ``debops.system_groups`` role is executed,
afterwards the role will ensure that the specified members are present in the
group.
Getting started
===============
.. contents::
:local:
.. _system_groups__ref_acl:
Access Control List
-------------------
The ``debops.system_groups`` role maintains a simple Access Control List in the
Ansible local facts, under ``ansible_local.system_groups.access.*`` variable
hierarchy. Other roles can inspect it to get a list of UNIX group names which
they can use to configure access in their respective applications.
The ``ansible_local.system_groups.access`` variable is a YAML dictionary. Each
key of this dictionary corresponds to a particular resource, and the value is
a list of UNIX group names. The resources are user-defined, by default the role
creates:
``root``
Members of these UNIX groups have full, privileged access to the ``root``
account on a given host. This resource should be reserved to system
administrators.
``sshd``
Members of these UNIX groups can login to the host via the SSH service.
See :ref:`debops.sshd` role for more details.
``webserver``