The Life Of The Ansible Data - Merging Dimensions

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:

  1. One, manageable data structure.
  2. The playbook can scale.
  3. Sane defaults are set.

Blocker:

  1. 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.