lineinfile be damned

The Ansible lineinfile module is designed to search a file for a line, and ensure that it is present or absent. lineinfile is very effective at that particular task. However, when the line has to be in a certain place or before or after a certain line, lineinfile becomes a hassle to manage.

Most people on IRC (#ansible) tend to agree, lineinfile is not a very good module in practice. Even Brain Coca says to avoid the lineinfile module. But, there is one use case I have found where line in file really excels.

If you are not using the Mozilla SSL Configuration Generator you are really missing out. It is a great tool that is super easy to use to maintain recommended SSL/TLS configurations on all your HTTP endpoints.

We recently had a task to get our TLS configurations up to the latest recommended guidance as well as make those configurations as consistent as possible. But this meant we had to touch over 900 different customer impacting Apache configurations.

Ansible to the rescue! But making and deploying templated configurations was going to be far too time-consuming to meet the self-imposed deadline we had set. This is the first time in my 2+ years of working with Ansible where I have come across a good use case for the lineinfile module.

There are only a handful of lines in the Apache configs that we needed to worry about two of which were already present and two of which that needed to be added:

  • SSLProtocol (present)
  • SSLCipherSuite (present)
  • SSLHonorCipherOrder (absent)
  • Header always set Strict-Transport-Security (absent)

I should also add that we have some configurations for endpoints that do not require encryption. Not only did we have to find and replace and add lines to the configs but we had to identify configs that only had configurations for port 443. Finding the files with the port 443 configurations is easy enough with the Ansible find module.

Here is the Ansible Playbook:

---
- hosts: webserver
  gather_facts: no
  tasks:
    - name: Find all the conf files
      find:
        paths="/etc/httpd/conf.d"
        patterns="*.conf"
        file_type=file
        contains=''
      register: conf_files
      always_run: yes
      tags: find

    - name: Edit SSLProtocol
      lineinfile:
        dest={{ item.path }}
        regexp="[ \t]SSLProtocol"
        line="\tSSLProtocol\t\tall -SSLv2 -SSLv3"
        insertafter="^[ \t]SSLEngine on"
      with_items: "{{ conf_files.files }}"

    - name: Edit SSLCipherSuite
      lineinfile:
        dest={{ item.path }}
        regexp="[ \t]SSLCipherSuite"
        line="\tSSLCipherSuite\t\tECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"
      with_items: "{{ conf_files.files }}"

    - name: Edit SSLHonorCipherOrder
      lineinfile:
        dest="{{ item.path }}"
        regexp="[ \t]SSLHonorCipherOrder[ \t]"
        line="\tSSLHonorCipherOrder\t\ton"
        insertafter="[ \t]SSLCipherSuite[ \t]"
      with_items: "{{ conf_files.files }}"

    - name: HSTS
      lineinfile:
        dest="{{ item.path }}"
        regexp="[ \t]Header always set Strict-Transport-Security[ \t]"
        insertbefore="</VirtualHost>"
        line='\tHeader always set Strict-Transport-Security "max-age=15768000"'
      with_items: "{{ conf_files.files }}"

There are a few bits I would like to expound on:

The Ansible find module has some unique return values; one of these values is files. The register of conf_files is all the output of the find module. The item {{ conf_files.files }} is the files array from the find module output. The {{ item.path }} is the path object from the files array. It is a little wonky but it grabs the full path for all files matching the options given to the find module which is exactly what we need.

The Ansible lineinfile module usage in this playbook is intense; insertafter, insertbefore, and regexp are used in every step. The matching of spaces and/or tabs is handled by the [ \t] regex throughout the playbook. The regexp option in the lineinfile module makes sure lines that match the expression are replaced. The insertafter and insertbefore options allow for putting the lines where they need to be.

This playbook took a while to run but once it was done we had consistent, well configured Apache configurations that went from F's to A's on the Qualys SSL Server Test. Give it a try and let me know how it works out for you.

UPDATE: Thanks to pferde on Hacker News for pointing out a flaw in my regex in insertafter for the Edit SSLProtocol task. It has been corrected.

Chris Short

Chris Short

https://chrisshort.net

View Comments