From e3fd11fefb4999787dc0f7ab8223b6a502f4adc0 Mon Sep 17 00:00:00 2001 From: Andrew J Huffman Date: Fri, 28 Jun 2019 17:28:23 -0400 Subject: [PATCH] version 2.0.0 --- .ansible-lint | 2 + .docker/debian/stretch/Dockerfile | 68 --- .docker/fedora/latest/Dockerfile | 71 --- .docker/ubuntu/xenial/Dockerfile | 68 --- .gitignore | 22 - .kitchen.yml | 75 --- .travis.yml | 41 -- Gemfile | 7 - README.md | 509 ++++++++++++------ defaults/main.yml | 118 ++-- filter_plugins/to_list.py | 11 - meta/main.yml | 84 ++- tasks/main.yml | 163 +++--- templates/sudoer_spec.j2 | 9 - templates/sudoers.j2 | 110 ++++ templates/sudoers_nospec.j2 | 70 --- templates/sudoers_plus_spec.j2 | 80 --- test/ansible-setup.sh | 30 -- test/integration/default/default.yml | 59 -- .../default/serverspec/default_spec.rb | 31 -- .../default/serverspec/spec_helper.rb | 11 - 21 files changed, 672 insertions(+), 967 deletions(-) create mode 100644 .ansible-lint delete mode 100644 .docker/debian/stretch/Dockerfile delete mode 100644 .docker/fedora/latest/Dockerfile delete mode 100644 .docker/ubuntu/xenial/Dockerfile delete mode 100644 .gitignore delete mode 100644 .kitchen.yml delete mode 100644 .travis.yml delete mode 100644 Gemfile delete mode 100644 filter_plugins/to_list.py delete mode 100644 templates/sudoer_spec.j2 create mode 100644 templates/sudoers.j2 delete mode 100644 templates/sudoers_nospec.j2 delete mode 100644 templates/sudoers_plus_spec.j2 delete mode 100644 test/ansible-setup.sh delete mode 100644 test/integration/default/default.yml delete mode 100644 test/integration/default/serverspec/default_spec.rb delete mode 100644 test/integration/default/serverspec/spec_helper.rb diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..c29b3ed --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,2 @@ +skip_list: + - "602" #python truthiness does not properly work in ansible diff --git a/.docker/debian/stretch/Dockerfile b/.docker/debian/stretch/Dockerfile deleted file mode 100644 index e2def17..0000000 --- a/.docker/debian/stretch/Dockerfile +++ /dev/null @@ -1,68 +0,0 @@ -FROM debian:stretch - -ENV DEBIAN_FRONTEND="noninteractive" container="docker" - -RUN apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y \ - apt-utils \ - curl \ - locales \ - lsb-release \ - net-tools \ - openssh-server \ - python-pip \ - python2.7 \ - sudo \ - systemd \ - && pip install --upgrade pip \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && login_shell=$(command -v bash) \ - && if ! getent passwd <%= @username %>; then \ - useradd -u 9000 -d /home/<%= @username %> -m -s "${login_shell}" -p '*' <%= @username %>; \ - fi \ - && echo "<%= @username %> ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/kitchen \ - && chmod 440 /etc/sudoers.d/kitchen \ - && echo "Defaults !requiretty" >> /etc/sudoers \ - && mkdir -p /home/<%= @username %>/.ssh \ - && chown -R <%= @username %> /home/<%= @username %>/.ssh \ - && chmod 0700 /home/<%= @username %>/.ssh \ - && echo '<%= IO.read(@public_key).strip %>' >> /home/<%= @username %>/.ssh/authorized_keys \ - && chown <%= @username %> /home/<%= @username %>/.ssh/authorized_keys \ - && chmod 0600 /home/<%= @username %>/.ssh/authorized_keys \ - && echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen \ - && cd /lib/systemd/system/sysinit.target.wants/; ls | grep -v systemd-tmpfiles-setup | xargs rm -f $1 \ - && rm -f /lib/systemd/system/multi-user.target.wants/* \ - && rm -f /etc/systemd/system/*.wants/* \ - && rm -f /lib/systemd/system/local-fs.target.wants/* \ - && rm -f /lib/systemd/system/sockets.target.wants/*udev* \ - && rm -f /lib/systemd/system/sockets.target.wants/*initctl* \ - && rm -f /lib/systemd/system/basic.target.wants/* \ - && rm -f /lib/systemd/system/anaconda.target.wants/* \ - && rm -f /lib/systemd/system/plymouth* \ - && rm -f /lib/systemd/system/systemd-update-utmp* \ - && sed -ri 's/^#?UsePAM\s+.*/UsePAM no/' /etc/ssh/sshd_config \ - && sed -ri 's/^#?PubkeyAuthentication\s+.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config \ - && sed -ri 's/^#?UsePrivilegeSeparation\s+.*/UsePrivilegeSeparation no/' /etc/ssh/sshd_config \ - && echo "UseDNS=no" >> /etc/ssh/sshd_config \ - && systemctl set-default multi-user.target \ - && ln -s /lib/systemd/system/systemd-journald.service /etc/systemd/system/multi-user.target.wants/systemd-journald.service \ - && ln -s /lib/systemd/system/sshd.service /etc/systemd/system/multi-user.target.wants/sshd.service \ - && echo $'[Unit]\ -\nDescription=Finish boot up\ -\nAfter=ssh.service\ -\n\ -\n[Service]\ -\nType=oneshot\ -\nRemainAfterExit=yes\ -\nExecStartPre=/bin/sleep 3s\ -\nExecStart=/bin/rm -f /run/nologin\ -\n\ -\n[Install]\ -\nWantedBy=default.target' >> /etc/systemd/system/FinishBootUp.service \ - && ln -s /etc/systemd/system/FinishBootUp.service /etc/systemd/system/multi-user.target.wants/FinishBootUp.service - -EXPOSE 22 - -VOLUME [ "/sys/fs/cgroup" ] diff --git a/.docker/fedora/latest/Dockerfile b/.docker/fedora/latest/Dockerfile deleted file mode 100644 index eadd2d1..0000000 --- a/.docker/fedora/latest/Dockerfile +++ /dev/null @@ -1,71 +0,0 @@ -FROM fedora:latest - -ENV container="docker" - -RUN dnf clean all \ - && dnf makecache \ - && dnf install -y \ - curl \ - findutils \ - gcc \ - glibc-langpack-en.x86_64 \ - libffi-devel \ - net-tools \ - openssh-server \ - openssl-devel \ - python2-devel \ - python2-pip \ - redhat-lsb \ - redhat-rpm-config \ - sudo \ - systemd \ - && pip install --upgrade pip \ - && dnf clean all \ - && login_shell=$(command -v bash) \ - && if ! getent passwd <%= @username %>; then \ - useradd -u 9000 -d /home/<%= @username %> -m -s "${login_shell}" -p '*' <%= @username %>; \ - fi \ - && echo "<%= @username %> ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/kitchen \ - && chmod 440 /etc/sudoers.d/kitchen \ - && echo "Defaults !requiretty" >> /etc/sudoers \ - && mkdir -p /home/<%= @username %>/.ssh \ - && chown -R <%= @username %> /home/<%= @username %>/.ssh \ - && chmod 0700 /home/<%= @username %>/.ssh \ - && echo '<%= IO.read(@public_key).strip %>' >> /home/<%= @username %>/.ssh/authorized_keys \ - && chown <%= @username %> /home/<%= @username %>/.ssh/authorized_keys \ - && chmod 0600 /home/<%= @username %>/.ssh/authorized_keys \ - && export LANG="en_US.UTF-8" && echo "LANG=\"en_US.UTF-8\"" > /etc/locale.conf \ - && cd /lib/systemd/system/sysinit.target.wants/; ls | grep -v systemd-tmpfiles-setup | xargs rm -f $1 \ - && rm -f /lib/systemd/system/multi-user.target.wants/* \ - && rm -f /etc/systemd/system/*.wants/* \ - && rm -f /lib/systemd/system/local-fs.target.wants/* \ - && rm -f /lib/systemd/system/sockets.target.wants/*udev* \ - && rm -f /lib/systemd/system/sockets.target.wants/*initctl* \ - && rm -f /lib/systemd/system/basic.target.wants/* \ - && rm -f /lib/systemd/system/anaconda.target.wants/* \ - && rm -f /lib/systemd/system/plymouth* \ - && rm -f /lib/systemd/system/systemd-update-utmp* \ - && sed -ri 's/^#?PubkeyAuthentication\s+.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config \ - && sed -ri 's/^#?UsePrivilegeSeparation\s+.*/UsePrivilegeSeparation no/' /etc/ssh/sshd_config \ - && echo "UseDNS=no" >> /etc/ssh/sshd_config \ - && systemctl set-default multi-user.target \ - && ln -s /lib/systemd/system/systemd-journald.service /etc/systemd/system/multi-user.target.wants/systemd-journald.service \ - && ln -s /lib/systemd/system/sshd.service /etc/systemd/system/multi-user.target.wants/sshd.service \ - && echo $'[Unit]\ -\nDescription=Finish boot up\ -\nAfter=ssh.service\ -\n\ -\n[Service]\ -\nType=oneshot\ -\nRemainAfterExit=yes\ -\nExecStartPre=/bin/sleep 3s\ -\nExecStart=/bin/rm -f /run/nologin\ -\n\ -\n[Install]\ -\nWantedBy=default.target' >> /etc/systemd/system/FinishBootUp.service \ - && ln -s /etc/systemd/system/FinishBootUp.service /etc/systemd/system/multi-user.target.wants/FinishBootUp.service - - -EXPOSE 22 - -VOLUME [ "/sys/fs/cgroup" ] diff --git a/.docker/ubuntu/xenial/Dockerfile b/.docker/ubuntu/xenial/Dockerfile deleted file mode 100644 index 61c057b..0000000 --- a/.docker/ubuntu/xenial/Dockerfile +++ /dev/null @@ -1,68 +0,0 @@ -FROM ubuntu:xenial - -ENV DEBIAN_FRONTEND="noninteractive" container="docker" - -RUN apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y \ - apt-utils \ - curl \ - locales \ - lsb-release \ - net-tools \ - openssh-server \ - python-pip \ - python2.7 \ - sudo \ - systemd \ - && pip install --upgrade pip \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && login_shell=$(command -v bash) \ - && if ! getent passwd <%= @username %>; then \ - useradd -u 9000 -d /home/<%= @username %> -m -s "${login_shell}" -p '*' <%= @username %>; \ - fi \ - && echo "<%= @username %> ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/kitchen \ - && chmod 440 /etc/sudoers.d/kitchen \ - && echo "Defaults !requiretty" >> /etc/sudoers \ - && mkdir -p /home/<%= @username %>/.ssh \ - && chown -R <%= @username %> /home/<%= @username %>/.ssh \ - && chmod 0700 /home/<%= @username %>/.ssh \ - && echo '<%= IO.read(@public_key).strip %>' >> /home/<%= @username %>/.ssh/authorized_keys \ - && chown <%= @username %> /home/<%= @username %>/.ssh/authorized_keys \ - && chmod 0600 /home/<%= @username %>/.ssh/authorized_keys \ - && echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen \ - && cd /lib/systemd/system/sysinit.target.wants/; ls | grep -v systemd-tmpfiles-setup | xargs rm -f $1 \ - && rm -f /lib/systemd/system/multi-user.target.wants/* \ - && rm -f /etc/systemd/system/*.wants/* \ - && rm -f /lib/systemd/system/local-fs.target.wants/* \ - && rm -f /lib/systemd/system/sockets.target.wants/*udev* \ - && rm -f /lib/systemd/system/sockets.target.wants/*initctl* \ - && rm -f /lib/systemd/system/basic.target.wants/* \ - && rm -f /lib/systemd/system/anaconda.target.wants/* \ - && rm -f /lib/systemd/system/plymouth* \ - && rm -f /lib/systemd/system/systemd-update-utmp* \ - && sed -ri 's/^#?UsePAM\s+.*/UsePAM no/' /etc/ssh/sshd_config \ - && sed -ri 's/^#?PubkeyAuthentication\s+.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config \ - && sed -ri 's/^#?UsePrivilegeSeparation\s+.*/UsePrivilegeSeparation no/' /etc/ssh/sshd_config \ - && echo "UseDNS=no" >> /etc/ssh/sshd_config \ - && systemctl set-default multi-user.target \ - && ln -s /lib/systemd/system/systemd-journald.service /etc/systemd/system/multi-user.target.wants/systemd-journald.service \ - && ln -s /lib/systemd/system/sshd.service /etc/systemd/system/multi-user.target.wants/sshd.service \ - && echo $'[Unit]\ -\nDescription=Finish boot up\ -\nAfter=ssh.service\ -\n\ -\n[Service]\ -\nType=oneshot\ -\nRemainAfterExit=yes\ -\nExecStartPre=/bin/sleep 3s\ -\nExecStart=/bin/rm -f /run/nologin\ -\n\ -\n[Install]\ -\nWantedBy=default.target' >> /etc/systemd/system/FinishBootUp.service \ - && ln -s /etc/systemd/system/FinishBootUp.service /etc/systemd/system/multi-user.target.wants/FinishBootUp.service - -EXPOSE 22 - -VOLUME [ "/sys/fs/cgroup" ] diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a0c236d..0000000 --- a/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -*.pyc - -# IDE specific -.idea/ - -# Bundler -.bundle -vendor -Gemfile.lock - -# Ruby -.ruby-version - -# Test-kitchen -.kitchen/ -.kitchen.local.yml - -# Molecule -.molecule/ - -# Ansible roles -.imported_roles/ diff --git a/.kitchen.yml b/.kitchen.yml deleted file mode 100644 index 5ebe38d..0000000 --- a/.kitchen.yml +++ /dev/null @@ -1,75 +0,0 @@ ---- -driver: - name: docker - use_sudo: false - -provisioner: - # name of the host - hosts: test-kitchen - # use an ansible playbook to provision our server - name: ansible_playbook - ansible_verbose: false - require_ansible_repo: false - require_ansible_omnibus: true - ansible_version: 2.3.1 - require_chef_for_busser: false - sudo_command: sudo -E -H - idempotency_test: true - sudo: true - -transport: - max_ssh_sessions: 3 - -platforms: - - name: ubuntu-xenial - driver_config: - run_command: /sbin/init - dockerfile: .docker/ubuntu/xenial/Dockerfile - platform: ubuntu - instance_name: test-role-ansible-sudoers-xenial - cap_add: - - SYS_ADMIN - volume: - - /sys/fs/cgroup:/sys/fs/cgroup:ro - run_options: - tmpfs: - - /run - - name: debian-stretch - driver_config: - run_command: /lib/systemd/systemd - dockerfile: .docker/debian/stretch/Dockerfile - platform: debian - instance_name: test-role-ansible-sudoers-stretch - cap_add: - - SYS_ADMIN - volume: - - /sys/fs/cgroup:/sys/fs/cgroup:ro - run_options: - tmpfs: - - /run - - name: fedora-latest - driver_config: - run_command: /lib/systemd/systemd - dockerfile: .docker/fedora/latest/Dockerfile - platform: fedora - instance_name: test-role-ansible-sudoers-fedora-latest - cap_add: - - SYS_ADMIN - volume: - - /sys/fs/cgroup:/sys/fs/cgroup:ro - run_options: - tmpfs: - - /run - -verifier: - name: serverspec - sudo_path: true - -suites: - # suites found at /test/integration/$test-name - - name: default - verifier: - patterns: - - roles/ansible-sudoers/test/integration/default/serverspec/*_spec.rb - bundler_path: '/usr/local/bin' - rspec_path: '/usr/local/bin' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7b5faa6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -language: ruby -rvm: - - 2.4 - -sudo: required -services: docker - -env: - global: - - ANSIBLE_VERSION=v2.3 - matrix: -# Split up the test-kitchen run to avoid exceeding 50 minutes: - - KITCHEN_REGEXP=fedora - - KITCHEN_REGEXP=debian - - KITCHEN_REGEXP=ubuntu - -before_install: - # Make sure everything's up to date. - - sudo apt-get update -qq - - sudo apt-get install -qq python-apt python-pycurl git python-pip ruby ruby-dev build-essential autoconf - - gem install bundler - -install: - - bash test/ansible-setup.sh - - bundle install - - ~/.avm/v1.9/venv/bin/pip install netaddr ansible-lint - - ~/.avm/v2.2/venv/bin/pip install netaddr ansible-lint - - ~/.avm/v2.3/venv/bin/pip install netaddr ansible-lint - -script: - - ansible --version - - ruby --version - - python --version - - pip --version - - bundler --version - - bundle show - - ~/.avm/$ANSIBLE_VERSION/venv/bin/ansible-lint -v ./ - - bundle exec kitchen test $KITCHEN_REGEXP - -notifications: - webhooks: https://galaxy.ansible.com/api/v1/notifications/ diff --git a/Gemfile b/Gemfile deleted file mode 100644 index fef6d20..0000000 --- a/Gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' - -gem 'test-kitchen' -gem 'serverspec' -gem 'kitchen-ansible' -gem 'kitchen-docker' -gem 'kitchen-verifier-serverspec' diff --git a/README.md b/README.md index 006d977..1b7b961 100644 --- a/README.md +++ b/README.md @@ -1,209 +1,368 @@ -# ahuffman.sudoers (formerly wtcross.sudoers) -An Ansible role for configuring the `/etc/sudoers` file and `/etc/sudoers.d` files. +# ahuffman.sudoers -This role makes it possible to completely define your sudoers configuration with Ansible. All of the following are configurable: -- defaults -- aliases - * Users - * Runas - * Hosts - * Commands -- specifications +Controls the configuration of the default `/etc/sudoers` file and included files/directories. -*Tip:* Here's a [great document about sudoers configuration](https://help.ubuntu.com/community/Sudoers) +--- +***Please note, release 2.0.0+ is a major re-write of the role. Please read the documentation to ensure you understand the changes prior to installation and use if coming from prior versions.*** -## Role Variables +--- + +## Table of Contents + + +1. [Table of Contents](#table-of-contents) +2. [Tips](#tips) +3. [Role Variables](#role-variables) +4. [sudoers_files Dictionary Fields](#sudoers_files-dictionary-fields) + 1. [sudoers_files.aliases Dictionary Fields](#sudoers_filesaliases-dictionary-fields) + 1. [cmnd_alias Dictionary Fields](#cmnd_alias-dictionary-fields) + 2. [host_alias Dictionary Fields](#host_alias-dictionary-fields) + 3. [runas_alias Dictionary Fields](#runas_alias-dictionary-fields) + 4. [user_alias Dictionary Fields](#user_alias-dictionary-fields) + 2. [user_specifications Dictionary Fields](#user_specifications-dictionary-fields) + 1. [Standard user_specifications](#standard-user_specifications) + 2. [Default Override user_specifications](#default-override-user_specifications) +5. [Automatically Generating the Sudoers Files Data from an Existing Configuration](#automatically-generating-the-sudoers-files-data-from-an-existing-configuration) +6. [Example Playbooks](#example-playbooks) + 1. [RHEL7.6 Default Sudoers Configuration](#rhel76-default-sudoers-configuration) + 1. [Results: /etc/sudoers](#results-etcsudoers) + 2. [Sudoers Configuration (multiple files)](#sudoers-configuration-multiple-files) + 1. [Results: /etc/sudoers](#results-etcsudoers) + 2. [Results: /etc/sudoers.d/pingers](#results-etcsudoersdpingers) + 3. [Results: /etc/sudoers.d/root](#results-etcsudoersdroot) + 3. [Migrating a Running Sudoers Configuration to Another Host](#migrating-a-running-sudoers-configuration-to-another-host) +11. [License](#license) +12. [Author Information](#author-information) + + -By default this role configures and manages all sudo specs. These are various -configurations. +## Tips +|*Tip:* Here's a few excellent resources on sudoers configuration:| +|---| +|[Start here](https://help.ubuntu.com/community/Sudoers) - Provides a great run-down on basic sudoers file configurations and terminology| +|[Sudoers Manual](https://www.sudo.ws/man/1.8.15/sudoers.man.html) - If you want to know all the details, this is for you.| + +## Role Variables +The defaults defined for this role are based on a default RHEL7.6 `/etc/sudoers` configuration. Please check the defaults in [`defaults/main.yml`](defaults/main.yml) prior to running for OS compatibility. | Variable Name | Description | Default Value | Variable Type | | --- | --- | :---: | :---: | -| sudoer_rewrite_sudoers_file | Use role default or user defined `default_specs` replacing distro supplied `/etc/sudoers` file. | True | boolean | -| sudoer_remove_unauthorized_specs | Each existing sudoer spec on the filesystem not generated by this role's values will be removed. ***Very Dangerous***. | False | boolean | -| sudoer_separate_specs | Each sudoer spec will be placed in a separate file within the `/etc/sudoers.d/` directory. | True | boolean | -| sudoer_separate_specs_cleanup | Remove any remaining files in `/etc/sudoers.d` if `sudoer_separate_specs` is set to `False`. If this value is set to `False`, the existing files from a previous configuration will be untouched. Set to `True` if you want this role's configuration to be your source of truth and remove old files. | False | boolean | -| sudoer_backup | Whether or not to create a backup of a changed /etc/sudoers file (does not pertain to files to be removed or individual spec files). Backup of individual spec files could create problematic configurations, as they will exist as a separate spec. in the /etc/sudoers.d directory.| True | boolean | +| sudoers_rewrite_default_sudoers_file | Use role default or user defined `sudoers_files` definition, replacing your distribution supplied `/etc/sudoers` file. Useful when attempting to deploy new configuration files to the `include_directories` and you do not wish to modify the `/etc/sudoers` file. | True | boolean | +| sudoers_remove_unauthorized_included_files | ***Very Dangerous!*** Each existing sudoer file found in the `include_directories` dictionary which have not been defined in `sudoers_files` will be removed. This allows for enforcing a desired state. | False | boolean | +| sudoers_backup | Whether or not to create a backup of the current state of the existing `/etc/sudoers` file as well as any files defined in `sudoers_files`. The files get backed up to the Ansible control node (Server you are executing Ansible from) and avoids accidentally leaving files behind in your `include_directories` that may be evaluated by the sudoers configuration(s).| True | boolean | +| sudoers_backup_path | Path relative to where you are executing your playbook to backup the remote copies of defined `sudoers_files` to. | "sudoers_backups" | string | +| sudoers_visudo_path | Fully-qualified path to the `visudo` binary required for validation of sudoers configuration changes. Added for Operating System compatibility. | "/usr/sbin/visudo" | string | +| sudoers_files | Definition of all your sudoers configurations | see [defaults/main.yml](defaults/main.yml)| list of dictionaries | + +## sudoers_files Dictionary Fields +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| path | Where to deploy the configuration file to on the filesystem. | string | +| aliases | Optional definition of `cmnd_alias`, `host_alias`, `runas_alias`, or `user_alias` items. | dictionary | +| defaults | This allows you to define the defaults of your sudoers configuration. Default overrides can be perfomed via the [`user_specifications`](#default-override-user_specifications) key.| list | +| include_files | Optional specific files that you would like your configuration to include. This is a list of fully-qualified paths to include via the `#include` option of a sudoers configuration. | list | +| include_directories | Optional specific directories that you would like your configurations to include. This is a list of fully-qualified paths to directories to include via the `#includedir` option of a sudoers configuration. | list | +| user_specifications | List of user specifications and default overrides to apply to a sudoers file configuration. | list | + +### sudoers_files.aliases Dictionary Fields +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| cmnd_alias | List of command alias definitions. | list of dictionaries | +| host_alias | List of host alias definitions | list of dictionaries | +| runas_alias | List of runas alias definitions | list of dictionaries | +| user_alias | List of user alias definitions | list of dictionaries | + +#### cmnd_alias Dictionary Fields +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| name | Name of the command alias. | string | +| commands | List of commands to apply to the alias | list | -## Role Default Variables +#### host_alias Dictionary Fields +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| name | Name of the host alias. | string | +| hosts | List of hosts to apply to the alias | list | + +#### runas_alias Dictionary Fields +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| name | Name of the runas alias | string | +| users | List of users to apply to the alias | list | + +#### user_alias Dictionary Fields +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| name | Name of the user_alias | string | +| users | List of users to apply to the alias | list | + +### user_specifications Dictionary Fields +This dictionary can be used to assign either user specifications or default overrides. + +#### Standard user_specifications +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| users | List of users to apply the specification to. You can use a `user_alias` name as well as user names. | list | +| hosts | List of hosts to apply the specification to. You can use a defined `host_alias` name as well as host names. | list | +| operators | List of operators to apply the specification to. You can use a defined `runas_alias` name as well as user names. | list | +| selinux_role | Optional selinux role to apply to the specification | list | +| selinux_type | Optional selinux type to apply to the specification | list | +| solaris_privs | Optional Solaris privset to apply to the specification | list | +| solaris_limitprivs | Optional Solaris privset to apply to the specification | list | +| tags | Optional list of tags to apply to the specification. | list | +| commands | List of commands to apply the specification to. You can use a defined `cmnd_alias` name as well as commands. | list | + +#### Default Override user_specifications +| Variable Name | Description | Variable Type | +| --- | --- | :---: | +| defaults | List of defaults to override from the main configuration | list | +| type | Type of default to override, this affects the operator in the configuration ( host -> `@`, user -> `:`, command -> `!`, and runas -> `>`). The type field can be one of the following values: `command`, `host`, `runas`, or `user`. | string | +| commands | Use when `type: "command"`. List of `cmnd_alias` names as well as commands to override specific default values.| list | +| hosts | Use when `type: "host"`. List of `host_alias` names as well as individual host names to override specific default values. | list | +| operators | Use when `type: "runas"`. List of `runas_alias` names as well as individual user names to override specific default values. | list | +| users | Use when `type: "user"`. List of `user_alias` names as well as individual user names to override specific default values. | list | + +## Automatically Generating the Sudoers Files Data from an Existing Configuration +Does this all sound way too complicated to configure from the documentation? Please check out and try [ahuffman.scan_sudoers](https://galaxy.ansible.com/ahuffman/scan_sudoers) to find a role that can auto-generate the proper data structure for you. With the [ahuffman.scan_sudoers](https://galaxy.ansible.com/ahuffman/scan_sudoers) role, you can take a running configuration in one play, and lay it down on another with the [ahuffman.sudoers](https://galaxy.ansible.com/ahuffman/sudoers) role (version 2.0.0+). You could also opt to take the collected data and push it into a source of truth such as a CMDB or repository via automation. The collected data that is generated by [ahuffman.scan_sudoers](https://galaxy.ansible.com/ahuffman/scan_sudoers) and can be consumed by [ahuffman.sudoers](https://galaxy.ansible.com/ahuffman/sudoers) would be `{{ ansible_facts['sudoers'].sudoers_files }}`. + +This should help alleviate some of the complication of manually defining the sudoers configurations as code, and get you up and running much quicker. + +See the [Playbook Example](#migrating-a-running-sudoers-configuration-to-another-host) below. + + +## Example Playbooks +### RHEL7.6 Default Sudoers Configuration +```yaml +- name: "Apply a RHEL7.6 Default /etc/sudoers configuration" + hosts: "all" + roles: + - role: "ahuffman.sudoers" +``` +...or with modern syntax: ```yaml -sudoer_aliases: {} -sudoer_specs: [] -sudoer_defaults: - # - requiretty (disabled, just uncomment if required) - - "!visiblepw" - - always_set_home - - env_reset - - env_keep: - - COLORS - - DISPLAY - - HOSTNAME - - HISTSIZE - - INPUTRC - - KDEDIR - - LS_COLORS - - MAIL - - PS1 - - PS2 - - QTDIR - - USERNAME - - LANG - - LC_ADDRESS - - LC_CTYPE - - LC_COLLATE - - LC_IDENTIFICATION - - LC_MEASUREMENT - - LC_MESSAGES - - LC_MONETARY - - LC_NAME - - LC_NUMERIC - - LC_PAPER - - LC_TELEPHONE - - LC_TIME - - LC_ALL - - LANGUAGE - - LINGUAS - - _XKB_CHARSET - - XAUTHORITY - - secure_path: /sbin:/bin:/usr/sbin:/usr/bin -sudoer_separate_specs: True -sudoer_rewrite_sudoers_file: True -sudoer_remove_unauthorized_specs: False -sudoer_separate_specs_cleanup: False -sudoer_backup: True +- name: "Apply a RHEL7.6 Default /etc/sudoers configuration" + hosts: "all" + tasks: + - name: "Configure /etc/sudoers" + include_role: + name: "ahuffman.sudoers" ``` -## Variable Schemas -```yaml -# host alias -name: string -hosts: string|[hostnames] -comment: string #procedes the alias with a comment - -# user alias -name: string -users: string|[username|%group] -comment: string #procedes the alias with a comment - -# runas alias -name: string -users: string|[username|%group|#uid] -comment: string #procedes the alias with a comment - -# cmnd alias -name: string -commands: string|[string] -comment: string #procedes the alias with a comment - -# sudoer specification -name: string -users: string|[string] -hosts: string|[string] -operators: string|[string] -tags: string|[string] -comment: string #procedes the alias with a comment -defaults: string|[string] - -## Other Variables -- sudoer_aliases: a dictionary that specifies which aliases to configure - - sudoer_aliases.host: a list of host alias descriptions - - sudoer_aliases.user: a list of user or group alias descriptions - - sudoer_aliases.runas: a list of runas alias descriptions - - sudoer_aliases.command: a list of command alias descriptions -- sudoer_specs: a list of sudoer specifications -- sudoer_defaults: a list of default settings - - can be any of the following types - - string - - string: string - - string: [string] +#### Results: /etc/sudoers +The above two examples using the role defaults will produce a `/etc/sudoers` configuration file that looks like this: ``` +# Ansible managed -## About and Usage -The top level `/etc/sudoers` file can be kept as light as possible by specifying `sudoer_separate_specs: True` in either the role's `defaults/main.yml` or your playbook's variables. Please be aware that `sudoer_separate_specs` is set to `True` by default, and therefore your changes will be expected in `/etc/sudoers.d` unless set to `False`. +# Default specifications +Defaults !visiblepw +Defaults always_set_home +Defaults match_group_by_gid +Defaults always_query_group_plugin +Defaults env_reset +Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin +Defaults env_keep = "COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR" +Defaults env_keep += "LS_COLORS MAIL PS1 PS2 QTDIR" +Defaults env_keep += "USERNAME LANG LC_ADDRESS LC_CTYPE LC_COLLATE" +Defaults env_keep += "LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME" +Defaults env_keep += "LC_NUMERIC LC_PAPER LC_TELEPHONE LC_TIME LC_ALL" +Defaults env_keep += "LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY" -If sudoer_separate_specs is set to `False`, it will include all defaults and aliases in /etc/sudoers rather than breaking the specs out into their own files in /etc/sudoers.d/. +# User specifications +root ALL=(ALL) ALL +%wheel ALL=(ALL) ALL -All sudoer specifications will each be placed in their own file within the `/etc/sudoers.d/` directory. A specification consists of the following: -- `name`: the name of the specification (file name in `/etc/sudoers.d/`) -- `users`: user list or user alias -- `hosts`: host list or host alias -- `operators`: operator list or runas alias -- `commands`: command list or +# Includes +## Include directories +#includedir /etc/sudoers.d +``` -The following properties are optional: -- `tags`: list of tags (ex: NOPASSWD) -- `comment`: A comment you'd like to add to your spec for clarity +### Sudoers Configuration (multiple files) +```yaml +- name: "Apply a multi-file sudoers configuration" + hosts: "all" + tasks: + - name: "Configure /etc/sudoers and included files" + include_role: + name: "ahuffman.sudoers" + vars: + sudoers_rewrite_default_sudoers_file: True + sudoers_remove_unauthorized_included_files: True + sudoers_backup: True + sudoers_backup_path: "sudoers-backups" + sudoers_files: + - path: "/etc/sudoers" + defaults: + - "!visiblepw" + - "always_set_home" + - "match_group_by_gid" + - "always_query_group_plugin" # maintains sudo pre-1.8.15 group behavior + - "env_reset" + - secure_path: + - "/sbin" + - "/bin" + - "/usr/sbin" + - "/usr/bin" + - env_keep: + - "COLORS" + - "DISPLAY" + - "HOSTNAME" + - "HISTSIZE" + - "KDEDIR" + - "LS_COLORS" + - "MAIL" + - "PS1" + - "PS2" + - "QTDIR" + - "USERNAME" + - "LANG" + - "LC_ADDRESS" + - "LC_CTYPE" + - "LC_COLLATE" + - "LC_IDENTIFICATION" + - "LC_MEASUREMENT" + - "LC_MESSAGES" + - "LC_MONETARY" + - "LC_NAME" + - "LC_NUMERIC" + - "LC_PAPER" + - "LC_TELEPHONE" + - "LC_TIME" + - "LC_ALL" + - "LANGUAGE" + - "LINGUAS" + - "_XKB_CHARSET" + - "XAUTHORITY" + user_specifications: + - users: + - "root" + hosts: + - "ALL" + operators: + - "ALL" + commands: + - "ALL" + - users: + - "%wheel" + hosts: + - "ALL" + operators: + - "ALL" + commands: + - "ALL" + include_directories: + - "/etc/sudoers.d" + aliases: + cmnd_alias: + - name: "PING" + commands: + - "/bin/ping" + user_alias: + - name: "PINGERS" + users: + - "ahuffman" + - path: "/etc/sudoers.d/pingers" + user_specifications: + - type: "user" + defaults: + - "!requiretty" + users: + - "PINGERS" + - path: "/etc/sudoers.d/root" + defaults: + - "syslog=auth" + user_specifications: + - type: "runas" + defaults: + - "!set_logname" + operators: + - "root" +``` +The example above will produce the following configuration files: +#### Results: /etc/sudoers +``` +# Ansible managed -Valid sudoer tags are: NOPASSWD, PASSWD, NOEXEC, EXEC, SETENV, NOSETENV, LOG_INPUT, NOLOG_INPUT, LOG_OUTPUT and NOLOG_OUTPUT. +# Default specifications +Defaults !visiblepw +Defaults always_set_home +Defaults match_group_by_gid +Defaults always_query_group_plugin +Defaults env_reset +Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin +Defaults env_keep = "COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR" +Defaults env_keep += "LS_COLORS MAIL PS1 PS2 QTDIR" +Defaults env_keep += "USERNAME LANG LC_ADDRESS LC_CTYPE LC_COLLATE" +Defaults env_keep += "LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME" +Defaults env_keep += "LC_NUMERIC LC_PAPER LC_TELEPHONE LC_TIME LC_ALL" +Defaults env_keep += "LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY" -User/Group specific defaults can be added to the defaults list by a preceding ':' followed by the user/group whitespace then the option. For example: +# Alias specifications +## Command Aliases +Cmnd_Alias PING = /bin/ping -```yaml ---- -sudoer_defaults: - - :MONITOR_USER !logfile -``` +## User Aliases +User_Alias PINGERS = ahuffman + +# User specifications +root ALL=(ALL) ALL +%wheel ALL=(ALL) ALL -This will generate a line: +# Includes +## Include directories +#includedir /etc/sudoers.d +``` +#### Results: /etc/sudoers.d/pingers +``` +# Ansible managed +# Default override specifications +Defaults:PINGERS !requiretty ``` -Defaults:MONITOR_USER !logfile + +#### Results: /etc/sudoers.d/root ``` +# Ansible managed +# Default specifications +Defaults syslog=auth -## Example Playbook -```yaml -- hosts: "all" - roles: - - role: "wtcross.sudoers" - sudoer_aliases: - user: - - name: "ADMINS" - comment: "Group of admin users" - users: - - "%admin" - runas: - - name: "ROOT" - comment: "Root stuff" - users: - - "#0" - host: - - name: "SERVERS" - comment: "XYZ servers" - hosts: - - "192.168.0.1" - - "192.168.0.2" - command: - - name: "ADMIN_CMNDS" - comment: "Stuff admins need" - commands: - - "/usr/sbin/passwd" - - "/usr/sbin/useradd" - - "/usr/sbin/userdel" - - "/usr/sbin/usermod" - - "/usr/sbin/visudo" - sudoer_specs: - - name: "administrators" - comment: "Stuff for admins" - users: "ADMIN" - hosts: "SERVERS" - operators: "ROOT" - tags: "NOPASSWD" - commands: "ADMIN_CMNDS" - defaults: - - '!requiretty' + +# Default override specifications +Defaults>root !set_logname ``` -## Requirements -The host operating system must be a member of one of the following OS families: +### Migrating a Running Sudoers Configuration to Another Host +```yaml +--- +- name: "Collect Existing Sudoers Facts" + hosts: "source-host" + tasks: + - name: "Collect Running Sudoers Configuration" + include_role: + name: "ahuffman.scan_sudoers" + + - name: "Set Collected Sudoers Facts" + set_fact: + sudoers_files: "{{ ansible_facts['sudoers'].sudoers_files }}" -- Debian -- RedHat -- SUSE + - name: "Display Collected Sudoers Configuration Facts" + debug: + var: "sudoers_files" + verbosity: "1" + +- name: "Deploy Running Configuration to Target" + hosts: "destination-host" + tasks: + - include_role: + name: "ahuffman.sudoers" + vars: + sudoers_remove_unauthorized_included_files: True +``` +The above example provides a method of using Infrastructure-as-Code in Reverse to take a known configuration converted to structured data to drive future automation. Alternatively to directly provisioning the collected configuration on a new host, you could push the data into a CMDB or repository for future use as a source of truth. ## License [MIT](LICENSE) ## Author Information +[Andrew J. Huffman](https://github.com/ahuffman) [Tyler Cross](https://github.com/wtcross) -[Andrew J. Huffman](https://github.com/ahuffman) diff --git a/defaults/main.yml b/defaults/main.yml index 54f3818..1e6708b 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,45 +1,75 @@ --- -sudoer_aliases: {} -sudoer_specs: [] -sudoer_defaults: -# - requiretty - - "!visiblepw" - - always_set_home - - env_reset - - env_keep: - - COLORS - - DISPLAY - - HOSTNAME - - HISTSIZE - - INPUTRC - - KDEDIR - - LS_COLORS - - MAIL - - PS1 - - PS2 - - QTDIR - - USERNAME - - LANG - - LC_ADDRESS - - LC_CTYPE - - LC_COLLATE - - LC_IDENTIFICATION - - LC_MEASUREMENT - - LC_MESSAGES - - LC_MONETARY - - LC_NAME - - LC_NUMERIC - - LC_PAPER - - LC_TELEPHONE - - LC_TIME - - LC_ALL - - LANGUAGE - - LINGUAS - - _XKB_CHARSET - - XAUTHORITY - - secure_path: /sbin:/bin:/usr/sbin:/usr/bin -sudoer_separate_specs: True -sudoer_separate_specs_cleanup: False -sudoer_rewrite_sudoers_file: True -sudoer_remove_unauthorized_specs: False -sudoer_backup: True +sudoers_rewrite_default_sudoers_file: True +sudoers_remove_unauthorized_included_files: False +# backups occur on the ansible control node by fetching the remote files to prevent accidental inclusion in a includedir +## The default sudoers_backup_path will be relative to the playbook execution path +sudoers_backup: True +sudoers_backup_path: "sudoers-backups" +sudoers_visudo_path: "/usr/sbin/visudo" + +# A default RHEL7.6 /etc/sudoers configuration was used to define the defaults +# Please check the defaults here to ensure you are applying sane settings for your OS +sudoers_files: + - path: "/etc/sudoers" + defaults: + - "!visiblepw" + - "always_set_home" + - "match_group_by_gid" + - "always_query_group_plugin" # maintains sudo pre-1.8.15 group behavior + - "env_reset" + - secure_path: + - "/sbin" + - "/bin" + - "/usr/sbin" + - "/usr/bin" + - env_keep: + - "COLORS" + - "DISPLAY" + - "HOSTNAME" + - "HISTSIZE" + - "KDEDIR" + - "LS_COLORS" + - "MAIL" + - "PS1" + - "PS2" + - "QTDIR" + - "USERNAME" + - "LANG" + - "LC_ADDRESS" + - "LC_CTYPE" + - "LC_COLLATE" + - "LC_IDENTIFICATION" + - "LC_MEASUREMENT" + - "LC_MESSAGES" + - "LC_MONETARY" + - "LC_NAME" + - "LC_NUMERIC" + - "LC_PAPER" + - "LC_TELEPHONE" + - "LC_TIME" + - "LC_ALL" + - "LANGUAGE" + - "LINGUAS" + - "_XKB_CHARSET" + - "XAUTHORITY" + user_specifications: + - users: + - "root" + hosts: + - "ALL" + operators: + - "ALL" + commands: + - "ALL" + - users: + - "%wheel" + hosts: + - "ALL" + operators: + - "ALL" + commands: + - "ALL" + include_directories: + - "/etc/sudoers.d" + # include_files: [] + # aliases: [] diff --git a/filter_plugins/to_list.py b/filter_plugins/to_list.py deleted file mode 100644 index dcb6ddc..0000000 --- a/filter_plugins/to_list.py +++ /dev/null @@ -1,11 +0,0 @@ -def to_list(a, *args, **kw): - if (isinstance(a, list)): - return a - else: - return [a] - -class FilterModule(object): - def filters(self): - return { - 'to_list': to_list - } diff --git a/meta/main.yml b/meta/main.yml index 2e7d213..aa85296 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,41 +1,75 @@ galaxy_info: role_name: "sudoers" author: - - Tyler Cross - - Andrew J. Huffman + - "Andrew J. Huffman" + - "Tyler Cross" company: "Red Hat" - description: Controls the configuration of the sudoers file and /etc/sudoers.d/ files - issue_tracker_url: https://github.com/wtcross/ansible-sudoers/issues - license: MIT - min_ansible_version: 2.3 - #github_branch: master + description: "Controls the configuration of the default /etc/sudoers file and included files/directories" + license: "MIT" + min_ansible_version: 2.5 platforms: - - name: EL + - name: "EL" versions: - - all - - name: Fedora + - "all" + - name: "Alpine" versions: - - all - - name: opensuse + - "all" + - name: "Amazon" versions: - - all - - name: Amazon + - "all" + - name: "ArchLinux" versions: - - all - - name: Ubuntu + - "all" + - name: "ClearLinux" versions: - - all - - name: SLES + - "all" + - name: "Debian" versions: - - all - - name: Debian + - "all" + - name: "DragonFlyBSD" versions: - - all + - "all" + - name: "Fedora" + versions: + - "all" + - name: "FreeBSD" + versions: + - "all" + - name: "GenericLinux" + versions: + - "all" + - name: "GenericBSD" + versions: + - "all" + - name: "Gentoo" + versions: + - "all" + - name: "HardenedBSD" + versions: + - "all" + - name: "macOS" + versions: + - "all" + - name: "OpenBSD" + versions: + - "all" + - name: "opensuse" + versions: + - "all" + - name: "SLES" + versions: + - "all" + - name: "Solaris" + versions: + - "all" + - name: "Ubuntu" + versions: + - "all" galaxy_tags: - - sudo - - sudoers - - admin - - system + - "sudo" + - "sudoers" + - "admin" + - "system" dependencies: [] diff --git a/tasks/main.yml b/tasks/main.yml index 290433f..7b54232 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -6,101 +6,124 @@ retries: "3" register: "sudo_installed" until: "sudo_installed is succeeded" + become: True -- name: "Ensure the sudoers.d directory is created" - file: - path: "/etc/sudoers.d" - owner: "root" - group: "root" - mode: "0750" - state: "directory" - -- name: "Find all existing separate sudoer specs" - find: - paths: "/etc/sudoers.d" - file_type: "file" - recurse: False - register: "existing_sudoer_spec_list" +- name: "Set include directories variable" + set_fact: + sudoers_include_dirs: "{{ sudoers_files | json_query('[*].include_directories') | flatten }}" -- name: "Get a list of all existing and authorized separate sudoer specs" +- name: "Set sudoer file paths variable" set_fact: - existing_sudoer_specs: "{{ existing_sudoer_spec_list.files | map(attribute='path') | map('basename') | list }}" - authorized_sudoer_specs: "{{ sudoer_specs | map(attribute='name') | list }}" - changed_when: False + sudoers_file_paths: "{{ sudoers_files | json_query('[*].path') | flatten }}" -- name: "Output existing separate sudoer specs" +- name: "Display all sudoers include directories" debug: - var: "existing_sudoer_specs" + var: "sudoers_include_dirs" verbosity: "1" -- name: "Output role variable defined authorized sudoer specs" +- name: "Display all sudoers file paths" debug: - var: "authorized_sudoer_specs" + var: "sudoers_file_paths" verbosity: "1" -- name: "Ensure all authorized separate sudoer specs are properly configured" - template: - src: "sudoer_spec.j2" - dest: "/etc/sudoers.d/{{ item.name }}" +- name: "Ensure include directories are created" + file: + path: "{{ item }}" owner: "root" group: "root" - mode: "0440" - validate: 'visudo -cf %s' - with_items: "{{ sudoer_specs }}" + mode: "0750" + state: "directory" + with_items: "{{ sudoers_include_dirs }}" + become: True + when: "sudoers_include_dirs | length > 0" + +# We want to pull the remote sudoer files to the ansible control node +## To prevent includedirs from accidentally activating backed up copies +- name: "Ensure local backup directory exists" + file: + state: "directory" + path: "{{ sudoers_backup_path }}" + owner: "{{ ansible_user }}" + group: "root" + mode: "0755" + become: True + delegate_to: "localhost" + connection: "local" + when: + - "sudoers_backup | bool" + - "sudoers_backup_path != ''" + +- name: "Backup sudoers files" + fetch: + dest: "{{ sudoers_backup_path }}" + fail_on_missing: False + src: "{{ item }}" + with_items: "{{ sudoers_file_paths }}" + become: True when: - - "sudoer_separate_specs" + - "sudoers_backup | bool" + - "sudoers_backup_path != ''" -- name: "Ensure the sudoers file is valid and up to date | sudoers separate specs" +# included files get created first, because if using #include +## visudo will not validate since the file does not exist yet (if new) +- name: "Ensure sudoers include files are configured" template: - src: "sudoers_nospec.j2" - dest: "/etc/sudoers" + src: "sudoers.j2" + dest: "{{ item.path }}" owner: "root" group: "root" mode: "0440" - backup: "{{ sudoer_backup }}" - validate: 'visudo -cf %s' - when: - - "sudoer_separate_specs" - - "sudoer_rewrite_sudoers_file" - -- name: "Ensure existing sudoers file supports seperate specs" - lineinfile: - state: "present" - dest: "/etc/sudoers" - line: "#includedir /etc/sudoers.d" - insertafter: "EOF" - backup: "{{ sudoer_backup }}" - validate: 'visudo -cf %s' - when: - - "sudoer_separate_specs" + validate: '{{ sudoers_visudo_path }} -cf %s' + with_items: "{{ sudoers_files }}" + become: True + loop_control: + label: "{{ item.path }}" + when: "item.path != '/etc/sudoers'" -- name: "Ensure the sudoers file is valid and up to date | omnibus sudoers" +- name: "Ensure /etc/sudoers is configured" template: - src: "sudoers_plus_spec.j2" + src: "sudoers.j2" dest: "/etc/sudoers" owner: "root" group: "root" mode: "0440" - backup: "{{ sudoer_backup }}" - validate: 'visudo -cf %s' + validate: '{{ sudoers_visudo_path }} -cf %s' + with_items: "{{ sudoers_files }}" + become: True + loop_control: + label: "{{ item.path }}" when: - - "not sudoer_separate_specs" - - "sudoer_rewrite_sudoers_file" + - "item.path == '/etc/sudoers'" + - "sudoers_rewrite_default_sudoers_file | bool" -- name: "Remove separate sudoer specs that are not authorized" - file: - path: "/etc/sudoers.d/{{ item }}" - state: "absent" - with_items: "{{ existing_sudoer_specs | difference(authorized_sudoer_specs) }}" - when: - - "sudoer_separate_specs" - - "sudoer_remove_unauthorized_specs" +# cleanup files in include_dirs that aren't listed in sudoers_files +- name: "Remove unauthorized included sudoer files" + block: + - name: "Search for sudoer files in included directories" + find: + paths: "{{ sudoers_include_dirs }}" + recurse: True + hidden: True + file_type: "any" + register: "sudoers_existing_included_files" -- name: "Remove separate sudoer specs if not using separate specs" - file: - path: "/etc/sudoers.d/{{ item }}" - state: "absent" - with_items: "{{ existing_sudoer_specs }}" + - name: "Set unauthorized included sudoers files variable" + set_fact: + sudoers_unauthorized_files: "{{ sudoers_existing_included_files | json_query('files[*].path') | + difference(sudoers_file_paths) }}" + + - name: "Display unauthorized sudoers include files to be removed" + debug: + var: "sudoers_unauthorized_files" + verbosity: "1" + + - name: "Remove unauthorized included sudoers files" + file: + path: "{{ item }}" + state: "absent" + with_items: "{{ sudoers_unauthorized_files }}" + when: "sudoers_unauthorized_files | length > 0" + become: True when: - - "not sudoer_separate_specs" - - "sudoer_separate_specs_cleanup" + - "sudoers_remove_unauthorized_included_files | bool" + - "sudoers_include_dirs | length > 0" diff --git a/templates/sudoer_spec.j2 b/templates/sudoer_spec.j2 deleted file mode 100644 index ac3e2fe..0000000 --- a/templates/sudoer_spec.j2 +++ /dev/null @@ -1,9 +0,0 @@ -# {{ ansible_managed }} - -{% if item.comment is defined and item.comment %} -# {{ item.comment }} -{% endif %} -{% if item.defaults is defined and item.defaults %} -Defaults:{{ item.users | to_list | join(',') }} {{ item.defaults | to_list | join(',') }} -{% endif %} -{{ item.users | to_list | join(',') }} {{ item.hosts | to_list | join(',') }}={% if item.operators is defined and item.operators %}({{ item.operators | to_list | join(',') }}){% endif %} {% if item.tags is defined and item.tags %}{{ item.tags | to_list | join(':') }}: {% endif %}{{ item.commands | to_list | join(',') }} diff --git a/templates/sudoers.j2 b/templates/sudoers.j2 new file mode 100644 index 0000000..a1b447a --- /dev/null +++ b/templates/sudoers.j2 @@ -0,0 +1,110 @@ +# {{ ansible_managed }} +{% if item.defaults is defined %} + +# Default specifications +{% for default in item.defaults %} +{% if default is mapping %} +{% for name, values in default.items() %} +{% if name == 'secure_path' %} +Defaults {{ name }} = {% for item in values %}{% if not loop.last %}{{ item }}:{% else %}{{ item }}{% endif %}{% endfor %} + +{% else %} +{% for items in values | list | slice(6) %} +{% if items %} +Defaults {{ name }} {% if not loop.first %}+{% endif %}= "{{ items | list | join(' ') }}" +{% endif -%} +{% endfor %} +{% endif %} +{% endfor %} +{% elif default | first == ':' %} +Defaults{{ default }} +{% else %} +Defaults {{ default }} +{% endif %} +{% endfor %} + +{% endif %} +{% if item.aliases is defined %} +# Alias specifications +{% if item.aliases.cmnd_alias is defined %} +## Command Aliases +{% for ca in item.aliases.cmnd_alias %} +Cmnd_Alias {{ ca.name }} = {% for cmnd in ca.commands %}{% if not loop.last %}{{ cmnd }}, {% else %}{{ cmnd }}{% endif %}{% endfor %} + +{% endfor %} + +{% endif %} +{% if item.aliases.host_alias is defined %} +## Host Aliases +{% for ha in item.aliases.host_alias %} +Host_Alias {{ ha.name }} = {% for host in ha.hosts %}{% if not loop.last %}{{ host }}, {% else %}{{ host }}{% endif %}{% endfor %} + +{% endfor %} + +{% endif %} +{% if item.aliases.runas_alias is defined %} +## Runas Aliases +{% for ra in item.aliases.runas_alias %} +Runas_Alias {{ ra.name }} = {% for user in ra.users %}{% if not loop.last %}{{ user }}, {% else %}{{ user }}{% endif %}{% endfor %} + +{% endfor %} + +{% endif %} +{% if item.aliases.user_alias is defined %} +## User Aliases +{% for ua in item.aliases.user_alias %} +User_Alias {{ ua.name }} = {% for user in ua.users %}{% if not loop.last %}{{ user }}, {% else %}{{ user }}{% endif %}{% endfor %} + +{% endfor %} + +{% endif %} +{% endif %} +{% if item.user_specifications is defined %} +{% if item.user_specifications | json_query('[?!type]') | flatten | length > 0 %} +# User specifications +{% for spec in item.user_specifications %} +{% if spec.type is undefined %} +{% for user in spec.users %}{% if not loop.last %}{{ user }}, {% else %}{{ user }}{% endif %}{% endfor %} {% for host in spec.hosts %}{% if not loop.last %}{{ host }}, {% else %}{{ host }}{% endif %}{% endfor %}={% if spec.operators is defined %}({% for op in spec.operators %}{% if not loop.last%}{{ op }}, {% else %}{{ op }}{% endif %}{% endfor %}){% endif %} {% if spec.selinux_type is defined %}TYPE={% for type in spec.selinux_type %}{% if not loop.last %}{{ type }}, {% else %}{{ type }} {% endif %}{% endfor %}{% endif %}{% if spec.selinux_role is defined %}ROLE={% for role in spec.selinux_role %}{% if not loop.last %}{{ role }}, {% else %}{{ role }} {% endif %}{% endfor %}{% endif %}{% if spec.solaris_privs is defined %}PRIVS={% for priv in spec.solaris_privs %}{% if not loop.last %}{{ priv }}, {% else %}{{ priv }} {% endif %}{% endfor %}{% endif %}{% if spec.solaris_limitprivs is defined %}LIMITPRIVS={% for lpriv in spec.solaris_limitprivs %}{% if not loop.last %}{{ lpriv }}, {% else %}{{ lpriv }} {% endif %}{% endfor %}{% endif %}{% if spec.tags is defined %}{% for tag in spec.tags %}{{ tag }}:{% endfor %} {% endif %}{% for cmnd in spec.commands %}{% if not loop.last %}{{ cmnd }}, {% else %}{{ cmnd }}{% endif %}{% endfor %} + +{% endif %} +{% endfor %} +{% endif %} + +{% endif %} +{% if item.user_specifications | json_query('[*].defaults') | flatten | length > 0 %} +# Default override specifications +{% for spec in item.user_specifications %} +{% if spec.type is defined %} +{% if spec.type == 'user'%} +Defaults:{% for user in spec.users %}{% if not loop.last %}{{ user }}, {% else %}{{ user }} {% endif %}{% endfor %}{% for default in spec.defaults %}{% if not loop.last %}{{ default }}, {% else %}{{ default }}{% endif %}{% endfor %} + +{% elif spec.type == 'runas' %} +Defaults>{% for op in spec.operators %}{% if not loop.last %}{{ op }}, {% else %}{{ op }} {% endif %}{% endfor %}{% for default in spec.defaults %}{% if not loop.last %}{{ default }}, {% else %}{{ default }}{% endif %}{% endfor %} + +{% elif spec.type == 'host' %} +Defaults@{% for host in spec.hosts %}{% if not loop.last %}{{ host }}, {% else %}{{ host }} {% endif %}{% endfor %}{% for default in spec.defaults %}{% if not loop.last %}{{ default }}, {% else %}{{ default }}{% endif %}{% endfor %} + +{% elif spec.type == 'command' %} +Defaults!{% for cmnd in spec.commands %}{% if not loop.last %}{{ cmnd }}, {% else %}{{ cmnd }} {% endif %}{% endfor %}{% for default in spec.defaults %}{% if not loop.last %}{{ default }}, {% else %}{{ default }}{% endif %}{% endfor %} + +{% endif %} +{% endif %} +{% endfor %} + +{% endif %} +{% if item.include_files is defined or item.include_directories is defined %} +# Includes +{% if item.include_files is defined and item.include_files | length > 0 %} +## Include files +{% for file in item.include_files %} +#include {{ file }} +{% endfor %} + +{% endif %} +{% if item.include_directories is defined and item.include_directories | length > 0 %} +## Include directories +{% for dir in item.include_directories %} +#includedir {{ dir }} +{% endfor %} +{% endif %} +{% endif %} diff --git a/templates/sudoers_nospec.j2 b/templates/sudoers_nospec.j2 deleted file mode 100644 index b840ba2..0000000 --- a/templates/sudoers_nospec.j2 +++ /dev/null @@ -1,70 +0,0 @@ -# {{ ansible_managed }} - -{% for default in sudoer_defaults %} -{% if default is mapping %} -{% for name, values in default.items() %} -{% for items in values | to_list | slice(6) %} -{% if items %} -Defaults {{ name }} {% if not loop.first %}+{% endif %}= "{{ items | to_list | join(' ') }}" -{% endif -%} -{% endfor %} -{% endfor %} -{% elif default|first == ':' %} -Defaults{{ default }} -{% else %} -Defaults {{ default }} -{% endif %} -{% endfor %} - -{% if sudoer_aliases.user is defined and sudoer_aliases.user %} -## User Aliases -## These aren't often necessary, as you can use regular groups -## (ie, from files, LDAP, NIS, etc) in this file - just use %groupname -## rather than USERALIAS -{% for alias in sudoer_aliases.user %} -{% if alias.comment is defined and alias.comment %} -# {{ alias.comment }} -{% endif %} -User_Alias {{ alias.name }} = {{ alias.users | join(',') }} -{% endfor %} - -{% endif %} -{% if sudoer_aliases.runas is defined and sudoer_aliases.runas %} -## Runas Aliases -{% for alias in sudoer_aliases.runas %} -{% if alias.comment is defined and alias.comment %} -# {{ alias.comment }} -{% endif %} -Runas_Alias {{ alias.name }} = {{ alias.users | join(',') }} -{% endfor %} - -{% endif %} -{% if sudoer_aliases.host is defined and sudoer_aliases.host %} -## Host Aliases -## Groups of machines. You may prefer to use hostnames (perhaps using -## wildcards for entire domains) or IP addresses instead. -{% for alias in sudoer_aliases.host %} -{% if alias.comment is defined and alias.comment %} -# {{ alias.comment }} -{% endif %} -Host_Alias {{ alias.name }} = {{ alias.hosts | join(',') }} -{% endfor %} - -{% endif %} -{% if sudoer_aliases.command is defined and sudoer_aliases.command %} -## Command Aliases -## These are groups of related commands... -{% for alias in sudoer_aliases.command %} -{% if alias.comment is defined and alias.comment %} -# {{ alias.comment }} -{% endif %} -Cmnd_Alias {{ alias.name }} = {{ alias.commands | join(',') }} -{% endfor %} - -{% endif %} - -## Allow root to run any commands anywhere -root ALL=(ALL) ALL - -## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment) -#includedir /etc/sudoers.d diff --git a/templates/sudoers_plus_spec.j2 b/templates/sudoers_plus_spec.j2 deleted file mode 100644 index 4f53ffc..0000000 --- a/templates/sudoers_plus_spec.j2 +++ /dev/null @@ -1,80 +0,0 @@ -# {{ ansible_managed }} - -{% for default in sudoer_defaults %} -{% if default is mapping %} -{% for name, values in default.items() %} -{% for items in values | to_list | slice(6) %} -{% if items %} -Defaults {{ name }} {% if not loop.first %}+{% endif %}= "{{ items | to_list | join(' ') }}" -{% endif -%} -{% endfor %} -{% endfor %} -{% elif default|first == ':' %} -Defaults{{ default }} -{% else %} -Defaults {{ default }} -{% endif %} -{% endfor %} - -{% if sudoer_aliases.user is defined and sudoer_aliases.user %} -## User Aliases -## These aren't often necessary, as you can use regular groups -## (ie, from files, LDAP, NIS, etc) in this file - just use %groupname -## rather than USERALIAS -{% for alias in sudoer_aliases.user %} -{% if alias.comment is defined %} -# {{ alias.comment }} -{% endif %} -User_Alias {{ alias.name }} = {{ alias.users | join(',') }} -{% endfor %} - -{% endif %} -{% if sudoer_aliases.runas is defined and sudoer_aliases.runas %} -## Runas Aliases -{% for alias in sudoer_aliases.runas %} -{% if alias.comment is defined %} -# {{ alias.comment }} -{% endif %} -Runas_Alias {{ alias.name }} = {{ alias.users | join(',') }} -{% endfor %} - -{% endif %} -{% if sudoer_aliases.host is defined and sudoer_aliases.host %} -## Host Aliases -## Groups of machines. You may prefer to use hostnames (perhaps using -## wildcards for entire domains) or IP addresses instead. -{% for alias in sudoer_aliases.host %} -{% if alias.comment is defined %} -# {{ alias.comment }} -{% endif %} -Host_Alias {{ alias.name }} = {{ alias.hosts | join(',') }} -{% endfor %} - -{% endif %} -{% if sudoer_aliases.command is defined and sudoer_aliases.command %} -## Command Aliases -## These are groups of related commands... -{% for alias in sudoer_aliases.command %} -{% if alias.comment is defined %} -# {{ alias.comment }} -{% endif %} -Cmnd_Alias {{ alias.name }} = {{ alias.commands | join(',') }} -{% endfor %} - -{% endif %} - -## Allow root to run any commands anywhere -root ALL=(ALL) ALL - -{% if sudoer_specs %} -## Sudoer specifications -{% for spec in sudoer_specs %} -{% if spec.comment is defined %} -# {{ spec.comment }} -{% endif %} -{% if spec.defaults is defined and spec.defaults %} -Defaults:{{ spec.users | to_list | join(',') }} {{ spec.defaults | to_list | join(',') }} -{% endif %} -{{ spec.users | to_list | join(',') }} {{ spec.hosts | to_list | join(',') }}={% if spec.operators is defined and spec.operators %}({{ spec.operators | to_list | join(',') }}){% endif %} {% if spec.tags is defined and spec.tags %}{{ spec.tags | to_list | join(':') }}: {% endif %}{{ spec.commands | to_list | join(',') }} -{% endfor %} -{% endif %} diff --git a/test/ansible-setup.sh b/test/ansible-setup.sh deleted file mode 100644 index 39b0dda..0000000 --- a/test/ansible-setup.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -set -e - -## This is an example setup script that you would encapsulate the installation -# What version of avm setup to use -echo "Setting up Ansible Version Manager" -AVM_VERSION="v1.0.0-rc.8" -## Install Ansible 1.9.6 using pip and label it 'v1.9' -export ANSIBLE_VERSIONS_0="1.9.6" -export INSTALL_TYPE_0="pip" -export ANSIBLE_LABEL_0="v1.9" -## Install Ansible 2.2.1 using pip and label it 'v2.2' -export ANSIBLE_VERSIONS_1="2.2.1.0" -export INSTALL_TYPE_1="pip" -export ANSIBLE_LABEL_1="v2.2" -## Install Ansible 2.3.1 using pip and label it 'v2.3' -export ANSIBLE_VERSIONS_2="2.3.1.0" -export INSTALL_TYPE_2="pip" -export ANSIBLE_LABEL_2="v2.3" -# Whats the default version -ANSIBLE_DEFAULT_VERSION="v2.3" - -## Create a temp dir to download avm -avm_dir="$(mktemp -d 2> /dev/null || mktemp -d -t 'mytmpdir')" -git clone https://github.com/ahelal/avm.git "${avm_dir}" > /dev/null 2>&1 - -## Run the setup -/bin/sh ${avm_dir}/setup.sh - -exit 0 diff --git a/test/integration/default/default.yml b/test/integration/default/default.yml deleted file mode 100644 index c44974c..0000000 --- a/test/integration/default/default.yml +++ /dev/null @@ -1,59 +0,0 @@ -- hosts: test-kitchen - remote_user: root - - roles: - - role: ansible-sudoers - sudoer_rewrite_sudoers_file: true - sudoer_remove_unauthorized_specs: false - sudoer_separate_specs: true - sudoer_defaults: - - "!visiblepw" - - always_set_home - - env_reset - - secure_path: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin - - env_keep: - - COLORS - - DISPLAY - - HOSTNAME - - HISTSIZE - - KDEDIR - - LS_COLORS - - MAIL - - PS1 - - PS2 - - QTDIR - - USERNAME - - LANG - - LC_ADDRESS - - LC_CTYPE - - LC_COLLATE - - LC_IDENTIFICATION - - LC_MEASUREMENT - - LC_MESSAGES - - LC_MONETARY - - LC_NAME - - LC_NUMERIC - - LC_PAPER - - LC_TELEPHONE - - LC_TIME - - LC_ALL - - LANGUAGE - - LINGUAS - - _XKB_CHARSET - - XAUTHORITY - sudoer_aliases: - user: - - name: ADMINS - comment: Alias of for groups of admin users - users: - - "%admin" - - "%wheel" - - "%adm" - - "%sudo" - sudoer_specs: - - name: admins - comment: Members of one of the admin groups may gain root privileges. - users: ADMINS - hosts: ALL - operators: ALL - commands: ALL diff --git a/test/integration/default/serverspec/default_spec.rb b/test/integration/default/serverspec/default_spec.rb deleted file mode 100644 index ba34dfc..0000000 --- a/test/integration/default/serverspec/default_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require_relative './spec_helper' - -describe 'ansible-sudoers::default' do - - describe package('sudo') do - it { should be_installed } - end - - describe file('/etc/sudoers') do - it { should exist } - it { should be_mode 440 } - it { should be_owned_by 'root' } - its(:content) { should match(/#includedir \/etc\/sudoers.d/) } - its(:content) { should match(/User_Alias ADMINS = %admin,%wheel/) } - end - - describe file('/etc/sudoers.d/kitchen') do - it { should exist } - it { should be_mode 440 } - it { should be_owned_by 'root' } - its(:content) { should match(/kitchen ALL=\(ALL\) NOPASSWD: ALL/) } - end - - describe file('/etc/sudoers.d/admins') do - it { should exist } - it { should be_mode 440 } - it { should be_owned_by 'root' } - its(:content) { should match(/ADMINS ALL=\(ALL\) ALL/) } - end - -end diff --git a/test/integration/default/serverspec/spec_helper.rb b/test/integration/default/serverspec/spec_helper.rb deleted file mode 100644 index a9c6f99..0000000 --- a/test/integration/default/serverspec/spec_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'serverspec' - -# :backend can be either :exec or :ssh -# since we are running local we use :exec -set :backend, :exec - -RSpec.configure do |c| - c.before :all do - c.path = '/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin' - end -end