We will see today how to use an Ansible callback plugin to hide sensitive data from ansible-playbook output.
Example playbook
Let’s say you need to create some MySQL databases with ansible, you will propably write a playbook like the one below, and protect your passwords with ansible-vault
:
---
- hosts: mysql-server
gather_facts: no
become: true
vars:
databases:
- name: foo
password: !vault |
$ANSIBLE_VAULT;1.2;AES256;main
37666232623836363831383162313333376436616662396334313033626663383239366266323334
3665363939326132333261633165326561313262613634320a363036646533333530663434303764
33666138343666363832303930653031646265366165656564646166393736326435356666366139
3362363331396464370a373437306232353039323861303931386630316533323538336166353332
65313538623561353461383539303265323336636465396264326230623663386439
- name: bar
password: !vault |
$ANSIBLE_VAULT;1.2;AES256;main
35343463336563396665616163623264633163326564323435613837323164633661623038383464
6334323632653862623836363165316339623465633362610a366630386164656363626237643234
34336339633966663830356562316536663138356331626265313731303134313733636363366437
6236623665663430300a663866646536363764623332373261623134383530643634373632343962
32343837316534373561303062656332666433366366353361613339656234653537
- name: toto
password: !vault |
$ANSIBLE_VAULT;1.2;AES256;main
37396563636435616461313331323463656239383266383766353363326135306133646635326363
3039353630306265633838333438386136386635653433650a363762386462633832666361346161
36343938616464663661623632656634363762366434373435323765656135666138333836303937
6135303330386264340a356235633433373165316465643938666135646564396665663766616539
31383238363761326139336365383639323663333235356664643232353438353766
- name: tata
password: !vault |
$ANSIBLE_VAULT;1.2;AES256;main
65616638393834613334623633383233326465623863613531636463636636383532313538643832
3335333637363138663630663336333163326238323235610a343730666335346361633939333335
63306165323239636530366463626632613138666663373735626531386361303063613932373830
3136306435666131390a643434643836366135336662376538633861633637613663633962346565
34613966353462306134636537306636346662383932353332373636643633633061
tasks:
- name: Install MySQL packages
ansible.builtin.package:
name:
- mysql-server
- adminer
- python3-pymysql
- name: Setup adminer
ansible.builtin.file:
src: /etc/apache2/conf-available/adminer.conf
dest: /etc/apache2/conf-enabled/adminer.conf
owner: root
group: root
state: link
notify:
- Restart apache
- name: Create databases
community.mysql.mysql_db:
name: "{{ item.name }}"
state: present
login_unix_socket: /run/mysqld/mysqld.sock
loop: "{{ databases }}"
- name: Create databases users
community.mysql.mysql_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
priv: "{{ item.name }}.*:ALL"
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
loop: "{{ databases }}"
handlers:
- name: Restart apache
ansible.builtin.service:
name: apache2
state: restarted
...
What’s the problem?
As you have several databases to create, you used a loop to invoke the ansible.builtin.mysql_db
module, and ansible is displaying your passwords in plain-text!
...
TASK [Create databases] *******************************************************
Saturday 02 September 2023 11:25:02 +0200 (0:00:00.083) 0:00:02.451 ****
ok: [mysql-server] => (item={'name': 'foo', 'password': 'my-vaulted-password'})
ok: [mysql-server] => (item={'name': 'bar', 'password': 'my-vaulted-password'})
ok: [mysql-server] => (item={'name': 'toto', 'password': 'my-vaulted-password'})
ok: [mysql-server] => (item={'name': 'tata', 'password': 'my-vaulted-password'})
TASK [Create databases users] *************************************************
Saturday 02 September 2023 11:25:02 +0200 (0:00:00.285) 0:00:02.736 ****
ok: [mysql-server] => (item={'name': 'foo', 'password': 'my-vaulted-password'})
ok: [mysql-server] => (item={'name': 'bar', 'password': 'my-vaulted-password'})
ok: [mysql-server] => (item={'name': 'toto', 'password': 'my-vaulted-password'})
ok: [mysql-server] => (item={'name': 'tata', 'password': 'my-vaulted-password'})
...
How to solve this?
With no_log parameter
Usually, ansible folks solve this issue by adding no_log: true
to the ansible tasks. But it hides the whole output by replacing the item content with None
value:
TASK [Create databases] **********************************************************************************************************************************************************************
Saturday 02 September 2023 11:42:53 +0200 (0:00:00.083) 0:00:02.489 ****
ok: [mysql-server] => (item=None)
ok: [mysql-server] => (item=None)
ok: [mysql-server] => (item=None)
ok: [mysql-server] => (item=None)
ok: [mysql-server]
In case of an error, how will you guess which database has failed?
With callback plugin
With a custom ansible callback plugin you can obfuscate only password:
TASK [Create databases] **********************************************************************************************************************************************************************
Saturday 02 September 2023 11:50:36 +0200 (0:00:00.085) 0:00:02.499 ****
ok: [mysql-server] => (item={'name': 'foo', 'password': '********'})
ok: [mysql-server] => (item={'name': 'bar', 'password': '********'})
ok: [mysql-server] => (item={'name': 'toto', 'password': '********'})
ok: [mysql-server] => (item={'name': 'tata', 'password': '********'})
Show me how to use this callback plugin!
In your ansible project, create a plugins/callback
folder and put this protect_data.py
file:
from ansible_collections.community.general.plugins.callback.yaml import CallbackModule as CallbackModule_default
import collections
DOCUMENTATION = """
author: Nelson G. & Jean-Christophe Vassort <[email protected]>
name: protect_data
type: stdout
short_description: hide sensitive data such as passwords from screen output
description:
- hide passwords from screen output
- https://serverfault.com/questions/754860/how-can-i-reduce-the-verbosity-of-certain-ansible-tasks-to-not-leak-passwords-in/809509#809509
extends_documentation_fragment:
- default_callback
requirements:
- set as stdout in configuration
options:
sensitive_keywords:
description:
- a list of sensitive keywords to hide separated with a comma
type: str
env:
- name: PROTECT_DATA_SENSITIVE_KEYWORDS
ini:
- section: callback_protect_data
key: sensitive_keywords
default: vault,pwd,pass
"""
EXAMPLES = r'''
ansible.cfg: >
# Enable plugin
[defaults]
stdout_callback = protect_data
[callback_protect_data]
sensitive_keywords = vault,pwd,pass
'''
class CallbackModule(CallbackModule_default):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = "stdout"
CALLBACK_NAME = "protect_data"
def __init__(self):
super(CallbackModule, self).__init__()
self.sensitive_keywords = ''
def set_options(self, task_keys=None, var_options=None, direct=None):
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.sensitive_keywords = self.get_option('sensitive_keywords').split(',')
# Debug info
#print(self.sensitive_keywords)
def v2_runner_on_start(self, host, task):
return True
def _get_item_label(self, result):
"""retrieves the value to be displayed as a label for an item entry from a result object"""
result = self.hide_password(result)
if result.get("_ansible_no_log", False):
item = "(censored due to no_log)"
else:
item = result.get("_ansible_item_label", result.get("item"))
return item
def hide_password(self, result):
ret = {}
for key, value in result.items():
sensitive_content = False
if isinstance(value, (collections.ChainMap, dict)):
ret[key] = self.hide_password(value)
else:
# Each variable containing sensitive_keywords will be hidden from output
for sensitive_keyword in self.sensitive_keywords:
if sensitive_keyword in key:
ret[key] = "********"
sensitive_content = True
if not sensitive_content:
ret[key] = value
return ret
def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=False):
return super(CallbackModule, self)._dump_results(
self.hide_password(result), indent, sort_keys, keep_invocation
)
And enable it in your ansible.cfg
:
[defaults]
callback_plugins = plugins/callback
stdout_callback = protect_data
[callback_protect_data]
sensitive_keywords = vault,pwd,pass
Customize variables you want to hide
By default the plugin will obfuscate variables containing vault
, pwd
or pass
(my_vault
or passwd
will be obfuscated too).
If you want to hide let’s say any variable containing key
, you can customize the sensitive_keywords
parameter from the [callback_protect_data]
in your ansible.cfg configuration file:
[callback_protect_data]
sensitive_keywords = vault,pwd,pass,key
Credits
Thanks to this serverfault page: