Every computer offers security measures to isolate it from outside attacks. In this 21st article in the DevOps series, we will learn how to harden, automate and verify a Parabola GNU/Linux-libre system using Ansible.
Parabola GNU/Linux-libre is a free software GNU/Linux distribution based on packages from Arch Linux, but without the binary blobs. It respects the freedom of users and is endorsed by the Free Software Foundation. It is extremely lightweight and simple in design. The distribution follows a rolling release, but also provides installation media in the ISO format. The Pacman package manager is used, and the software packages are available for i686, x86_64 and armv7h architectures. The Linux-libre kernel is used instead of the generic Linux kernel. The official IRC channel is #parabola on irc.freenode.net, and the website of the project is https://www.parabola.nu/.
Setup
A Parabola GNU/Linux-libre (x86_64) 2018.06.02 ISO is used to set up a guest virtual machine (VM) with KVM/QEMU. The host system is also a Parabola GNU/Linux-libre x86_64 system, and Ansible is installed using the distribution package manager. The version of Ansible used is 2.6.0, as is indicated below:
$ ansible --version ansible 2.6.0 config file = /etc/ansible/ansible.cfg configured module search path = [‘/home/guest/.ansible/plugins/modules’, ‘/usr/share/ansible/plugins/modules’] ansible python module location = /usr/lib/python3.6/site-packages/ansible executable location = /usr/bin/ansible python version = 3.6.5 (default, May 11 2018, 04:00:52) [GCC 8.1.0]
The Ansible inventory, playbook and configuration files are created on the host system as follows:
ansible/inventory/kvm/ /playbooks/configuration/ /files/
The inventory/kvm/inventory file contains the following:
parabola ansible_host=192.168.122.128 ansible_connection=ssh ansible_user=parabola ansible_password=password
The sudo package needs to be installed in the guest Parabola VM. A parabola user is created in the guest VM, and sudo access is provided for this user with the visudo command. You also need to ensure that the ssh daemon is started in the guest VM using the following command:
$ sudo systemctl start sshd
You should add an entry in the /etc/hosts file on the host system for the Parabola guest VM as indicated below:
192.168.122.128 parabola
You can now test connectivity from Ansible to the Parabola guest VM using the following Ansible commands:
$ ansible -i inventory/kvm/inventory parabola -m ping parabola | SUCCESS => { “changed”: false, “ping”: “pong” }
Passwords
The pwgen utility is useful to generate passwords. It has a number of options that you can use to generate strong passwords with a combination of numerals, symbols and characters. The Ansible playbook to install pwgen is given below for reference:
- name: Harden Parabola hosts: parabola become: yes become_method: sudo gather_facts: yes tags: [security] tasks: - name: Install pwgen package: name: pwgen state: latest
/etc file permissions
/etc/shadow and /etc/passwd should have restricted file permissions. The /etc/shadow file should be owned by the root user and should not be accessible by any other user. The /etc/passwd file should similarly be owned by the root user and have restricted permissions. The following part of the Ansible playbook checks the required permissions of these two files:
- name: File permissions for /etc/shadow stat: path: /etc/shadow register: shadow - assert: that: - “shadow.stat.pw_name == ‘root’” - “shadow.stat.readable == true” - “shadow.stat.rgrp == false” - “shadow.stat.roth == false” - “shadow.stat.writeable == true” - “shadow.stat.wgrp == false” - “shadow.stat.woth == false” - “shadow.stat.xgrp == false” - “shadow.stat.xoth == false” - “shadow.stat.xusr == false” - name: File permissions for /etc/passwd stat: path: /etc/passwd register: passwd - assert: that: - “passwd.stat.pw_name == ‘root’” - “passwd.stat.readable == true” - “passwd.stat.rgrp == true” - “passwd.stat.roth == true” - “passwd.stat.writeable == true” - “passwd.stat.wgrp == false” - “passwd.stat.woth == false” - “passwd.stat.xgrp == false” - “passwd.stat.xoth == false” - “passwd.stat.xusr == false”
Enforcing strong passwords
The password policy can be defined in the /etc/pam.d/passwd file. For example, the following constraints are enforced when creating new passwords using the Ansible playbook given below:
- In case of error, prompt twice for password.
- The minimum length of the password should be ten characters.
- At least six characters should be different from the old password.
- There should be at least one digit.
- There should be at least one upper case character.
- There should be at least one other character.
- There should be at least one lower case character.
- name: Update /etc/pam.d/passwd lineinfile: path: /etc/pam.d/passwd insertbefore: ‘^password.*required.*pam_unix.so’ line: ‘password required pam_cracklib.so retry=2 minlen=10 difok=6 dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1’
Disk encryption
It is recommended that your disk be encrypted, and you use a strong passphrase that can be provided as input when booting the system. The Parabola wiki has useful documentation on disk encryption at https://wiki.parabola.nu/Disk_encryption. You can verify that the disk is encrypted using the following Ansible code snippet:
- name: Check disk is encrypted shell: lsblk /dev/sda | grep / | grep crypt register: encrypted - assert: that: - “encrypted.rc == 0”
Mount options
The file systems that are mounted should have restrictive options, and use only what is required. The following commands check the nosuid, nodev and noexec options for the /tmp and /dev/shm directories:
- name: Check /tmp shell: mount | grep /tmp | grep -E ‘nosuid.*nodev’ register: tmp - assert: that: - “tmp.rc == 0” - name: Check /dev/shm shell: mount | grep /dev/shm | grep -E ‘nosuid.*nodev.*noexec’ register: tmp - assert: that: - “tmp.rc == 0”
File access permissions
Access to /boot and /etc/iptables should be restricted to only users with privileged access. The following playbook snippet updates the 0700 permission for both the /boot and /etc/iptables directories.
- name: Change file mode for /boot, /etc/iptables file: path: “{{ item }}” mode: 0700 with_items: - /boot -/etc/iptables
Lock out user
You can lock a user account after a certain number of failed logins by updating the /etc/pam.d/system-login file as shown below:
- name: Update /etc/pam.d/system-login lineinfile: path: /etc/pam.d/system-login regexp: ‘^auth.*required.*pam_tally.so’ line: ‘auth required pam_tally.so deny=2 unlock_time=600 onerr=succeed file=/var/log/faillog’
The account can only be unlocked after a certain time (unlock_time) or by the root user.
Limit the processes
In order to prevent Denial of Service (DoS) attacks, it is a good practice to limit the number of processes that a user can run. This policy can be updated in the /etc/security/limits.conf file as shown below:
- name: Update /etc/security/limits.conf lineinfile: path: /etc/security/limits.conf insertbefore: ‘# End of file’ line: “{{ item }}” with_items: - ‘* soft nproc 100’ - ‘* hard nproc 200’
Restrict root login
You can restrict root login by allowing only users who belong to the wheel group to log in to root using su. The same needs to be updated in the /etc/pam.d/su and /etc/pam.d/su-l files as shown below:
- name: Update /etc/pam.d/su lineinfile: path: /etc/pam.d/su regexp: ‘^#auth required pam_wheel.so use_uid’ line: ‘auth required pam_wheel.so use_uid’ - name: Update /etc/pam.d/su-l lineinfile: path: /etc/pam.d/su-l regexp: ‘^#auth required pam_wheel.so use_uid’ line: ‘auth required pam_wheel.so use_uid’
Restrict access to kernel log
The output of dmesg can provide useful information to attackers and hence one needs to restrict access to it. The ansible/files/ folder has both 50-dmesg-restrict.conf and 50-kptr-restrict.conf files, which need to be copied to etc/sysctl.d for the above functionality.
$ cat files/50-kptr-restrict.conf kernel.kptr_restrict = 1 $ cat files/50-dmesg-restrict.conf kernel.dmesg_restrict = 1 - name: Restrict access to kernel logs copy: src: ../../files/50-dmesg-restrict.conf dest: /etc/sysctl.d/50-dmesg-restrict.conf owner: root group: root mode: 0644 - name: Restrict access to kernel pointers in procfs copy: src: ../../files/50-kptr-restrict.conf dest: /etc/sysctl.d/50-kptr-restrict.conf owner: root group: root mode: 0644
Disable root login
The root SSH login should be disabled, by default. A remote user should only be able to log in to the system, and sudo if they have been given permissions. The same can be accomplished by setting PermitRootlogin no in /etc/ssh/sshd_config as shown below:
- name: Disable root login lineinfile: path: /etc/ssh/sshd_config regexp: ‘^#PermitRootLogin’ line: ‘PermitRootLogin no’
The console logins for root can also be disabled by commenting them out in /etc/securetty file.
- name: Disable root console logins replace: path: /etc/securetty regexp: ‘^(?!#)(.*)’ replace: ‘# \1’
Execution
The entire playbook is provided below for reference:
--- - name: Harden Parabola hosts: parabola become: yes become_method: sudo gather_facts: yes tags: [security] tasks: - name: Install pwgen package: name: pwgen state: latest - name: File permissions for /etc/shadow stat: path: /etc/shadow register: shadow - assert: that: - “shadow.stat.pw_name == ‘root’” - “shadow.stat.readable == true” - “shadow.stat.rgrp == false” - “shadow.stat.roth == false” - “shadow.stat.writeable == true” - “shadow.stat.wgrp == false” - “shadow.stat.woth == false” - “shadow.stat.xgrp == false” - “shadow.stat.xoth == false” - “shadow.stat.xusr == false” - name: File permissions for /etc/passwd stat: path: /etc/passwd register: passwd - assert: that: - “passwd.stat.pw_name == ‘root’” - “passwd.stat.readable == true” - “passwd.stat.rgrp == true” - “passwd.stat.roth == true” - “passwd.stat.writeable == true” - “passwd.stat.wgrp == false” - “passwd.stat.woth == false” - “passwd.stat.xgrp == false” - “passwd.stat.xoth == false” - “passwd.stat.xusr == false” - name: Update /etc/pam.d/passwd lineinfile: path: /etc/pam.d/passwd insertbefore: ‘^password.*required.*pam_unix.so’ line: ‘password required pam_cracklib.so retry=2 minlen=10 difok=6 dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1’ - name: Check disk is encrypted shell: lsblk /dev/sda | grep / | grep crypt register: encrypted - assert: that: - “encrypted.rc == 0” - name: Check /tmp shell: mount | grep /tmp | grep -E ‘nosuid.*nodev’ register: tmp - assert: that: - “tmp.rc == 0” - name: Check /dev/shm shell: mount | grep /dev/shm | grep -E ‘nosuid.*nodev.*noexec’ register: tmp - assert: that: - “tmp.rc == 0” - name: Change file mode for /boot, /etc/iptables file: path: “{{ item }}” mode: 0700 with_items: - /boot - /etc/iptables - name: Update /etc/pam.d/system-login lineinfile: path: /etc/pam.d/system-login regexp: ‘^auth.*required.*pam_tally.so’ line: ‘auth required pam_tally.so deny=2 unlock_time=600 onerr=succeed file=/var/log/faillog’ - name: Update /etc/security/limits.conf lineinfile: path: /etc/security/limits.conf insertbefore: ‘# End of file’ line: “{{ item }}” with_items: - ‘* soft nproc 100’ - ‘* hard nproc 200’ - name: Update /etc/pam.d/su lineinfile: path: /etc/pam.d/su regexp: ‘^#auth required pam_wheel.so use_uid’ line: ‘auth required pam_wheel.so use_uid’ - name: Update /etc/pam.d/su-l lineinfile: path: /etc/pam.d/su-l regexp: ‘^#auth required pam_wheel.so use_uid’ line: ‘auth required pam_wheel.so use_uid’ - name: Restrict access to kernel logs copy: src: ../../files/50-dmesg-restrict.conf dest: /etc/sysctl.d/50-dmesg-restrict.conf owner: root group: root mode: 0644 - name: Restrict access to kernel pointers in procfs copy: src: ../../files/50-kptr-restrict.conf dest: /etc/sysctl.d/50-kptr-restrict.conf owner: root group: root mode: 0644 - name: Disable root login lineinfile: path: /etc/ssh/sshd_config regexp: ‘^#PermitRootLogin’ line: ‘PermitRootLogin no’ - name: Disable root console logins replace: path: /etc/securetty regexp: ‘^(?!#)(.*)’ replace: ‘# \1’
You can invoke the above playbook using:
$ ansible-playbook -i inventory/kvm/inventory playbooks/configuration/parabola.yml -vv -K
The -vv represents the verbosity in the Ansible output. You can use up to four v’s for a more detailed output. The -K option prompts for the sudo password for the guest Parabola user account.
You are encouraged to read the security guide and best practices for Parabola GNU/Linux-libre available at https://wiki.parabola.nu/Security for more information.