The Life Of The Ansible Data - Merging Dimensions
Tasks.
Roles.
Templates.
Playbooks.
What are they without data? Without variables?
They can pop up from every direction.
It's the Ghost Operator's task to handle them.
Mission Briefing
Write a role: create new users on freshly orchestrated systems.
Requirements:
- Idempotence
- Sane defaults
- Only orchestration (no user management)
- Keep it simple
- Declare variables only once
Configured parameters:
Parameter | Mandatory? | Comment |
---|---|---|
Username | yes | Break playbook run if not provided |
Password | yes | Lock user with "!" or "*" (default), or set the password |
Full name (GECOS) | no | Defaults to the username |
Additional groups | no | List or defaults to [] |
Shell | no | Defaults to "/bin/bash" |
SSH public key | no |
One Ansible run must create and configure the new users on the system.
Data Structures - The First Model
Simplicity and only one declaration.
The first data-model, a users.yml
file for host_vars
or group_vars
:
users: deadswitch: full_name: "DeadSwitch" shell: "/bin/bash" ssh_key: "..." password: "$6$secret hash" groups: - ansible - operator
An Operator can loop the data:
- name: Orchestrate the system users ansible.builtin.user: name: " {{ item.key }} " comment: " {{ item.value.full_name | default(item.key) }} " append: yes create_home: yes groups: " {{ item.value.groups | default([]) }} " shell: " {{ item.value.shell | default('/bin/bash') }} " password: " {{ item.value.password | default('*') }} " update_password: on_create state: present loop: " {{ users | dict2items }} " - name: Deploy the SSH public keys ansible.builtin.authorized_key: user: " {{ item.key }} " key: " {{ item.value.ssh_key }} " when: item.value.ssh_key is defined loop: " {{ users | dict2items }} "
The Ghost is in the details. Ghost Operators see it immediately.
Strengths:
- One, manageable data structure.
- The playbook can scale.
- Sane defaults are set.
Blocker:
- The password hash is exposed in plain text.
This data-model doesn't survive the wild.
Protecting The Sensitive Data - The Second Model
The same structure - with a clinical cut.
- The
Orchestrate the system users
task loops the current data flawlessly. - The
password
field is the risk. - Encrypting everything with Ansible Vault is heavy. Too heavy.
You must remove the password
field from the data structure:
users: deadswitch: full_name: "DeadSwitch" shell: "/bin/bash" ssh_key: "..." groups: - ansible - operator
The task scales. Leave it untouched.
A new task would break the update_password: on_create
logic.
Where do you put the password?
In an Ansible Vault encrypted new file: vault.yml
vault_users: deadswitch: password: "$6$secret hash"
One user - two dimensions:
- General data in the
users.yml
. - Secret data in the
vault.yml
.
A Ghost Operator can merge the dimensions:
- name: Merge users with vault_users ansible.builtin.set_fact: merged_users: "{{ users | ansible.builtin.combine(vault_users | default({}), recursive=True) }}" - name: Orchestrate the system users ansible.builtin.user: name: " {{ item.key }} " comment: " {{ item.value.full_name | default(item.key) }} " append: yes create_home: yes groups: " {{ item.value.groups | default([]) }} " shell: " {{ item.value.shell | default('/bin/bash') }} " password: " {{ item.value.password | default('*') }} " update_password: on_create state: present loop: " {{ merged_users | dict2items }} "
Sharp.
Surgical.
Clean.
General info comes from one direction, the secrets come from another dimension.
No data exposure. No human-readable secrets in the variables.
The Final Dimension
Ansible still bleeds: variables leak.
On the screen. In the ansible.log
file.
No secret data shall be visible:
The no_log: yes
option for the task will prevent to log the password hash on the screen.
Yet, the merged_users
dictionary will persist in the facts.
A solution to merge the dimensions in-place in the task:
- name: Orchestrate the system users ansible.builtin.user: name: " {{ item.key }} " comment: " {{ item.value.full_name | default(item.key) }} " append: yes create_home: yes groups: " {{ item.value.groups | default([]) }} " shell: " {{ item.value.shell | default('/bin/bash') }} " password: " {{ item.value.password | default('*') }} " update_password: on_create state: present no_log: yes loop: "{{ users | ansible.builtin.combine(vault_users | default({}), recursive=True) | dict2items }}"
The system obeys.
No leaks. No echoes.
Only execution.