35

I am setting up a MySQL server and want Ansible to set the mysql-root password during installation.

With the help of the internet I came up with this solution:

- name: Set MySQL root password before installing
  debconf: name='mysql-server' question='mysql-server/root_password' value='{{mysql_root_pwd | quote}}' vtype='password'
- name: Confirm MySQL root password before installing
  debconf: name='mysql-server' question='mysql-server/root_password_again' value='{{mysql_root_pwd | quote}}' vtype='password'
- name: Install Mysql
  apt: pkg=mysql-server state=latest

mysql_root_pwd is a variable loaded from the Ansible Vault. This runs fine, but now on the server there are many lines in the log:

Apr 10 14:39:59 servername ansible-debconf: Invoked with value=THEPASSWORD vtype=password question=mysql-server/root_password name=mysql-server unseen=None
Apr 10 14:39:59 servername ansible-debconf: Invoked with value=THEPASSWORD vtype=password question=mysql-server/root_password_again name=mysql-server unseen=None

How can I stop Ansible from writing clear text passwords to the logfiles?

Mxx
  • 2,362
  • 2
  • 28
  • 40
claus
  • 571
  • 1
  • 4
  • 11

9 Answers9

50

To prevent a task with confidential information from being logged, in syslog or other, set no_log: true on the task:

- name: secret stuff
  command: "echo {{secret_root_password}} | sudo su -"
  no_log: true

The running of the task will still be logged, but with little details. Also, the module used has to support no_log, so test custom modules.

See Ansible FAQ for further details. It can be applied to an entire playbook, however the output gets a little nasty with "censored!" messages.

Murage
  • 107
  • 3
Bill Carlson
  • 501
  • 4
  • 3
10

The observed behaviour seems to be a bug in the debconf module. I filed a bug report.

The user bcoca at github pointed out that one can use the no_log: true directive in tasks, that set passwords, to prevent logging. This is a workaround, that works for me until the bug is fixed.

claus
  • 571
  • 1
  • 4
  • 11
  • I'm getting an error when i use that directive.. Any idea what i'm doing wrong? `ERROR: no_log is not a legal parameter in an Ansible task or handler` – Bouke Versteegh Jun 13 '15 at 13:06
  • 2
    Turns out I had an old version of ansible! To fix (on ubuntu): `sudo apt-add-repository ppa:ansible/ansible`, `sudo apt-get update`, `sudo apt-get install ansible` – Bouke Versteegh Jun 13 '15 at 13:12
  • Same problem for me but I can't make n_log: true to work as expected. My Ansible version is 1.7.2. What is yours ? – jmcollin92 Jun 28 '16 at 12:51
  • @jmcollin92 I currently use 2.1. There is a guide [here](http://docs.ansible.com/ansible/intro_installation.html#running-from-source) on how to install the latest version from source. I use that as ansible is still maturing. – claus Jun 30 '16 at 12:24
  • You probably also want to set `diff: no` – DylanYoung Dec 17 '19 at 17:52
5

There is a better way than just no_log: True

- name: write in string variables login and password
  set_fact:
    temp_user: "{{ USER_VAR }}"
    temp_pass: "{{ PASSWORD_VAR }}"


- name: Your operation with password in output
  shell: '/opt/hello.sh'
  ignore_errors: True
  no_log: True
  register: myregister

- debug:
    msg: '{{ myregister.stderr | regex_replace(temp_user) | regex_replace(temp_pass) }}'
  when: myregister.stderr != ""

- debug:
    msg: '{{ myregister.stdout | regex_replace(temp_user) | regex_replace(temp_pass) }}'
  when: myregister.stdout != ""

- fail:
    msg: "error shell /opt/hello.sh"
  when: myregister.stderr != ""

As you can see, you need to add:

ignore_errors: true
no_log: true

And then make the output of the result of the command with regex_replace, where:

USER_VAR - login variable

PASSWORD_VAR - password variable

With this approach, you will not only hide the passwords and logins, but also get the output of your operation

TheDESTROS
  • 385
  • 3
  • 10
1

As per Ansible docs:

log_path

If present and configured in ansible.cfg, Ansible will log information about executions at the designated location. Be sure the user running Ansible has permissions on the logfile:

log_path=/var/log/ansible.log 

This behavior is not on by default. Note that ansible will, without this setting, record module arguments called to the syslog of managed machines. Password arguments are excluded.

Sounds like setting log_path on your control node will result in not having logs on destination nodes.

chicks
  • 3,793
  • 10
  • 27
  • 36
Droopy4096
  • 680
  • 4
  • 8
  • Actually I have an ansible.cfg in my local dir, where I call ansible, setting the log_path. The local log is created alright and uptodate after a new run (logging works). This does not (even though the doc that you pointed out promises it) prevent the remote host from logging. Also the statement "Password arguments are excluded" seems to be not 100% true? Is this a bug (or even two)? – claus Apr 13 '15 at 10:33
  • 2
    @claus "password arguments excuded" only applies to modules where password argument is explicit. There is no way for ansible to know which argument would be password and which wouldn't with general commands like debconf, shell, raw, etc. – Droopy4096 Apr 13 '15 at 16:27
  • Please scroll to the right in my initial playbook. It says `vtype='password'` . That should be explicit enough IMHO? I assume that the log message is also created by the debconf module. – claus Jun 30 '16 at 12:23
  • This is incorrect. The documentation should more accurately say "Note that ansible will, **regardless of this setting**, record module arguments called to the syslog of managed machines." – augurar Sep 14 '16 at 17:34
1

I solved by upgrading Ansible version to 1.6.1

sudo pip install ansible==1.6.1
0x3bfc
  • 129
  • 2
1

This is an additon to the answer of TheDESTROS from this thread:

  1. write a template, which wraps the command with a secret:

wrapper-script.sh.j2

echo {{ secret_eg_from_ansible_vault }} | su - "ls -l"
  1. Invoke the wrapper script and remove it at once:
- name: create template
  template:
    src: wrapper-script.sh.j2
    dest: /tmp/wrapper-script.sh
    mode: 0700
  no_log: True
- name: invoke command with secret and remove it
  shell: /tmp/wrapper-script.sh; rm -f /tmp/wrapper-script.sh

You need a little bit less code and can thee stdout of the commands in your logs. There is only one caveeat, if a secret is in the commands stdout. If you want to avoid the external template, the copy module with the parameter content might help to write a small wrapper script in the fly.

  • How will it hide passwords? when I run wsadmin.bat for WebSphere, the output will still be. By the same token, it’s quite not obvious what is better - fewer lines of code or less understanding of what is done in it. When you create a "template" file and execute something from it, it is absolutely not clear why this was done if the code is being watched by another person. – TheDESTROS Mar 04 '20 at 09:17
  • In addition, the password file will be hosted on the server, which, to some extent, may be unacceptable. – TheDESTROS Mar 04 '20 at 09:19
1

The no_log: true approach is to be used as last-resort if other attempts fail because it will make the task execution totally opaque and you will have no clue when it fail.

Security practices recommend giving credentials from stdin or when not possible using credential files (or even executables).

Here is an example on how to perform a secure podman login by avoiding exposing the password:

- name: secured login
  become: true
  command: >
    podman login --username={{ user }} --password-stdin ...
  args:
    stdin: "{{ secret }}"
  register: result

With this, the secret will not be exposed, in result but you will still be able to see the output of the command.

Most of tools needing login do implement one of the more secured approaches mentioned. Using credentials on CLI in code is like have 123456 as your bank password.

sorin
  • 8,016
  • 24
  • 79
  • 103
  • I did this, using stdin to pipe the secret into the command. I could see the secret in the logs on failure! This actually was WHY I came here in the first place – anthony Feb 22 '23 at 01:40
0

The environment argument in the playbook is suitable for keeping a secret hidden. The secret is not printed even with -vvvv. It is available for the whole play.

- hosts: control_host
  gather_facts: no
  become: no
  environment:
    PASSWORD: "{{ password }}"
  tasks:

   - local_action: "shell ./foobar.sh ${PASSWORD}"

   - local_action: "shell echo ${PASSWORD}"

output:

ansible-playbook -i ... playbooks/demo.yml -v

PLAY [control_host] *********************************************************************************

TASK [shell] *********************************************************************************
changed: [localhost -> localhost] => {
    "changed": true,
    "cmd": "./foobar.sh ${PASSWORD}",
    "delta": "0:00:00.013467",
    "end": "2020-04-03 17:42:03.950534",
    "rc": 0,
    "start": "2020-04-03 17:42:03.937067"
}

TASK [shell] *********************************************************************************
changed: [localhost -> localhost] => {
    "changed": true,
    "cmd": "echo ${PASSWORD}",
    "delta": "0:00:00.005925",
    "end": "2020-04-03 17:42:04.319085",
    "rc": 0,
    "start": "2020-04-03 17:42:04.313160"
}

STDOUT:

my_secret_password
selle
  • 101
  • 1
  • 1
    Unfortunately, as of Ansible version 2.9.6 this approach is only safe up to -vv (higher verbosities disclose the arguments passed to the wrapper script over ssh) – DomQ Aug 14 '20 at 09:16
0

As I needed something similar, I created a test.yml playbook that hides things coming from a .secret file.

--- 

- hosts: localhost

  vars: 
    hello_var: "{{ lookup('ini', 'hello_var type=properties file=config.secret') }}"
    output_file: "/test.txt"

    dirty_lines: ['cmd']
    dirty_values: ['hello_env', 'hello_var']
    dirty_word: "**SECRET**"

  tasks:  
    - name: Write output file (has dirty content possibly, not showing output)
      shell: "echo \"{{ hello_var }}\" > {{ output_file }}"  
      register: output_raw
      ignore_errors: true       
      # don't forget
      no_log: true 

    #- debug:
    #    var: output_raw 

    - name: Read secrets for 'Write output file'
      shell: ansible-vault view --vault-password-file ./vault.secret ./config.secret
      register: ansible_secret_content
      # don't forget
      no_log: true 

    - name: Filter passwords from the secrets for 'Write output file'
      set_fact:
        secrets: "{{ secrets|default([]) + [ item.split('=')[1] ] }}"
      when: item.split('=')[0] in dirty_values
      with_items: "{{ ansible_secret_content.stdout_lines | list }} "
      # don't forget
      no_log: true 

    #- debug:
    #    var: secrets

    - name: Init cleaned output from 'Write output file'
      set_fact:
        output_cleaned: "{{ output_cleaned|default({}) | combine({item.key : item.value}) }}"
      when: item.key not in dirty_lines
      with_dict: "{{ output_raw }}"
      # don't forget
      no_log: true 

    #- debug:
    #    var: output_cleaned

    - name: Init dirty output from 'Write output file'
      set_fact:
        output_dirty: "{{ output_dirty|default({}) | combine({item.key : item.value}) }}"
      when: item.key in dirty_lines
      with_dict: "{{ output_raw }}"
      # don't forget
      no_log: true 

    #- debug:
    #    var: output_dirty

    - name: Clean the dirt for 'Write output file'
      set_fact:
        output_dirty: "{{ output_dirty|default({}) | combine({item[0] : output_dirty[item[0]].replace(item[1], dirty_word)}) }}"
      loop: "{{ dirty_lines | product(secrets) | list }}"
      # loops over the carthesian product of dirty_lines with dirty_values, providing a sort of nested loop
      # don't forget
      no_log: true 

    #- debug:
    #    var: output_dirty

    - name: Combine output for 'Write output file'
      set_fact:
        output: "{{ output|default({}) | combine(output_cleaned) | combine(output_dirty) }}"
      # don't forget
      no_log: true 

    - name: Cleaned output for 'Write output file' (will show as failed if the original command failed also)
      debug:
        var: output
      # Picking up the original error, if any
      failed_when:        
        output.stderr != ''     

#---

output:

PLAY [localhost] ***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Write output file (has dirty content possibly, not showing output)] ******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this result", "changed": true}
...ignoring

TASK [Read secrets for 'Write output file'] ************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
changed: [localhost]

TASK [Filter passwords from the secrets for 'Write output file'] ***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
ok: [localhost]

TASK [Init cleaned output from 'Write output file'] ****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
skipping: [localhost] => (item=None) 
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost]

TASK [Init dirty output from 'Write output file'] ******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost] => (item=None)
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
skipping: [localhost] => (item=None) 
ok: [localhost]

TASK [Clean the dirt for 'Write output file'] **********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost]

TASK [Combine output for 'Write output file'] **********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Cleaned output for 'Write output file' (will show as failed if the original command failed also)] ************************************************************************************************************************************************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {
    "failed_when_result": true,
    "output": {
        "changed": true,
        "cmd": "echo \"**SECRET**\" > /test.txt",
        "delta": "0:00:00.001262",
        "end": "2021-03-18 14:36:20.248332",
        "failed": true,
        "msg": "non-zero return code",
        "rc": 2,
        "start": "2021-03-18 14:36:20.247070",
        "stderr": "/bin/sh: 1: cannot create /test.txt: Permission denied",
        "stderr_lines": [
            "/bin/sh: 1: cannot create /test.txt: Permission denied"
        ],
        "stdout": "",
        "stdout_lines": []
    }
}

PLAY RECAP *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=8    changed=2    unreachable=0    failed=1    skipped=0    rescued=0    ignored=1