Ansible Pitfall: File Modes

tl;dr: To avoid unexpected behaviour, always quote your file modes in Ansible tasks and variables.

Ansible often exhibits unintuitive behaviour for people who are unfamiliar with the intricacies of YAML syntax and parsing, so today we’re going to explore some of those intricacies by setting a trap for the unwary:

- copy:
    src: /dev/null
    dest: /var/tmp/foo
    mode: 0755
$ ls -lh /var/tmp/foo
-rwxr-xr-x 1 root root 0 Nov 27 17:58 /var/tmp/foo

This is a perfectly valid Ansible task that sets the mode to exactly what you would expect. A week later your coworker comes along and copies your code, but wants their file to be setuid:

    - copy:
        src: /dev/null
        dest: /var/tmp/bar
        mode: 4755
$ ls -lh /var/tmp/bar
--w--w--wt 1 root root 0 Nov 27 17:58 /var/tmp/bar

Ansible will run this without falling over, but the state of their file is somewhat surprising. What went wrong?

YAML scalars can often be safely expressed without any quotes (e.g. src: /dev/null is exactly the same as src: '/dev/null') but there are multiple scalar types and quotes do sometimes matter. Unquoted numeric values are parsed as ints, while quoted ones are strings:

- debug:
    msg: 0755
- debug:
    msg: '0755'
- debug:
    msg: 4755
TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": 493
}

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "0755"
}

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": 4755
}

“Where did 493 come from?” you ask. While 0755 looks like a normal four-digit octal mode that you would pass to chmod, in YAML it’s actually a three-digit octal number with a format specifier so parsing converts this to its decimal equivalent. Going back to our example task with mode: 4755 we see that it’s parsed as decimal 4755, which is very different from the intended octal 4755.

“Fine,” you say, “I’ll just make a note in our consistently excellent documentation (which everyone reads and follows) that modes should always be notated in YAML as octal numbers, and advise my coworker to use mode: 04755. Problem solved!” And it is. For now. Then a week later you decide to install a second executable, and modify the task again:

- copy:
    src: /dev/null
    dest: /var/tmp/{{ item.0 }}
    mode: "{{ item.1 }}"
  loop:
    - [ bar, 04755 ]
    - [ baz, 04750 ]
$ ls -lh /var/tmp/ba[rz]
-r-xr-S--x 1 root root 0 Nov 27 17:58 /var/tmp/bar
-r-x-wsrw- 1 root root 0 Nov 27 18:03 /var/tmp/baz

Not only are you back to broken files, they’re not even the same broken as previously! This exciting new behaviour is due to the introduction of templating. Currently Jinja templating defaults to producing strings, so your mode is translated from octal to decimal then transformed into a string, and the various file modules assume that this numeric string is octal. 04755 becomes "2541", which can be parsed as an octal mode but its effect is very different from your intent.

Which brings us back to the recommendation: always quote your file modes so that you don’t have to care about any of this. Quoted file modes behave consistently, survive both parsing and templating intact, and don’t provide exciting traps for your future self.

- copy:
    src: /dev/null
    dest: /var/tmp/foo
    mode: '0755'
- copy:
    src: /dev/null
    dest: /var/tmp/{{ item.0 }}
    mode: "{{ item.1 }}"
  loop:
    - [ bar, '4755' ]
    - [ baz, '4750' ]