Ansible – Managing /etc/security/access.conf

ansiblelinuxpam

New to Ansible and have several machines which make use of the pam_access module which is configured in /etc/security/access.conf. Multiple lines specifying ALLOW/DENY (+/-) and then either users or groups along with src IP addresses to match.

What's a cleaner way to manage this file in Ansible?

The file /etc/security/access.conf looks something like this:

+ : root     : cron crond :0 ttyS0 tty1 tty2 tty3 tty4 tty5 tty6
+ : root     : 10.137.198.176
+ : inventory: 10.137.198.25
+ : sysadmin : 10.137.198.202
- : ALL : ALL

Most machines will employ this base ACL and then other groups or users may be added as need be. Web devs login over SFTP on two of the web servers so an ACL would be inserted for that group (before the - : ALL : ALL line) like so:

+ : webdev   : 10.137.198.0/24
- : ALL : ALL

Some web servers also run passenger. The developers for these systems push out code as the passenger user, so this user is allowed from the dev LAN, but this is not on all web servers.

+ : passenger: 10.137.197.0/24
- : ALL : ALL

The rules become quite specific for each machine and I don't see an easy way of managing this file. I started with the playbook and group_vars below which sort of works but having multiple definitions for the same machine needs help. Probably an array definition for line: in the playbook is needed and looking into that.

Also, this model would require re-writing the file every time since removing a host from group_vars doesn't remove the entry from access.conf. The only states for lineinfile are absent or present. I need a state called add_line_if_missing_remove_if_absent_from_vars. Is there a better way?


- hosts: all
  vars_files:
    - ../group_vars/test-etc-security-access-conf.yml
  gather_facts: false

  tasks:
    - name: "Set up /etc/security/access.conf for {{ inventory_hostname }}"
      when: inventory_hostname is match (item.name)
      lineinfile:
        dest  : /tmp/access.conf
        regexp: '{{ item.regexp }}'
        line: '{{ item.line }}'
      with_items: "{{ access_rules }}"

group_vars – got to be a better way:

access_rules:
  - name: 10.137.1.66
    regexp: '.*passenger.*'
    line: '+ : passenger : 10.137.10.0/24'

  - name: 10.137.1.66
    regexp: '.*webdev.*'
    line: '+ : webdev : 10.137.198.0/24'

Best Answer

This is what I ended up using. Maybe it will be useful to someone else. The playbook will apply a base ACL to every host and then look for an additional ACL under the directory {{ acl_dir }} with the name of the current host being provisioned. If this exists, it gets applied. Finally, the default deny rule is appended last.

I thought a comment at the beginning of the file would be nice to have noting when the file was last updated. gather_facts adds a lot of execution time so opted for the shell: command to get the date instead.

---
- hosts: all
  vars:
    dest_file: /etc/security/access.conf
    base_acl : base
    acl_dir  : ../group_vars/pam_access
  gather_facts: false

  tasks:
    - name: "Empty existing ACL and replace with base ACL"
      copy:
        content: ""
        dest   : "{{ dest_file }}"
        owner  : root
        group  : root
        mode   : 0644

    - name: "Get current date from shell rather than wait for gather_facts"
      shell: date '+%Y-%m-%d'
      register: date_now

    - name: "Add ansible header with modified date to beginning of file"
      copy:
        content: "# updated via ansible on {{ date_now.stdout }} by {{ lookup ('env', 'LOGNAME') }}"
        dest: "{{ dest_file }}"
        owner  : root
        group  : root
        mode   : 0644

    - name: "Add base ACL {{ acl_dir }}/base to {{ inventory_hostname }}"
      lineinfile:
        dest: "{{ dest_file }}"
        line: "{{ lookup ('file', '{{ acl_dir }}/{{ base_acl }}') }}"

    - name: "Check for additional ACL {{ acl_dir }}/{{ inventory_hostname }}..."
      stat:
        path  : "{{ acl_dir }}/{{ inventory_hostname }}"
      register: host_acl

    - name: "Add host specific ACL from {{ acl_dir }}/{{ inventory_hostname }}"
      when: host_acl.stat.exists
      lineinfile:
        dest: "{{ dest_file }}"
        line: "{{ lookup ('file', '{{ acl_dir }}/{{ inventory_hostname }}') }}"

    - name: "Add default DENY as last ACL as a security precaution"
      lineinfile:
        dest: "{{ dest_file }}"
        line: "- : ALL : ALL"

base ACL:

+ : root     : cron crond :0 ttyS0 tty1 tty2 tty3 tty4 tty5 tty6
+ : root     : 10.137.198.176
+ : inventory: 10.137.198.25
+ : sysadmin : 10.137.198.202

additional host ACL named websrv01.mydom.com:

+ : webdev   : 10.137.198.0/24
+ : passenger: 10.137.197.0/24