Add new role, 'debops.pam_access'

parent f8e88c23
......@@ -984,6 +984,14 @@ stages:
# --- p --- [[[2
'pam_access role':
<<: *test_role_no_deps
variables:
JANE_TEST_PLAY: '${DEBOPS_PLAYBOOKS}/service/pam_access.yml'
JANE_INVENTORY_GROUPS: 'debops_service_pam_access'
JANE_DIFF_PATTERN: '.*/debops.pam_access/.*'
JANE_LOG_PATTERN: '\[debops\.pam_access\]'
'persistent_paths role':
<<: *test_role_no_deps
variables:
......
......@@ -35,6 +35,10 @@ Added
- :ref:`debops.nslcd` role can be used to configure LDAP lookups for NSS and
PAM services on a Linux host.
- :ref:`debops.pam_access` role manages PAM access control files located in
the :file:`/etc/security/` directory. The role is designed to allow other
Ansible roles to easily manage their own PAM access rules.
- [debops.nginx] The role will automatically generate configuration which
redirects short hostnames or subdomains to their FQDN equivalents. This
allows HTTP clients to reach websites by specifying their short names via DNS
......
......@@ -111,6 +111,9 @@
- role: debops.system_groups
tags: [ 'role::system_groups', 'skip::system_groups' ]
- role: debops.pam_access
tags: [ 'role::pam_access', 'skip::pam_access' ]
- role: debops.etc_services
tags: [ 'role::etc_services', 'skip::etc_services' ]
etc_services__dependent_list:
......
---
- name: Manage PAM Access Control Lists
hosts: [ 'debops_all_hosts', 'debops_service_pam_access' ]
become: True
environment: '{{ inventory__environment | d({})
| combine(inventory__group_environment | d({}))
| combine(inventory__host_environment | d({})) }}'
roles:
- role: debops.pam_access
tags: [ 'role::pam_access', 'skip::pam_access' ]
debops.pam_access - Manage pam_access(8) ACL rules using Ansible
Copyright (C) 2019 Maciej Delmanowski <drybjed@gmail.com>
Copyright (C) 2019 DebOps Project 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 http://www.gnu.org/licenses/.
---
# .. vim: foldmarker=[[[,]]]:foldmethod=marker
# debops.pam_access default variables
# ===================================
# .. contents:: Sections
# :local:
#
# .. include:: ../../../includes/global.rst
# General configuration [[[
# -------------------------
# .. envvar:: pam_access__enabled [[[
#
# Enable or disable support for PAM Access Control files management.
pam_access__enabled: True
# ]]]
# ]]]
# Access Control configuration [[[
# --------------------------------
# These variables define the contents of PAM Access Control files, located in
# the :file:`/etc/security/` directory. See :ref:pam_access__ref_rules` for
# more details.
# .. envvar:: pam_access__default_rules [[[
#
# List of PAM Access Control rules defined by the role.
pam_access__default_rules:
- name: 'global'
filename: 'access.conf'
divert: True
state: 'init'
options: []
# ]]]
# .. envvar:: pam_access__rules [[[
#
# List of PAM Access Control rules defined on all hosts in the Ansible
# inventory.
pam_access__rules: []
# ]]]
# .. envvar:: pam_access__group_rules [[[
#
# List of PAM Access Control rules defined on hosts in a specific Ansible
# inventory group.
pam_access__group_rules: []
# ]]]
# .. envvar:: pam_access__host_rules [[[
#
# List of PAM Access Control rules defined on specific hosts in the Ansible
# inventory.
pam_access__host_rules: []
# ]]]
# .. envvar:: pam_access__dependent_rules [[[
#
# List of the PAM Access Control rules defined via dependent role variables.
pam_access__dependent_rules: []
# ]]]
# .. envvar:: pam_access__combined_rules [[[
#
# The variable that combines all other PAM Access Control rules variables and
# is used in the role tasks and templates.
pam_access__combined_rules: '{{ q("flattened", (pam_access__default_rules
+ pam_access__dependent_rules
+ pam_access__rules
+ pam_access__group_rules
+ pam_access__host_rules)) }}'
# ]]]
# ]]]
---
dependencies:
- role: debops.ansible_plugins
galaxy_info:
role_name: 'pam_access'
company: 'DebOps'
author: 'Maciej Delmanowski'
description: 'Manage pam_access(8) ACL rules'
license: 'GPL-3.0'
min_ansible_version: '2.7.0'
platforms:
- name: Ubuntu
versions:
- xenial
- bionic
- name: Debian
versions:
- jessie
- stretch
- buster
galaxy_tags:
- pam
- acl
- security
---
- name: Check current pam_access diversions
environment:
LC_ALL: 'C'
shell: set -o nounset -o pipefail -o errexit &&
dpkg-divert --list '/etc/security/access*.conf.dpkg-divert' | awk '{print $NF}' || true
args:
executable: '/bin/bash'
register: pam_access__register_diversions
check_mode: False
changed_when: False
- name: Divert PAM access control files
vars:
access_conf: '{{ "/etc/security/"
+ item.filename | d("access-" + (item.name | regex_replace("\.conf$","")) + ".conf") }}'
command: dpkg-divert --quiet --local
--divert {{ access_conf }}.dpkg-divert
--rename {{ access_conf }}
loop: '{{ pam_access__combined_rules | parse_kv_items }}'
loop_control:
label: '{{ {"access_conf": access_conf, "state": item.state|d("present")} }}'
when: (pam_access__enabled|bool and item.name|d() and item.options|d() and
item.state|d('present') not in [ 'absent', 'init', 'ignore' ] and
((item.divert|d())|bool and
(access_conf + '.dpkg-divert') not in pam_access__register_diversions.stdout_lines))
- name: Generate PAM access control files
vars:
access_conf: '{{ "/etc/security/"
+ item.filename | d("access-" + (item.name | regex_replace("\.conf$","")) + ".conf") }}'
template:
src: 'etc/security/access.conf.j2'
dest: '/etc/security/{{ item.filename | d("access-" + (item.name | regex_replace("\.conf$","")) + ".conf") }}'
mode: '0644'
loop: '{{ pam_access__combined_rules | parse_kv_items }}'
loop_control:
label: '{{ {"access_conf": access_conf, "state": item.state|d("present")} }}'
when: pam_access__enabled|bool and item.name|d() and item.options|d() and
item.state|d('present') not in [ 'absent', 'init', 'ignore' ]
- name: Remove PAM access control files
vars:
access_conf: '{{ "/etc/security/"
+ item.filename | d("access-" + (item.name | regex_replace("\.conf$","")) + ".conf") }}'
file:
path: '/etc/security/{{ item.filename | d("access-" + (item.name | regex_replace("\.conf$","")) + ".conf") }}'
state: 'absent'
loop: '{{ pam_access__combined_rules | parse_kv_items }}'
loop_control:
label: '{{ {"access_conf": access_conf, "state": item.state|d("present")} }}'
when: pam_access__enabled|bool and item.name|d() and item.state|d('present') == 'absent' and
item.divert is undefined
- name: Revert PAM access control files
vars:
access_conf: '{{ "/etc/security/"
+ item.filename | d("access-" + (item.name | regex_replace("\.conf$","")) + ".conf") }}'
shell: set -o nounset -o pipefail -o errexit &&
rm -f {{ access_conf }} &&
dpkg-divert --quiet --local --rename --remove {{ access_conf }}
args:
executable: '/bin/bash'
loop: '{{ pam_access__combined_rules | parse_kv_items }}'
loop_control:
label: '{{ {"access_conf": access_conf, "state": item.state|d("present")} }}'
when: (pam_access__enabled|bool and item.name|d() and
item.state|d('present') == 'absent' and
((item.divert|d())|bool and
(access_conf + '.dpkg-divert') in pam_access__register_diversions.stdout_lines))
- name: Make sure that Ansible local facts directory exists
file:
path: '/etc/ansible/facts.d'
state: 'directory'
mode: '0755'
- name: Save pam_access local facts
template:
src: 'etc/ansible/facts.d/pam_access.fact.j2'
dest: '/etc/ansible/facts.d/pam_access.fact'
mode: '0755'
register: pam_access__register_facts
- name: Update Ansible facts if they were modified
action: setup
when: pam_access__register_facts is changed
#!{{ ansible_python['executable'] }}
# {{ ansible_managed }}
from __future__ import print_function
from json import dumps, loads
import os
output = loads('''{{ {"configured": True,
"enabled": pam_access__enabled|bool
} | to_nice_json }}''')
access_rules = []
for filename in os.listdir('/etc/security'):
if (filename.startswith('access-') and filename.endswith('.conf')):
access_rules.append(filename[7:-5])
output['rules'] = access_rules
print(dumps(output, sort_keys=True, indent=4))
# {{ ansible_managed }}
{% set fieldsep = item.fieldsep | d(':') %}
{% set listsep = item.listsep | d(' ') %}
{% for element in item.options %}
{% if element.state|d('present') not in [ 'absent', 'init', 'ignore' ] %}
{% if element.permission | lower in [ 'allow', '+', 'accept' ] %}
{% set element_permission = '+' %}
{% elif element.permission | lower in [ 'deny', '-', 'decline' ] %}
{% set element_permission = '-' %}
{% endif %}
{% set element_users = [] %}
{% if element.users_except|d() or element.groups_except|d() %}
{% set _ = element_users.append('ALL EXCEPT') %}
{% endif %}
{% if element.groups_except|d() %}
{% set _ = element_users.extend(([ element.groups_except ] if (element.groups_except is string) else element.groups_except) | map("regex_replace", "(.*)", "(\\1)") | list) %}
{% elif element.groups|d() %}
{% set _ = element_users.extend(([ element.groups ] if (element.groups is string) else element.groups) | map("regex_replace", "(.*)", "(\\1)") | list) %}
{% endif %}
{% if element.users_except|d() %}
{% set _ = element_users.extend([ element.users_except ] if (element.users_except is string) else element.users_except) %}
{% elif element.users|d() %}
{% set _ = element_users.extend([ element.users ] if (element.users is string) else element.users) %}
{% endif %}
{% if element.origins_except|d() %}
{% set element_origins = ([ 'ALL EXCEPT' ] + ([ element.origins_except ] if (element.origins_except is string) else element.origins_except)) %}
{% elif element.origins|d() %}
{% set element_origins = ([ element.origins ] if (element.origins is string) else element.origins) %}
{% endif %}
{% set element_comment = ('#' if (element.state|d('present') == 'comment') else '') %}
{% if element.separator|bool and not element.comment|d() and not loop.first %}
{% endif %}
{% if element.comment|d() %}
{% if not loop.first %}
{% endif %}
{{ element.comment | regex_replace('\n$','') | comment(prefix='', postfix='') -}}
{% endif %}
{{ '{}{}{}{}{}{}'.format(element_comment, element_permission, fieldsep, element_users | join(listsep), fieldsep, element_origins | join(listsep)) }}
{% endif %}
{% endfor %}
......@@ -252,6 +252,7 @@ Security
- :ref:`debops.fail2ban`
- :ref:`debops.ferm`
- :ref:`debops.freeradius`
- :ref:`debops.pam_access`
- :ref:`debops.proc_hidepid`
- :ref:`debops.sshd`
- :ref:`debops.sudo`
......@@ -280,6 +281,7 @@ System configuration
- :ref:`debops.nslcd`
- :ref:`debops.nsswitch`
- :ref:`debops.ntp`
- :ref:`debops.pam_access`
- :ref:`debops.resources`
- :ref:`debops.root_account`
- :ref:`debops.swapfile`
......
Default variable details
========================
Some of the ``debops.pam_access`` default variables have more extensive
configuration than simple strings or lists, here you can find documentation and
examples for them.
.. _pam_access__ref_rules:
pam_access__rules
-----------------
The ``pam_access__*_rules`` variables define the state of the
:file:`/etc/security/access*.conf` configuration files and their contents.
The variables are merged in order defined by the
:envvar:`pam_access__combined_rules` variable, which allows modification of the
default configuration through the Ansible inventory.
Examples
~~~~~~~~
Include some of the examples from the :man:`access.conf(5)` manual page in the
global :file:`/etc/security/access.conf` configuration file. Note that the
configuration is explicitly enabled because the role does not configure the
global access list by default:
.. code-block:: yaml
- name: 'global'
state: 'present'
options:
- name: 'allow-root-locally'
comment: |
User root should be allowed to get access via cron, X11 terminal :0, tty1-6
permission: 'allow'
users: 'root'
origins: [ 'crond', ':0', 'tty1', 'tty2', 'tty3', 'tty4', 'tty5', 'tty6' ]
- name: 'allow-root-loopback'
permission: '+'
users: 'root'
origins: '127.0.0.1'
- name: 'allow-root-subnet'
comment: |
User root should get access from network 192.0.2. which can also be
specified using a CIDR prefix, 192.0.2.0/24
permission: 'allow'
users: 'root'
origins: [ '192.0.2.', '192.0.2.0/24' ]
- name: 'allow-root-domain'
comment: |
User root should be able to have access from a specific domain
permission: '+'
users: 'root'
origins: '.example.org'
- name: 'deny-root'
comment: |
Deny access to the root account from any other sources
permission: 'deny'
users: 'root'
origins: 'ALL'
- name: 'allow-foo-admins'
comment: |
User 'foo' and members of netgroup 'admins' should be allowed to get
access from all sources. This will only work if netgroup service is
available.
permission: '+'
users: [ '@admins', 'foo' ]
origins: 'ALL'
- name: 'allow-john-ipv6subnet'
comment: |
User 'john' should get access from IPv6 net/mask.
permission: 'allow'
users: 'john'
origins: '2001:db8:0:101::/64'
- name: 'allow-local-wheel'
comment: |
Disallow console logins to all but the 'shutdown', 'sync' and all
other accounts, which are a member of the 'wheel' group.
permission: '-'
groups_except: 'wheel'
users_except: [ 'shutdown', 'sync' ]
origins: 'LOCAL'
- name: 'deny-all'
comment: |
All other users should be denied access from all sources. This rule
will be placed at the end of the configuration, to allow easy
addition of more rules before it.
permission: 'deny'
users: 'ALL'
origins: 'ALL'
weight: 99999
Add some of the examples from the default :file:`/etc/security/access.conf`
file installed by Debian to the :file:`/etc/security/access-sshd.conf`
configuration file used by the ``sshd`` service.
Note that the configuration has state ``append`` which means that even though
the values are defined in the Ansible inventory, they will only be applied when
the :ref:`debops.pam_access` role is used in the context of the
:ref:`debops.sshd` role, via the ``sshd.yml`` playbook (the configuration entry
was defined elsewhere and inventory entry is appended to it). Otherwise the
custom access file used by the ``sshd`` service would be overwritten during
normal usage of the :ref:`debops.pam_access` role.
The examples are nonsensical in the context of the OpenSSH service, but are
provided here to show how to implement specific ACL rules.
.. code-block:: yaml
pam_access__rules:
- name: 'sshd'
state: 'append'
options:
- name: 'deny-non-root'
comment: 'Disallow non-root logins on tty1'
permission: 'deny'
users_except: 'root'
origins: 'tty1'
- name: 'deny-non-privileged'
comment: 'Disallow non-local logins to privileged accounts'
permission: '-'
groups: 'wheel'
origins_except: [ 'LOCAL', '.sub.example.org' ]
Syntax
~~~~~~
The variables contain a list of YAML dictionaries, each dictionary can have
specific parameters:
``name``
Required. Name of an access control configuration file managed by the
:ref:`debops.pam_access` role. The role will create the file in:
.. code-block:: none
/etc/security/access-<name>.conf
Configuration entries with the same ``name`` parameter will be merged
together in order of appearance; this can be used to modify existing entries
via the Ansible inventory.
``filename``
Optional. Override the autogenerated file name. You should only specify the
filename itself, files are stored in the :file:`/etc/security/` directory.
``state``
Optional. If not specified or ``present``, the configuration file will be
generated. If ``absent``, the specified configuration file will be removed.
If ``init``, the configuration entry will be initialized, but not active
- this can be used to prepare an entry and activate it conditionally later.
If ``ignore``, a given configuration entry will not be evaluated by the role.
If ``append``, the configuration entry will be processed only if a given
entry was defined earlier. This should be a preferred method to modify access
rules defined by other Ansible roles through the Ansible inventory, otherwise
the user roles will override the role rules.
``divert``
Optional, boolean. If ``True``, the role will automatically divert or revert
the original access control rule file depending on its state, to preserve it
for APT upgrades. This parameter shouldn't be changed if a diverted file is
present, otherwise the role will not track the diversion.
``fieldsep``
Optional. Specify the character that will be used as the field separator in
the generated rule files. If not specified, colon (``:``) is used by default.
See :man:`pam_access(8)` for information about the usage of this parameter.
``listsep``
Optional. Specify the character that will be used as the list element
separator in the generated rule files. If not specified, space is used by
default. See :man:`pam_access(8)` for information about the usage of this
parameter.
``options``
Required. List of YAML dictionaries which describe PAM access rules. The
lists in the entries with the same ``name`` parameter are merged together,
with the rules that use the same ``name`` affecting each other in order of
appearance. Rules can be defined using specific parameters:
``name``
Required. Name of a given access rule, not used directly. Entries with the
same ``name`` parameter will be merged together in order of appearance;
this allows modification of existing entries via Ansible inventory.
``permission``
Required. Specify the permission of a given access rule. Possible values:
- ``allow`` / ``+`` / ``accept``
- ``deny`` / ``-`` / ``decline``
``users``
String or YAML list of usernames, netgroups or ``ALL`` that matches
everyone. If ``users_except`` parameter is specified, this parameter is
ignored.
``users_except``
String or YAML list of usernames, netgroups or ``ALL`` that matches
everyone. If this parameter is specified, the list of users or groups will
be prefixed with ``ALL EXCEPT`` which allows for negation.
``groups``
String or YAML list of UNIX group names, which will be automatically
wrapped in parentheses (``( )``) to mark them as groups. If
``groups_except`` parameter is specified, this parameter is ignored.
``groups_except``
String or YAML list of UNIX group names, which will be automatically
wrapped in parentheses (``( )``) to mark them as groups. If this parameter
is specified, the list of users or groups will be prefixed with ``ALL
EXCEPT`` which allows for negation.
``origins``
String or YAML list of "origins" - TTY names, hostnames, domain names
(specified with the ``.`` prefix), IP addresses, network addresses
(specified with the ``.`` suffix or with CIDR netmask), netgroup names,
``ALL`` which matches everything, or ``LOCAL`` which matches only local
TTYs and services. If ``origins_except`` parameter is specified, this
parameter is ignored.
``origins_except``
String or YAML list of "origins" - TTY names, hostnames, domain names
(specified with the ``.`` prefix), IP addresses, network addresses
(specified with the ``.`` suffix or with CIDR netmask), netgroup names,
``ALL`` which matches everything, or ``LOCAL`` which matches only local
TTYs and services. If this parameter is specified, the list of origins will
be prefixed with ``ALL EXCEPT`` which allows for negation.
``comment``
Optional. String or YAML text block that contains comments about a given
access rule.
``state``
Optional. If not specified or ``present``, a given access rule will be
included in the generated rule file. If ``absent``, the rule will be
removed from the generated rule file.
``weight``
Optional. Positive or negative number, which can be used to affect the
position of the rule within the rule file. Positive numbers will force the
rule to be lower than normal (adding weight), negative numbers will move
the role higher on the list (substracting weight).
Getting started
===============
.. contents::
:local:
Default configuration
---------------------
By default, the role does not configure any access rules in the
:file:`/etc/security/access.conf` file. Control over this file is initialized
using a configuration entry named ``global``. See the examples in the
:ref:`pam_access__ref_rules` for an explanation how to use it in the Ansible
inventory to set the access rules.
Role is designed to be used by other Ansible roles to manage their own access
lists, with a custom file per service. However, the rules defined via dependent
variables are not tracked outside of the context of a given role (ie. in
different playbooks), and roles cannot affect each other's access rules using
this method. Similarly, in Ansible inventory users should set the state of the
defined rules as ``append``, so that they don't clobber the existing rule files
when the :ref:`debops.pam_access` role is executed on its own, or via
a different playbook.
The activation of the ``pam_access.so`` PAM module for each service is not
managed by the :ref:`debops.pam_access` role itself, and should be managed by
the Ansible roles designed to configure the services.
Example inventory
-----------------
The :ref:`debops.pam_access` role is included in the DebOps common playbook,
therefore you don't need to do anything special to enable it on a host.
Example playbook
----------------
If you are using this role without DebOps, here's an example Ansible playbook
that uses the ``debops.pam_access`` role:
.. literalinclude:: ../../../../ansible/playbooks/service/pam_access.yml
:language: yaml
Ansible tags
------------
You can use Ansible ``--tags`` or ``--skip-tags`` parameters to limit what
tasks are performed during Ansible run. This can be used after host is first
configured to speed up playbook execution, when you are sure that most of the
configuration has not been changed.
Available role tags:
``role::pam_access``
Main role tag, should be used in the playbook to execute all of the role
tasks as well as role dependencies.
Other resources
---------------
List of other useful resources related to the ``debops.pam_access`` Ansible
role:
- Manual pages: :man:`pam_access(8)`, :man:`access.conf(5)`
.. _debops.pam_access:
debops.pam_access
=================
The `Linux Pluggable Authentication Modules`__ provide dynamic authentication
support to services on Linux hosts. The ``debops.pam_access`` role can be used
to manage one aspect of PAM - access control rules that can be used to grant or
revoke access to services based on users, groups and origins.
.. __: https://en.wikipedia.org/wiki/Linux_PAM
.. toctree::
:maxdepth: 2
getting-started
defaults
defaults-detailed
Copyright
---------
.. literalinclude:: ../../../../ansible/roles/debops.pam_access/COPYRIGHT
..
Local Variables:
mode: rst
ispell-local-dictionary: "american"
End:
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment