diff --git a/README.md b/README.md index 53beb76..9b83b58 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # wtcross.sudoers -An Ansible role for configuring the /etc/sudoers file and /etc/sudoers.d files. +An Ansible role for configuring the `/etc/sudoers` file and `/etc/sudoers.d` files. This role makes it possible to completely define your sudoers configuration with Ansible. All of the following are configurable: - defaults @@ -12,101 +12,20 @@ This role makes it possible to completely define your sudoers configuration with *Tip:* Here's a [great document about sudoers configuration](https://help.ubuntu.com/community/Sudoers) -Role Tunables --------------- +## Role Variables By default this role configures and manages all sudo specs. These are various -variables that can be set to adjust how the role will affect existing sudo configurations. -| Variable name | Variable type | Description | Default Value | -| --- | --- | --- | --- | -| `sudoer_rewrite_sudoers_file` | boolean | Use role default or user defined `default_specs` replacing distro supplied `/etc/sudoers` file. | True | -| `sudoer_remove_unauthorized_specs` | boolean | Each sudoer spec not generated by role will be removed. ***Very Dangerous***. | True | -| `sudoer_separate_specs` | boolean | Each sudoer spec will be placed in a separate file within the `/etc/sudoers.d/` directory. | True | - -## 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 defaults or your playbook. sudoer_separate_specs is set to True by default. - -***Warning, this role will clean out /etc/sudoers.d/ if sudoer_separate_specs is set to false. You will lose any files stored there even if not generated by this role.*** - -If sudoer_separate_specs is set to true, it will include all defaults and aliases in /etc/sudoers rather than breaking the specs out into their own files in /etc/sudoers.d/. - -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 - -The following properties are optional: -- `tags`: list of tags (ex: NOPASSWD) -- `comment`: A comment you'd like to add to your spec for clarity - -Valid sudoer tags are: NOPASSWD, PASSWD, NOEXEC, EXEC, SETENV, NOSETENV, LOG_INPUT, NOLOG_INPUT, LOG_OUTPUT and NOLOG_OUTPUT. - -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: +| 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 | -```yaml ---- -sudoer_defaults: - - :MONITOR_USER !logfile -``` - -This will generate a line: - -``` -Defaults:MONITOR_USER !logfile -``` - - -## Example Playbook -```yaml -- hosts: all - vars: - 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' - - roles: - - wtcross.sudoers -``` - -## Defaults: +## Role Default Variables ```yaml sudoer_aliases: {} sudoer_specs: [] @@ -149,19 +68,11 @@ sudoer_defaults: - secure_path: /sbin:/bin:/usr/sbin:/usr/bin sudoer_separate_specs: True sudoer_rewrite_sudoers_file: True -sudoer_remove_unauthorized_specs: True +sudoer_remove_unauthorized_specs: False +sudoer_separate_specs_cleanup: False +sudoer_backup: True ``` -## Requirements -The host operating system must be a member of one of the following OS families: - -- Debian -- RedHat -- SUSE - -## Dependencies -None - ## Variable Schemas ```yaml # host alias @@ -193,20 +104,103 @@ tags: string|[string] comment: string #procedes the alias with a comment defaults: string|[string] -## Role 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 +## 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]` + - string + - string: string + - string: [string] +``` + +## 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`. + +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/. + +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 + +The following properties are optional: +- `tags`: list of tags (ex: NOPASSWD) +- `comment`: A comment you'd like to add to your spec for clarity + +Valid sudoer tags are: NOPASSWD, PASSWD, NOEXEC, EXEC, SETENV, NOSETENV, LOG_INPUT, NOLOG_INPUT, LOG_OUTPUT and NOLOG_OUTPUT. + +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: + +```yaml +--- +sudoer_defaults: + - :MONITOR_USER !logfile ``` +This will generate a line: + +``` +Defaults:MONITOR_USER !logfile +``` + + +## 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' +``` + +## Requirements +The host operating system must be a member of one of the following OS families: + +- Debian +- RedHat +- SUSE + ## License [MIT](LICENSE) diff --git a/defaults/main.yml b/defaults/main.yml index 13c62c6..54f3818 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -39,5 +39,7 @@ sudoer_defaults: - 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: True +sudoer_remove_unauthorized_specs: False +sudoer_backup: True diff --git a/meta/main.yml b/meta/main.yml index aba346b..44c7c16 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -2,10 +2,11 @@ galaxy_info: author: - Tyler Cross - Andrew J. Huffman + 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.0 + min_ansible_version: 2.3 #github_branch: master platforms: - name: EL @@ -33,7 +34,6 @@ galaxy_info: galaxy_tags: - sudo - sudoers - - sudoers.d - admin - system diff --git a/tasks/main.yml b/tasks/main.yml index febf2d9..39bff6f 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,91 +1,103 @@ --- -- name: Ensure sudo is installed +- name: "Ensure sudo is installed" package: - name: sudo - state: present + name: "sudo" + state: "present" -- name: Ensure the sudoers.d directory is created +- name: "Ensure the sudoers.d directory is created" file: - path: /etc/sudoers.d - owner: root - group: root - mode: 0750 - state: directory + path: "/etc/sudoers.d" + owner: "root" + group: "root" + mode: "0750" + state: "directory" -- name: Find all existing separate sudoer specs +- name: "Find all existing separate sudoer specs" find: paths: "/etc/sudoers.d" - file_type: file - recurse: no - register: existing_sudoer_spec_list + file_type: "file" + recurse: False + register: "existing_sudoer_spec_list" -- name: Get a list of all existing and authorized separate sudoer specs +- name: "Get a list of all existing and authorized separate sudoer specs" 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 -- name: Ensure all authorized separate sudoer specs are properly configured +- name: "Output existing separate sudoer specs" + debug: + var: "existing_sudoer_specs" + verbosity: "1" + +- name: "Output role variable defined authorized sudoer specs" + debug: + var: "authorized_sudoer_specs" + verbosity: "1" + +- name: "Ensure all authorized separate sudoer specs are properly configured" template: - src: sudoer_spec.j2 + src: "sudoer_spec.j2" dest: "/etc/sudoers.d/{{ item.name }}" - owner: root - group: root - mode: 0440 + owner: "root" + group: "root" + mode: "0440" validate: 'visudo -cf %s' - with_items: '{{ sudoer_specs }}' + with_items: "{{ sudoer_specs }}" when: - - sudoer_separate_specs | bool + - "sudoer_separate_specs" -- name: Ensure the sudoers file is valid and up to date (separate specs) +- name: "Ensure the sudoers file is valid and up to date | sudoers separate specs" template: - src: sudoers_nospec.j2 - dest: /etc/sudoers - owner: root - group: root - mode: 0440 + src: "sudoers_nospec.j2" + dest: "/etc/sudoers" + owner: "root" + group: "root" + mode: "0440" + backup: "{{ sudoer_backup }}" validate: 'visudo -cf %s' when: - - sudoer_separate_specs | bool - - sudoer_rewrite_sudoers_file | bool + - "sudoer_separate_specs" + - "sudoer_rewrite_sudoers_file" -- name: Make sudoers file support seperate specs if not already replaced +- name: "Ensure existing sudoers file supports seperate specs" lineinfile: - state: present - dest: /etc/sudoers - line: '#includedir /etc/sudoers.d' - insertafter: EOF - backup: yes - validate: visudo -cf %s + state: "present" + dest: "/etc/sudoers" + line: "#includedir /etc/sudoers.d" + insertafter: "EOF" + backup: "{{ sudoer_backup }}" + validate: 'visudo -cf %s' when: - - sudoer_separate_specs | bool + - "sudoer_separate_specs" -- name: Ensure the sudoers file is valid and up to date (specs all in one) +- name: "Ensure the sudoers file is valid and up to date | omnibus sudoers" template: - src: sudoers_plus_spec.j2 - dest: /etc/sudoers - owner: root - group: root - mode: 0440 - validate: visudo -cf %s + src: "sudoers_plus_spec.j2" + dest: "/etc/sudoers" + owner: "root" + group: "root" + mode: "0440" + backup: "{{ sudoer_backup }}" + validate: 'visudo -cf %s' when: - - not sudoer_separate_specs | bool - - sudoer_rewrite_sudoers_file | bool + - "not sudoer_separate_specs" + - "sudoer_rewrite_sudoers_file" -- name: Remove separate sudoer specs that are not authorized +- name: "Remove separate sudoer specs that are not authorized" file: path: "/etc/sudoers.d/{{ item }}" - state: absent + state: "absent" with_items: "{{ existing_sudoer_specs | difference(authorized_sudoer_specs) }}" when: - - sudoer_separate_specs | bool - - sudoer_remove_unauthorized_specs | bool + - "sudoer_separate_specs" + - "sudoer_remove_unauthorized_specs" -- name: Remove separate sudoer specs if not using separate specs +- name: "Remove separate sudoer specs if not using separate specs" file: path: "/etc/sudoers.d/{{ item }}" - state: absent + state: "absent" with_items: "{{ existing_sudoer_specs }}" when: - - not sudoer_separate_specs | bool - - sudoer_remove_unauthorized_specs | bool + - "not sudoer_separate_specs" + - "sudoer_separate_specs_cleanup" diff --git a/templates/sudoer_spec.j2 b/templates/sudoer_spec.j2 index 6a91a5b..ac3e2fe 100644 --- a/templates/sudoer_spec.j2 +++ b/templates/sudoer_spec.j2 @@ -1,7 +1,7 @@ -#{{ ansible_managed }} +# {{ ansible_managed }} {% if item.comment is defined and item.comment %} -#{{ item.comment }} +# {{ item.comment }} {% endif %} {% if item.defaults is defined and item.defaults %} Defaults:{{ item.users | to_list | join(',') }} {{ item.defaults | to_list | join(',') }} diff --git a/templates/sudoers_nospec.j2 b/templates/sudoers_nospec.j2 index aac3ae2..2340d27 100644 --- a/templates/sudoers_nospec.j2 +++ b/templates/sudoers_nospec.j2 @@ -1,4 +1,4 @@ -#{{ ansible_managed }} +# {{ ansible_managed }} {% for default in sudoer_defaults %} {% if default is mapping %} @@ -6,7 +6,7 @@ {% for items in values | to_list | slice(6) %} {% if items %} Defaults {{ name }} {% if not loop.first %}+{% endif %}= "{{ items | to_list | join(' ') }}" -{% endif -%} +{% endif -%} {% endfor %} {% endfor %} {% elif default|first == ':' %} @@ -17,40 +17,46 @@ Defaults {{ default }} {% endfor %} {% if sudoer_aliases.user is defined and sudoer_aliases.user %} -#User Aliases +## 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 }} +# {{ alias.comment }} {% endif %} User_Alias {{ alias.name }} = {{ alias.users | join(',') }} {% endfor %} {% endif %} {% if sudoer_aliases.runas is defined and sudoer_aliases.runas %} -#Runas Aliases +## Runas Aliases {% for alias in sudoer_aliases.runas %} {% if alias.comment is defined and alias.comment %} -#**{{ 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 +## 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 }} +# {{ alias.comment }} {% endif %} Host_Alias {{ alias.name }} = {{ alias.hosts | join(',') }} {% endfor %} {% endif %} {% if sudoer_aliases.command is defined and sudoer_aliases.command %} -#Command Aliases +## Command Aliases +## These are groups of related commands... {% for alias in sudoer_aliases.command %} {% if alias.comment is defined and alias.comment %} -#**{{ alias.comment }} +# {{ alias.comment }} {% endif %} Cmnd_Alias {{ alias.name }} = {{ alias.commands | join(',') }} {% endfor %} @@ -60,5 +66,5 @@ Cmnd_Alias {{ alias.name }} = {{ alias.commands | join(',') }} ## Allow root to run any commands anywhere root ALL=(ALL) ALL -# Include all sudoer specifications +## 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 index 6f0ee76..d0d3d41 100644 --- a/templates/sudoers_plus_spec.j2 +++ b/templates/sudoers_plus_spec.j2 @@ -1,4 +1,4 @@ -#{{ ansible_managed }} +# {{ ansible_managed }} {% for default in sudoer_defaults %} {% if default is mapping %} @@ -6,7 +6,7 @@ {% for items in values | to_list | slice(6) %} {% if items %} Defaults {{ name }} {% if not loop.first %}+{% endif %}= "{{ items | to_list | join(' ') }}" -{% endif -%} +{% endif -%} {% endfor %} {% endfor %} {% elif default|first == ':' %} @@ -17,40 +17,46 @@ Defaults {{ default }} {% endfor %} {% if sudoer_aliases.user is defined and sudoer_aliases.user %} -#User Aliases +## 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 }} +# {{ alias.comment }} {% endif %} User_Alias {{ alias.name }} = {{ alias.users | join(',') }} {% endfor %} {% endif %} {% if sudoer_aliases.runas is defined and sudoer_aliases.runas %} -#Runas Aliases +## Runas Aliases {% for alias in sudoer_aliases.runas %} {% if alias.comment is defined %} -#**{{ 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 +## 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 }} +# {{ alias.comment }} {% endif %} Host_Alias {{ alias.name }} = {{ alias.hosts | join(',') }} {% endfor %} {% endif %} {% if sudoer_aliases.command is defined and sudoer_aliases.command %} -#Command Aliases +## Command Aliases +## These are groups of related commands... {% for alias in sudoer_aliases.command %} {% if alias.comment is defined %} -#**{{ alias.comment }} +# {{ alias.comment }} {% endif %} Cmnd_Alias {{ alias.name }} = {{ alias.commands | join(',') }} {% endfor %} @@ -61,10 +67,10 @@ Cmnd_Alias {{ alias.name }} = {{ alias.commands | join(',') }} root ALL=(ALL) ALL {% if sudoer_specs %} -#Sudoer specifications +## Sudoer specifications {% for spec in sudoer_specs %} {% if spec.comment is defined %} -#**{{ spec.comment }} +# {{ spec.comment }} {% endif %} {% if spec.defaults is defined and spec.defaults %} Defaults:{{ spec.users | to_list | join(',') }} {{ spec.defaults | to_list | join(',') }} diff --git a/test/ansible-setup.sh b/test/ansible-setup.sh old mode 100755 new mode 100644