Provide feedback
Saved searches
Use saved searches to filter your results more quickly
Sign up
- Context
- Prerequisites
- Setup Ansible managed node using Docker Machine
- Clone Ansible Vault example
- Encountered issues
- Issue #1: Executable bit
- Issue #2: Line endings
- Ansible Vault commands
- Run Ansible via Docker container
- Resources
Context
After having successfully run Ansible on Windows using Docker, as documented inside my previous post, I thought about documenting how to use Ansible Vault on Windows.
This tool was included in Ansible since version 1.5 and its purpose is to ensure sensitive data like credentials, private keys, certificates, etc., used by Ansible playbooks, are stored encrypted.
This post will present my approach for running Ansible Vault on Windows using Docker, along with the issues I have encountered and their fixes.
As a real life example of when to use Ansible Vault, I have chosen the task of running a Docker container inside a virtual machine:
- Create the VM
- I’ll use Docker Machine to create a VM using Hyper-V driver; this approach has the added benefit of creating a VM which already has Docker installed
- Beside having access to a Docker host with minimum medium effort, I ended up tinkering with a new Linux distro, other than what I’m usually exposed to (Ubuntu and CentOS)
- Setup the VM to be managed by Ansible
- Provide SSH access — already done, since Docker Machine will handle it while creating the VM
- Provide a working Python version — as you’ll see below, this step is not difficult at all
- Clone a git repository from GitHub containing the Ansible playbook used for running the Docker container based on the hello-world image
- Add the Docker Hub credentials ued for pulling the image inside the appropriate Ansible variable YAML file
- Run Ansible Vault from a Docker container to encrypt these credentials
- Run the Ansible playbook used for pulling the Docker image, run the container, then remove them both
I will use satrapu/ansible-alpine-apk Docker image for running both Ansible and Ansible Vault on Windows.
All the Docker and Docker Machine related commands below must be executed inside a Powershell console run as admin (use Git Bash as a backup for some commands — e.g. “docker-machine ssh”).
Prerequisites
All versions below are the latest at the time of writing this particular section (March 26th, 2018).
- Windows 10 Professional Edition (v1709)
- Hyper-V
- Docker for Windows — I’ve recently upgraded to v18.03.0-ce, but older versions should be good enough
- Docker Machine — v0.13.0 or older, since v0.14.0 (coming with Docker for Windows v18.0.3.0-ce) is unable to create VMs using hyperv driver — see more details here
- Right after I’ve upgraded Docker for Windows from 17.12.1-ce to v18.03.0-ce, I was no longer able to create VMs using Docker Machine and hyperv driver; this issue did not occur when using v0.13.0!
- Download Docker Machine v0.13.0 from GitHub, rename it to docker-machine.exe and then move it inside %DOCKER_HOME%\resources\bin to overwrite the existing docker-machine.exe (v0.14.0)
- Visual Studio Code — v1.21.1
- Any other editor capable of switching line endings between CRLF and LF should be fine too — see below for the actual motivation behind this prerequisite
- Any other editor capable of switching line endings between CRLF and LF should be fine too — see below for the actual motivation behind this prerequisite
- Git — v2.16.2
- The version is not that important, but installing Git Bash along with Git is!
Setup Ansible managed node using Docker Machine
- Create a virtual network switch named ansible, as described here
- Create a Hyper-V virtual machine named ansible-vault having 2 CPUs, 2048 MB RAM, 10 GB disk and attached to the previously created external virtual switch
- The boot2docker ISO URL is explicitly set to fixate the Docker version (v18.03.0-ce) for repeatability purposes
- Prepare to wait for a rather long period of time (10 minutes or more) for the VM to be created
- Ignore the SSH reported error
docker-machine create `
--driver hyperv `
--hyperv-cpu-count 2 `
--hyperv-memory 2048 `
--hyperv-disk-size 10240 `
--hyperv-virtual-switch "ansible" `
--hyperv-boot2docker-url https://github.com/boot2docker/boot2docker/releases/download/v18.03.0-ce/boot2docker.iso `
ansible-vault
# Running pre-create checks...
# (ansible-vault) Boot2Docker URL was explicitly set to "https://github.com/boot2docker/boot2docker/releases/download/v18.03.0-ce/boot2docker.iso" at create time, so Docker Machine cannot upgrade this machine to the latest version.
# Creating machine...
# (ansible-vault) Boot2Docker URL was explicitly set to "https://github.com/boot2docker/boot2docker/releases/download/v18.03.0-ce/boot2docker.iso" at create time, so Docker Machine cannot upgrade this machine to the latest version.
# (ansible-vault) Downloading C:\Users\admin\.docker\machine\cache\boot2docker.iso from https://github.com/boot2docker/boot2docker/releases/download/v18.03.0-ce/boot2docker.iso...
# (ansible-vault) 0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%
# (ansible-vault) Creating SSH key...
# (ansible-vault) Creating VM...
# (ansible-vault) Using switch "ansible"
# (ansible-vault) Creating VHD
# (ansible-vault) Starting VM...
# (ansible-vault) Waiting for host to start...
# Waiting for machine to be running, this may take a few minutes...
# Detecting operating system of created instance...
# Waiting for SSH to be available...
# Error creating machine: Error detecting OS: Too many retries waiting for SSH to be available. Last error: Maximum number of retries (60) exceeded
- Check that the VM is running (look for “STATE Running”):
docker-machine ls
# NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
# ansible-vault - hyperv Running tcp://192.168.1.168:2376 Unknown Unable to query docker version: Get https://192.168.1.168:2376/v1.15/version: x509: certificate signed by unknown authority
- Get the IPv4 address of the VM, since you’ll needed it inside the Ansible inventory file:
docker-machine ip ansible-vault
# 192.168.1.168
- Connect to the VM using SSH (see more here)
- In case you’re unable to enter the VM via SSH from a Powershell terminal, try using Git Bash run as admin — welcome to Windows!
docker-machine ssh ansible-vault
# ## .
# ## ## ## ==
# ## ## ## ## ## ===
# /"""""""""""""""""\___/ ===
# ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ / ===- ~~~
# \______ o __/
# \ \ __/
# \____\_______/
# _ _ ____ _ _
# | |__ ___ ___ | |_|___ \ __| | ___ ___| | _____ _ __
# | '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
# | |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ |
# |_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
# Boot2Docker version 18.03.0-ce, build HEAD : 404ee40 - Thu Mar 22 17:12:23 UTC 2018
# Docker version 18.03.0-ce, build 0520e24
- Install Python and Python setup tools on the VM, as they are needed by Ansible — check this StackOverflow article for instructions.
Keep in mind that all changes done to this machine will be lost after a restart, as documented here!
tce-load -wi python python-setuptools
# python.tcz.dep OK
# tk.tcz.dep OK
# readline.tcz.dep OK
# Downloading: libffi.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# python-setuptools.tcz.dep OK
# libffi.tcz 100% |*************************************************************************************************| 16384 0:00:00 ETA
# libffi.tcz: OK
# Downloading: expat2.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# expat2.tcz 100% |*************************************************************************************************| 73728 0:00:00 ETA
# expat2.tcz: OK
# Downloading: ncurses.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# ncurses.tcz 100% |*************************************************************************************************| 196k 0:00:00 ETA
# ncurses.tcz: OK
# Downloading: readline.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# readline.tcz 100% |*************************************************************************************************| 144k 0:00:00 ETA
# readline.tcz: OK
# Downloading: gdbm.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# gdbm.tcz 100% |*************************************************************************************************| 73728 0:00:00 ETA
# gdbm.tcz: OK
# Downloading: tcl.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# tcl.tcz 100% |*************************************************************************************************| 1128k 0:00:00 ETA
# tcl.tcz: OK
# Downloading: tk.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# tk.tcz 100% |*********************************************************************************************************************************************| 916k 0:00:00 ETA
# tk.tcz: OK
# Downloading: openssl.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# openssl.tcz 100% |*********************************************************************************************************************************************| 1500k 0:00:00 ETA
# openssl.tcz: OK
# Downloading: bzip2-lib.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# bzip2-lib.tcz 100% |*********************************************************************************************************************************************| 28672 0:00:00 ETA
# bzip2-lib.tcz: OK
# Downloading: sqlite3.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# sqlite3.tcz 100% |*********************************************************************************************************************************************| 388k 0:00:00 ETA
# sqlite3.tcz: OK
# Downloading: python.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# python.tcz 100% |*********************************************************************************************************************************************| 11820k 0:00:00 ETA
# python.tcz: OK
# Downloading: python-setuptools.tcz
# Connecting to repo.tinycorelinux.net (89.22.99.37:80)
# python-setuptools.tc 100% |*********************************************************************************************************************************************| 236k 0:00:00 ETA
# python-setuptools.tcz: OK
In case you forgot this step, when running Ansible playbook you’ll see something like this:
# PLAY [docker_hosts] ************************************************************
# TASK [Gathering Facts] *********************************************************
# fatal: [ansible_vault_example]: FAILED! => {"changed": false, "failed": true, "module_stderr": "", "module_stdout": "/bin/sh: /usr/local/bin/python: not found\r\n", "msg": "MODULE FAILURE", "rc": 0}
# to retry, use: --limit @/opt/ansible-playbooks/hello-world.retry
# PLAY RECAP *********************************************************************
# ansible_vault_example : ok=0 changed=0 unreachable=0 failed=1
- Display Python version
python --version
# Python 2.7.14
- Exit the VM:
Clone Ansible Vault example
- Clone the following git repository hosted on GitHub somewhere on your Windows machine (e.g. E:\Satrapu\Programming\Ansible\ansible-vault-on-windows):
cd E:/Satrapu/Programming/Ansible
git clone https://github.com/satrapu/ansible-vault-on-windows.git
This git repo is based on the classic Ansible folder structure, as documented here.
- Change the Ansible inventory file named local
- Set the value of the ansible_host property to the IP address of the ansible-vault VM (e.g. ansible_host=192.168.1.168)
- Please note property ansible_ssh_private_key_file has been set to “/opt/docker-machine/ansible-vault/id_rsa” value — the id_rsa represents a private key generated by Docker Machine while creating ansible-vault VM and which will be made available inside the Ansible Docker container via a Docker volume; this property should not be changed without fully understanding what else needs to be changed (see below)
- Create a file named vault_password under ../ansible-vault-password folder (outside Git repo!) and add a password (one line, no line ending)
- Since this file contains a password, it must not be put under source control, that’s why it should be created outside the Git repo
- To make it available inside Ansible Docker container, we’ll mount the containing folder as a Docker volume under path “/opt/ansible-vault-password”
- Example: “-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password”
- I have used https://strongpasswordgenerator.com to generate such password
- Click “Show Options” panel under the “Generate password” big green button to fine tune your password
- Replace the TBD placeholders from the /ansible-vault-on-windows/group_vars/docker_hosts/vault.yml file:
vault_docker_registry_url: TBD
vault_docker_registry_auth_username: TBD
vault_docker_registry_auth_password: TBD
vault_docker_registry_auth_email: TBD
with the appropriate values, like this:
vault_docker_registry_url: https://index.docker.io/v1/
vault_docker_registry_auth_username: some_user_name
vault_docker_registry_auth_password: P@zZwWwooRdddd
vault_docker_registry_auth_email: some_user_name@server.ro
This file should be put under source control once it has been encrypted.
For instance, the Docker Hub registry URL can be found via this command:
docker info | findstr Registry
# Registry: https://index.docker.io/v1/
In case you forgot to correctly update vault.yml file, when running Ansible playbook you should see something like this:
# PLAY [docker_hosts] ************************************************************
# TASK [Gathering Facts] *********************************************************
# ok: [ansible_vault_example]
# TASK [run_hello_world_container : Install pip] *********************************
# changed: [ansible_vault_example]
# TASK [run_hello_world_container : Install docker-py] ***************************
# changed: [ansible_vault_example]
# TASK [run_hello_world_container : Login into Docker registry TBD] **************
# fatal: [ansible_vault_example]: FAILED! => {"changed": false, "failed": true, "msg": "Parameter error: the email address appears to be incorrect. Expecting it to match /[^@]+@[^@]+\\.[^@]+/"}
# to retry, use: --limit @/opt/ansible-playbooks/hello-world.retry
# PLAY RECAP *********************************************************************
# ansible_vault_example : ok=3 changed=2 unreachable=0 failed=1
- You’ll see a vars.yml file under the same folder, /ansible-vault-on-windows/group_vars/docker_hosts:
docker_registry_url: "{{ vault_docker_registry_url }}"
docker_registry_auth_username: "{{ vault_docker_registry_auth_username }}"
docker_registry_auth_password: "{{ vault_docker_registry_auth_password }}"
docker_registry_auth_email: "{{ vault_docker_registry_auth_email }}"
Ansible will use the password residing inside the one-line file passed as the value of the –vault-password-file argument (e.g. –vault-password-file=/opt/ansible-vault-password/vault_password) to automatically decrypt the vault.yml file and will populate the above variables with the correct sensitive data, e.g. the user name and password used for pulling images from Docker Hub.
- After applying the aforementioned changes, the local git repo should look like this:
# Change drive letters and paths according to your local setup
E:; cd E:/Satrapu/Programming/Ansible/ansible-vault-on-windows; tree /F
# E:\SATRAPU\PROGRAMMING\ANSIBLE\ANSIBLE-VAULT-ON-WINDOWS
# │ .gitattributes
# │ .gitignore
# │ ansible.cfg
# │ hello-world.yml
# │ LICENSE
# │ local
# │ README.md
# │ vault_password_provider.py
# │
# ├───group_vars
# │ └───docker_hosts
# │ vars.yml
# │ vault.yml
# │
# └───roles
# └───run_hello_world_container
# ├───defaults
# │ main.yml
# │
# └───tasks
# main.yml
Encountered issues
Issue #1: Executable bit
Running Ansible Vault from a Docker container will fail since I’m trying to mount a Windows folder in a Linux container and all of its files will be mounted with all Linux permissions (read, write and execute):
docker container run `
--rm `
-v E:/Satrapu/Programming/Ansible/ansible-vault-on-windows:/opt/ansible-playbooks `
-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password `
satrapu/ansible-alpine-apk:2.4.1.0-r0 `
ansible-vault encrypt `
--vault-password-file=/opt/ansible-vault-password/vault_password `
./group_vars/docker_hosts/vault.ym
# [WARNING]: Error in vault password file loading (default): Problem running
# vault password script /opt/ansible-vault-password/vault_password ([Errno 8]
# Exec format error). If this is not a script, remove the executable bit from the
# file.
# ERROR! Problem running vault password script /opt/ansible-vault-password/vault_password ([Errno 8] Exec format error). If this is not a script, remove the executable bit from the file.
Here are the permissions found inside the Docker container:
docker container run `
--rm `
-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password `
satrapu/ansible-alpine-apk:2.4.1.0-r0 `
ls -al /opt/ansible-vault-password
# total 5
# drwxr-xr-x 2 root root 0 Mar 29 19:56 .
# drwxr-xr-x 1 root root 4096 Mar 29 20:03 ..
# -rwxr-xr-x 1 root root 100 Mar 24 19:20 vault_password
The above executable bit related error message is pretty clear, unfortunately, at the moment there is no easy way of mounting files without the execute bit, as stated here.
On the other hand, Ansible knows how to process a file with executable bit containing a Vault password if it is a Python script, as documented here, so the idea is to load the password via a Python script, which will be passed as the value of the –vault-password-file argument — see an example here.
At this moment I’m able to bypass the pesky Windows-Docker-folder-mounting issue, but this has lead me to the 2nd issue
Issue #2: Line endings
Ansible Vault being able to run a Python script which returns the password is great news, but keep in mind we’re still editing files on Windows, which uses CRLF as line ending, which, of course, will not work on Linux:
docker container run `
--rm `
-v E:/Satrapu/Programming/Ansible/ansible-vault-on-windows:/opt/ansible-playbooks `
-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password `
satrapu/ansible-alpine-apk:2.4.1.0-r0 `
ansible-vault encrypt `
--vault-password-file=./vault_password_provider.py `
./group_vars/docker_hosts/vault.yml
# [WARNING]: Error in vault password file loading (default): Problem running
# vault password script /opt/ansible-playbooks/vault_password_provider.py ([Errno
# 2] No such file or directory). If this is not a script, remove the executable
# bit from the file.
# ERROR! Problem running vault password script /opt/ansible-playbooks/vault_password_provider.py ([Errno 2] No such file or directory). If this is not a script, remove the executable bit from the file.
The fix is to edit vault_password_provider.py with an editor having line endings set for this file to “LF” instead of “CRLF” — see such setup for Visual Studio Code.
Ansible Vault commands
Having fixed the above 2 issues, the following Ansible Vault commands will work like a charm:
- Encrypt vault.yml:
docker container run `
--rm `
-v E:/Satrapu/Programming/Ansible/ansible-vault-on-windows:/opt/ansible-playbooks `
-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password `
satrapu/ansible-alpine-apk:2.4.1.0-r0 `
ansible-vault encrypt `
--vault-password-file=./vault_password_provider.py `
./group_vars/docker_hosts/vault.yml
- Decrypt vault.yml:
docker container run `
--rm `
-v E:/Satrapu/Programming/Ansible/ansible-vault-on-windows:/opt/ansible-playbooks `
-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password `
satrapu/ansible-alpine-apk:2.4.1.0-r0 `
ansible-vault decrypt `
--vault-password-file=./vault_password_provider.py `
./group_vars/docker_hosts/vault.yml
- View the decrypted vault.yml:
docker container run `
--rm `
-v E:/Satrapu/Programming/Ansible/ansible-vault-on-windows:/opt/ansible-playbooks `
-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password `
satrapu/ansible-alpine-apk:2.4.1.0-r0 `
ansible-vault view `
--vault-password-file=./vault_password_provider.py `
./group_vars/docker_hosts/vault.yml
# vault_docker_registry_url: https://index.docker.io/v1/
# vault_docker_registry_auth_username: xxxxxxx
# vault_docker_registry_auth_password: xxxxxxx
# vault_docker_registry_auth_email: xxxxxxx
Run Ansible via Docker container
- Run Ansible playbook:
# Replace <YOUR_ADMIN_USERS> placeholder with the Windows user name used for creating ansible-vault VM.
# Tip: Increase the verbosity of the ansible-playbook output by adding "-vvv" option at the end of the below line
docker container run `
--rm `
-v E:/Satrapu/Programming/Ansible/ansible-vault-on-windows:/opt/ansible-playbooks `
-v E:/Satrapu/Programming/Ansible/ansible-vault-password:/opt/ansible-vault-password `
-v C:/Users/<YOUR_ADMIN_USERS>/.docker/machine/machines/ansible-vault:/opt/docker-machine/ansible-vault `
satrapu/ansible-alpine-apk:2.4.1.0-r0 `
ansible-playbook `
--inventory-file=local `
--vault-password-file=./vault_password_provider.py `
hello-world.yml
# PLAY [docker_hosts] ************************************************************
# TASK [Gathering Facts] *********************************************************
# ok: [ansible_vault_example]
# TASK [run_hello_world_container : Install pip] *********************************
# ok: [ansible_vault_example]
# TASK [run_hello_world_container : Install docker-py] ***************************
# ok: [ansible_vault_example]
# TASK [run_hello_world_container : Login into Docker registry https://index.docker.io/v1/] ***
# changed: [ansible_vault_example]
# TASK [run_hello_world_container : Pull Docker image hello-world:linux] *********
# changed: [ansible_vault_example]
# TASK [run_hello_world_container : Logout from Docker registry https://index.docker.io/v1/] ***
# ok: [ansible_vault_example]
# TASK [run_hello_world_container : Run Docker container hello-world-from-satrapu] ***
# changed: [ansible_vault_example]
# TASK [run_hello_world_container : Remove Docker container hello-world-from-satrapu] ***
# changed: [ansible_vault_example]
# TASK [run_hello_world_container : Remove Docker image hello-world:linux] *******
# changed: [ansible_vault_example]
# PLAY RECAP *********************************************************************
# ansible_vault_example : ok=9 changed=5 unreachable=0 failed=0
Resources
- Docker Machine
- Docker Machine command-line reference
- boot2docker
- Tiny Core Linux
- Ansible modules
- easy_install
- pip
- docker_login
- docker_image
- docker_container
Welcome Back
Hey there! Glad to have you back for the second Ansible article. This time around, we’re diving into Ansible Vault and how to keep those Microsoft Windows passwords safe by encrypting them whilst they are at rest.
If you missed out on the last article regarding the setup of Ansible and handling some basic tasks on a non-domain joined Windows Server, make sure to catch up on that first, by following this link.
What is Ansible Vault
Ansible Vault is a feature that allows users to encrypt sensitive information, such as passwords and secret keys, within Ansible playbooks and files. This encryption ensures that the secrets are secure while they are at rest.
To encrypt a secret, you simply use the «ansible-vault encrypt» command followed by the name of the file or «ansible-vault encrypt_string ‘Secret'» followed by the name to be assigned to the secret. You’ll then be prompted to enter and confirm a password or passphrase. Once encrypted, the secret is stored in a format that is unreadable without the decryption key, providing a secure way to protect sensitive information within Ansible projects.
Ansible Vault uses AES symmetric encryption by using the same password or passphrase for both encryption and decryption.
Basic Commands
Below are a few fundamental commands for utilizing Ansible Vault:
Create an encrypted file
ansible-vault create newFile.yml
Encrypt an existing file
ansible-vault encrypt existingFile.yml
View encrypted content of a file
anisble-vault view existingFile.yml
Edit the encrypted file
ansible-vault edit existingFile.yml
Decrypt an encrypted file
ansible-vault decrypt existingFile.yml
Change the password that encrypts\decrypts the secret (Rekeying)
ansible-vault rekey existingFile.yml
Create an encrypted string
ansible-vault encrypt_string ‘ChangeMe1234’ —name ansible_password
Help Yourselves….
A working set of files deploying ansible-vault with encrypted secrets can be found at the following link, do help yourselves.
Set Nano as the Default Editor
To avoid ansible-vault opening new files with vi, let’s designate Nano as the default editor.
Type ‘select-editor‘ and then choose option 1
Let’s prove it works before Encrypting
I won’t immediately introduce encrypted passwords into the mix. Instead, we’ll set up and test the files using plain text passwords. Later, I’ll encrypt them, this will aid in troubleshooting.
Ansible Jinja2 is a templating engine used to create dynamic content within Ansible playbooks. It allows for the use of variables, conditionals, loops, and filters to customize configurations based on the environment or data. The ansible_password=»{{vault_ansible_password}}» is one such example and it’s used in the hosts.ini file and resolves to the values in win.yml.
If you have been following, Visual Code for Linux is installed, if not nano will suffice. First, navigate to the Ansible directory previously creating under the Documents directory and execute the following command:
mkdir win-encrypt
Change Directory (cd win-encrypt) into the directory and create the following 3 files, hosts.ini, ping.yml and win.yml. This will provide a simple ping test to the Windows Server on 10.1.1.1 with the Administrator account and a password of ‘ChangeMe1234’.
Ensure that ‘ping.yml’ adheres to the Yaml framework or a whole world of pain and ‘why aren’t you working’ will ensue.
The «no_log: true» parameter in Ansible is used to prevent sensitive data, such as passwords or API keys, from being displayed in the console output or logged to files. Including this now will make life difficult, waiting until your fully working.
hosts.ini
[win]
10.1.1.1
[win:vars]
ansible_user=administrator
ansible_connection=winrm
ansible_password=»{{vault_ansible_password}}»
ansible_winrm_scheme=https
ansible_port=5986
ansible_winrm_server_cert_validation=ignore
ansible_kerberos_delegation=false
ping.yml
—
— name: Ping win Test
hosts: win
gather_facts: false
vars_files:
— win.yml
tasks:
— name: Ping targets
win_ping:
no_log: True
win.yml
vault_ansible_password: ChangeMe1234
Execute the following command to test the use of the clear text password:
ansible-playbook -i hosts.ini ping.yml
Let’s get it Encrypted
Once we’ve confirmed the clear text password works, we can proceed to encrypt the win.yml file using the following command.
ansible-vault encrypt win.yml
Enter the password used for encrypting the file, I’m using the ultra-secure ‘Password1234’. In production don’t do this…..
Confirm the win.yml is encrypted with ‘cat win.yml‘. It should look something like the image below.
Type the following command to test accessing Windows using the encrypted vault file:
ansible-playbook -i host.ini ping.yml —ask-vault-pass
Enter the password ‘Password1234’ at the prompt.
Alternative Method to Encrypt the Password
Another way to encrypt the password is by utilizing the encrypt-string option.
Type the following command directing the output to winString.yml
ansible-vault encrypt-string ‘ChangeMe1234’ —name vault_ansible_password > winString.yml
I then renamed the existing win.yml and then renamed winString.yml to win.yml using the mv command.
This is a Bad Idea…….
Once we’ve secured the Windows passwords and grown weary of the password prompts or the playbooks are to be scheduled, we’ll embed the ansible-vault password into a plaintext file, undoing our previous efforts. I’ve rooted enough Linux boxes to know this is a bad idea. However, today is all about encrypting the Windows passwords whilst at rest.
Vault Password File
Here we go, create a file named ‘key’ in the root of the Ansible directory and enter the vault password of ‘Password1234’:
nano ../key
Secure the key file to allow the owner Read and Write access.
chmod 600 ../key
Execute the playbook swapping out —ask-vault-pass for —vault-password-file ../key.
ansible-playbook -i host.ini ping.yml —vault-password-file ../key
Alternatively, if you prefer not to use —vault-password-file, create an ansible.cfg file within the win-encrypt directory using Nano, and input the following details.
Run the playbook again without the vault password or by specifying the file location.
Final Thoughts
That wraps up this guide on employing ansible vault to secure Windows passwords while they’re at rest.
While Ansible Vault effectively secures Windows passwords, its effectiveness is compromised by storing the vault password in plain text. Despite its encryption capabilities, this vulnerability underscores the importance of implementing additional security measures to safeguard sensitive information effectively or another product in addition to ansible vault to manage secrets. Maybe that should be the aim of the next article, it’s that or ansible managing domain computers with Kerberos. Drop a comment and let me know?
Thank you for taking the time to read this article, your feedback, comments, and shares are immensely valued and deeply appreciated.
Overview
Ansible is an open source IT automation engine that automates provisioning, configuration management, application deployment, orchestration, and many other IT processes.
Ansible works by connecting to your nodes and pushing out small programs, called modules to them.
Idempotency — An operation is idempotent if the result of performing it once is exactly the same as the result of performing it repeatedly without any intervening actions.
Idempotency in Ansible Playbook — When a playbook is executed to configure a system, the system should always have the same, well defined state. If a playbook consists of 10 steps, and the system deviates in step 4 from the desired state, then only this particular step should be applied.
Usecases
- Infrastructure Provisioning
From traditional bare metal through to serverless or function-as-a-service, automating the provisioning of any infrastructure is the first step in automating the operational life cycle of your applications. Ansible can provision the latest cloud platforms, virtualized hosts and hypervisors, network devices and bare-metal servers.
- Configuration Management
It’s likely you currently manage your systems with a collection of scripts and ad-hoc practices curated by a talented team of administrators. Or perhaps you’re using an automation framework that requires a bit too much of your time to maintain. Virtualization and cloud technology have increased the complexity and the number of systems to manage is only growing.
You need a consistent, reliable and secure way to manage the environment — but many solutions have gone way too far the other direction, actually adding complexity to an already complicated problem. You need a system that builds on existing concepts you already understand and doesn’t require a large team of developers to maintain.
- Application Deployment
Ansible is the simplest way to deploy your applications. It gives you the power to deploy multi-tier applications reliably and consistently, all from one common framework. You can configure needed services as well as push application artifacts from one common system.
Rather than writing custom code to automate your systems, your team writes simple task descriptions that even the newest team member can understand on first read — saving not only up-front costs, but making it easier to react to change over time.
Ansible Terminologies
Control Node Ansible Server. Any machine with Ansible installed
Managed Node Ansible Client. The network devices (and/or servers) you manage with Ansible. Managed nodes are also sometimes called “hosts”.
Inventory A list of managed nodes. An inventory file is also sometimes called a “hostfile”
Playbook Ordered lists of tasks, saved so you can run those tasks in that order repeatedly
Module The units of code Ansible executes
Task The units of action in Ansible. You can execute a single task once with an ad-hoc command.
Roles Let you automatically load related vars_files, tasks, handlers, and other Ansible artifacts based on a known file structure. Once you group your content in roles, you can easily reuse them and share them with other users.
Ansible Vault encrypts variables and files so you can protect sensitive content such as passwords or keys rather than leaving it visible as plaintext in playbooks or roles.
Usage
Run adhoc commands:
ansible <host-pattern> -m <module-name>
#For example:
ansible localhost -m ping
Run a Playbook:
ansible-playbook -i <inventory-file> <playbook-name>
#For example:
ansible-playbook -i sample_inventory.yml sample_playbook.yml
A sample Playbook
---
- hosts: all
vars_files:
- ../.passwd.yml
vars:
ansible_user: user1
user1_passwd: "{{user1_passwd}}"
- name: Install notepadplusplus
win_command: npp.7.9.5.Installer.x64.exe /S
args:
creates: C:\Program Files (x86)\Notepad++\notepad++.exe
tags: npp
- name: Install firefox
win_shell: \"Firefox Setup 85.0.exe\" -ms -ma
args:
creates: C:\Program Files\Mozilla Firefox\firefox.exe
tags: firefox
- name: Add domain users to a Administrators group
win_group_membership:
name: Administrators
members:
- domain\user1
- domain\user2
state: present
tags: add_users
Setup Ansible Control Node (Server) & Managed Nodes (Clients)
Ansible is an agentless automation tool that you install on a control node. From the control node, Ansible manages machines and other devices remotely (by default, over the SSH protocol).
winrm (Windows Remote Management) protocol is used (instead of SSH) for connecting Windows Managed Node(s) from Ansible server.
When using ‘winrm’ protocol, there are multiple authentication types are available like, kerberos, CredSSP, NTLM etc. Here, I am using CredSSP authentication.
Please note, Ansible control node (server) cannot be setup on Windows.
Setup Ansible Control node (Server)
-
Install Ansible on a Linux server by referring to: Install Ansible
- Install additional python modules which are required for connecting Windows managed nodes:
pywinrm
&pywinrm[credssp]
pip install "pywinrm>=0.3.0" pip install pywinrm[credssp]
- Set below variables in host/inventory file or in the Playbook directly
ansible_user: <Username> ansible_password: <Password> ansible_connection: winrm ansible_winrm_transport: credssp
Setup Windows Managed nodes(clients)
- WinRM setup
WinRM service has to be configured on clients so that Ansible control node can connect to it.
There are two main components of the WinRM service that governs how Ansible can interface with the Windows host: the listener
and the service
configuration settings.
Run below powershell script (from client machine) to configure WinRM service
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$url = "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"
(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
powershell.exe -ExecutionPolicy ByPass -File $file
- Enable CredSSP
CredSSP authentication is not enabled by default on a Windows host, but can be enabled by
Enable-WSManCredSSP -Role Server -Force
Now, both Control Node & Managed node(s) are setup. You can run an Ansible ad-hoc command to test it. For example:
ansible localhost -m win_ping
What is Ansible Vault and How it is Setup & Used
Ansible Vault encrypts variables and files so you can protect sensitive content such as passwords or keys rather than leaving it visible as plaintext in playbooks or roles.
- Create a new encrypted file
ansible-vault create <vault_file>
You will be prompted to enter and confirm password.
Output would look like,
New Vault password:
Confirm New Vault password:
NOTE: This is NOT the password/secret you are going to encrypt. This is the password used to open/view/edit the encrypted file which will contain your secrets. Your actual secrets are added in the next action. To automatically accept the password to read/access the encrypted file (without prompting for the password), we can create a file with password in it. Usage is explained in the next section.
- Once you confirm your password, Ansible will immediately open an editing window where you can enter your desired contents. For example:
- You can view the contents of existing encrypted file (which would contain variables for your password/secrets)
ansible-vault view <vault_file>
#It will ask to enter the password
- Edit/update the content of existing encrypted file (which would contain variables for your password/secrets)
ansible-vault edit <vault_file>
#It will ask to enter the password
- Automatically accept the password (without prompting for the password), to read/access encrypted file.
Create a file & add the password, which will be used to read/access encrypted file. Preferably, it should be readonly to the owner (should be readable to other users). For example:
$ cat vault_passwd_file
my_password
- Run a Playbook which uses vault credentials/content
ansible-playbook <playbook> -i <inventory> --vault-password-file vault_passwd_file
Here, the playbook would use the secrets mentioned in the encrypted file which are decrypted using vault password file.
Note: Which encrypted file has to be referred will be mentioned in the playbook.
Popular Ansible modules for Windows
Typically, windows module names will start with win_
Some of the popular Ansible modules for Windows are:
- win_regedit Add, modify or remove registry keys and values
- win_reg_stat whether the key/property exists
- win_psmodule Install Windows PowerShell modules
- win_package Installs or uninstalls software packages for Windows
- win_shell Execute shell commands on target hosts
- win_command Executes a command on a remote Windows node
- win_group_membership Manage Windows local group membership
- win_unzip Unzips compressed files and archives on the Windows node
- win_timzone Sets Windows machine timezone
- win_path Manage Windows path environment variables
- win_environment Modify environment variables on windows hosts
- win_whoami Get information about the current user and process
Ansible Vault is a pretty nifty tool that allows people to easily encrypt secrets for use in Ansible. It’s a builtin tool that can be use to encrypt secrets and make them easily usable for Ansible.
When calling Ansible, you must supply the password by either manually entering it or use a password file. From there, Ansible will automatically decrypt the contents when it is required, simple stuff.
As an example I can turn something like this
host_password: supersecretpass123!
into this
$ANSIBLE_VAULT;1.1;AES256 33343835306666636239373663396363643766613363343837646633343933376633323964663030 3134616235646661306436643134383333633730376233650a663466323032343633383061336461 36393261363338616337613039363435313631343437323164386661326633313339396238396236 3462393338636632650a653036663266373533343232393838343161396564333963643632653932 30386135636131656130346537356637396139323134386162306431376564346537633566666532 6331323061373237336639356165393563613765663864366231
As a dev on the Ansible project, this was an area that I didn’t really understand and I wanted to fix that by understanding how the vault works. At the same time I also wanted to have another way for people working on Windows to encrypt and decrypt vault files, reducing the barrier for using Ansible for those users. Currently Ansible, and by extension ansible-vault
, is unable to run natively on Windows. There are ways around this such as using WSL or Cygwin to run the Ansible scripts but either this won’t work for all users (WSL) or is quite complex to setup (Cygwin).
I decided to try and kill 2 birds with 1 stone, which was to re implement the functionality of ansible-vault
in another language. In doing so I gain an understanding of how ansible-vault
works as well as giving Windows users a native solution for managing vault files.
Before I started to write this new tool, I had a few goals in mind which were;
- Runs natively on Windows with no extra software dependencies
- Also works on older Windows hosts like Windows 7, 8, and 8.1
- Simple and easy to setup/use
- Easy to maintain the software going forward
I could have just ported the Python code to a script that works on Windows and be done with it but, in my mind, that would be cheating and doesn’t really fulfill all my goals. Specifically it would require Python to be installed (not a hard ask I know but still extra software) and I wouldn’t be learning too much if I just reused code from the Ansible codebase.
The result is a PowerShell module that includes cmdlets to encrypt and decrypt vault files but before I go into the PowerShell side I want to explain how Ansible Vault works based on what I learnt.
Analysing Ansible Vault
The code behind Ansible Vault is all open source and can viewed in the Ansible GitHub Repo by anyone. At the time of writing this blog post, AES256 at the 1.1/1.2 implementation was the latest and that is what this section will break down. Each vault is split into 2 main parts;
- Header – On the first line of the vault file and defines the structure of the vault
- Cipher text – Contains the salt, hmac hash, and the encrypted bytes as a hex string with a newline at every 80 chars
Header
The header of the vault is comprised of a few keys fields, each separated by ;
, which are;
- The file format ID, currently only
$ANSIBLE_VAULT
is used - The vault version that indicates how it was encrypted
- The cipher used for encryption
- An optional ID field
Versions
There are 3 versions of the Ansible Vault that exist;
1.0
– Introduced in Ansible 1.5, this is the original vault format and no longer in use1.1
– Introduced in Ansible 1.5.1, fixed issues with the original format and is still in use today1.2
– Introduced in Ansible 2.4, is the same as1.1
but includes theID
field in the header
Currently 1.1
is used when no ID is set to the vault and 1.2
is used when an ID set. The cipher contents and encryption process are the same between the two, the only difference is the extra field in the header. Because the 1.0
format was quickly removed in a single minor release, I did not implement support for decrypting that format. If you still have a vault file in that format, change it immediately!
Ciphers
The next field in the header is the cipher that is used, currently it can be either of the following;
AES
– Used in the1.0
version and no longer in useAES256
– Used in both1.1
and1.2
, is based on the AES cipher with a 256 bit key in CTR mode
This made thing relatively simple as there is only cipher I needed to add support for.
Vault ID
The vault ID was added in the 1.2
version and it is used by Ansible to map a password to a particular vault file. For example, a user can create a vault file for each environment, say dev
, uat
, prod
, and create a vault file for each environment with different passwords per ID/environment. When a vault file contains an ID, the header would look like the following;
$ANSIBLE_VAULT;1.2;AES256;prod
Cipher Text
Before breaking down the cipher text, I will briefly cover the various cryptography tools used as part of the vault process. Currently these functions/protocls are being used;
- AES block cipher with a 32-byte (256 bit) key in CTR mode to encrypt/decrypt
- PBKDF2 using HMAC SHA256 to derive the various keys, this takes in a 32-byte salt, runs for 10000 iterations
- HMAC using SHA256 to verify the KDF output against the encrypted bytes
- PKCS7 padding on the encrypted bytes
When bringing this all together we need the following bits of information;
- A password used as the secret input into the PBKDF2 function
- A 32-byte salt to use with the password in the PBKDF2 function
- A 32-byte key to use as part of the HMAC function
- A 32-byte key used to initialise the AES cipher
- A 16-byte key used as the base counter/nonce of the AES CTR mode
We already know the password and the salt is stored in the cipher text, from there we can get the rest of the keys as they are the output of the PBKDF2 function. To get the salt, lets first break down the cipher text (excluding the header). Here is a sample cipher text that I posted in the beginning of this blog;
33343835306666636239373663396363643766613363343837646633343933376633323964663030
3134616235646661306436643134383333633730376233650a663466323032343633383061336461
36393261363338616337613039363435313631343437323164386661326633313339396238396236
3462393338636632650a653036663266373533343232393838343161396564333963643632653932
30386135636131656130346537356637396139323134386162306431376564346537633566666532
6331323061373237336639356165393563613765663864366231
This is a hex encoded string that contains the salt, the SHA256 based HMAC output of the encrypted bytes, and finally the encrypted bytes. Each entry is split by a newline which in hex form is 0a
, applying this split this is what we get;
# Salt
33343835306666636239373663396363
64376661336334383764663334393337
66333239646630303134616235646661
30643664313438333363373037623365
# HAMC
66346632303234363338306133646136
39326136333861633761303936343531
36313434373231643866613266333133
39396238396236346239333863663265
# Encrypted Bytes
65303666326637353334323239383834
31613965643339636436326539323038
61356361316561303465373566373961
39323134386162306431376564346537
63356666653263313230613732373366
39356165393563613765663864366231
Each entry is actually a hex string of a hex string, I’m not sure why this has been hex encoded twice but that’s just how it is. When “un-hexified” we get the following hex values for each entry;
# Salt (32 bytes)
34850ffcb976c9ccd7fa3c487df34937
f329df0014ab5dfa0d6d14833c707b3e
# HMAC (32 bytes)
f4f20246380a3da692a638ac7a096451
6144721d8fa2f31399b89b64b938cf2e
# Encrypted Bytes (48 bytes)
e06f2f75342298841a9ed39cd62e9208
a5ca1ea04e75f79a92148ab0d17ed4e7
c5ffe2c120a7273f95ae95ca7ef8d6b1
Bingo, we know have the salt and password and can use that in conjunction with the PBKDF2 function to produce the remaining keys. PBKDF2 is a key derivation function that applies a pseudorandom function to a secret input, such as a password, along with a salt to produce a derived key. Part of this function is the ability to specify the number of iterations that repeats the process to make the computational cost of calculating the key more expensive. As shown on Wikipedia, PBKDF2 has five input parameters
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
where:
PRF
: is a pseudorandom function like a HMAC, for Ansible Vault this uses the SHA256 algorithmPassword
: the password/secret that is known to the userSalt
: the salt, this is randomly generated when creating the vault but stored as part of the cipher text of an existing vault as we saw abovec
: the number of iterations desired, Ansible Vault is set to 10000dkLen
: the length of output key, for Ansible Vault we want 80 as (80 == (32 + 32 + 16) == (AES Key + HMAC Key + CTR Counter/Nonce))
Putting this into practice, you can use this handy Python script to generate the derived key. Note: for this to work you need the cryptography package installed;
import binascii
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
password = b"password" # this is the password I used to create the example vault
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=80,
salt=binascii.unhexlify("34850ffcb976c9ccd7fa3c487df34937f329df0014ab5dfa0d6d14833c707b3e"),
iterations=10000,
backend=default_backend())
derived_key = kdf.derive(password)
print(binascii.hexlify(derived_key))
The output of this script is the following hex string;
# KDF (80 bytes)
fc4a21fb71bfaad6a0bbb078f0704721
ccad80519fc349c3ff14268fced14203
9bfb1a43effdfb8f8d7119387fccec54
8859c7fccc26589a65a2ee856e05763f
394f9f4a44152b33234cba44c930921b
Ansible Vault, splits this output at 32 and 64 bytes into 3 parts like so;
# AES Key (32 bytes)
fc4a21fb71bfaad6a0bbb078f0704721
ccad80519fc349c3ff14268fced14203
# HMAC Key (32 bytes)
9bfb1a43effdfb8f8d7119387fccec54
8859c7fccc26589a65a2ee856e05763f
# AES CTR Counter/Nonce (16 bytes)
394f9f4a44152b33234cba44c930921b
Now we know all the keys that are required to decrypt/encrypt the bytes but before we do that, we want to verify the HMAC output against what is expected. If the HMAC value does not match the expectation, we know the password/secret was incorrect and can report that back to the user. Using a simple Python script we can get the HMAC value of the encrypted bytes
import binascii
import hashlib
import hmac
key = binascii.unhexlify("9bfb1a43effdfb8f8d7119387fccec548859c7fccc26589a65a2ee856e05763f")
encrypted_bytes = binascii.unhexlify("e06f2f75342298841a9ed39cd62e9208a5ca1ea04e75f79a92148ab0d17ed4e7c5ffe2c120a7273f95ae95ca7ef8d6b1")
hmac_digest = hmac.new(key, encrypted_bytes, digestmod=hashlib.sha256).digest()
print(binascii.hexlify(hmac_digest))
This produces the result f4f20246380a3da692a638ac7a0964516144721d8fa2f31399b89b64b938cf2e
which matches the HMAC value stored in the vault. We know the secret was correct and we can move onto decrypting the bytes themselves.
To decrypt the bytes, we need to use AES in CTR mode, here is some Python code to decrypt the data;
import binascii
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
key = binascii.unhexlify("fc4a21fb71bfaad6a0bbb078f0704721ccad80519fc349c3ff14268fced14203")
nonce = binascii.unhexlify("394f9f4a44152b33234cba44c930921b")
encrypted_bytes = binascii.unhexlify("e06f2f75342298841a9ed39cd62e9208a5ca1ea04e75f79a92148ab0d17ed4e7c5ffe2c120a7273f95ae95ca7ef8d6b1")
aes_cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), default_backend())
decryptor = aes_cipher.decryptor()
plaintext = decryptor.update(encrypted_bytes) + decryptor.finalize()
print(plaintext)
This produces the following UTF-8 string;
host_password: supersecretpass123!\n\r\r\r\r\r\r\r\r\r\r\r\r\r
So close to the plaintext but now we have \r
padded onto the end of the string. This is just simple PKCS7 padding to make sure the input to the AES cipher is a multiple of the AES block size (16 bytes). This padding is the same byte where the decimal value of that byte is the number of bytes needed. In the case above, the length of the plaintext value (including the last newline) is 35 while the nearest full block is 48. To reach the full block size of 48, the byte with a decimal value of 13 (\r
) needs to be appended. After removing the padding we get the original plaintext which Ansible loads as required.
To encrypt a string, it’s pretty much the same process in reverse order. For example this is what Ansible does when it goes to encrypt a string;
- Pad the text using the PKCS7 mechanism to the block size of 16 (AES block size /
- Generate a secure random 32-byte salt to use in the PBKDF2 function
- Generate the AES, HMAC, and AES CTR counter/nonce bytes using the PBKDF2 function with the salt and password specified
- Encrypt the padded text text using AES in CTR mode with the keys produced in step 3
- Generate the HMAC SHA256 hash of the encrypted bytes based on the HMAC key from step 3
- Create a hex string of the salt, HMAC hash, and encrypted bytes
- Join the three hex string’s with a newline
- Create another hex string of the value from step 7
- Create the vault header and append the value from step 8
From there you have an Ansible Vault that is securely protected by a password of your choosing.
Working with Windows
So now we know how the vault works and how Ansible encrypts/decrypts the data, it should be fairly trivial to reimplement it in PowerShell, or so I thought.
Problems on Windows
The biggest issue I had with Windows was finding a way to interop with the functions that are being used in the Ansible Vault process. More specifically I needed a way to;
- Use PBKDF2 with the SHA256 algorithm
- Use AES in CTR mode
- Pad and unpad the decrypted bytes with the PKCS7 spec
Each one of the above is either not possible in the standard .NET framework that PowerShell uses, or requires a newer version of the framework. Let’s break down each part and how I ultimately implemented it in PowerShell.
PBKDF2 with SHA256
The first step in implementing Ansible Vault on Windows is to derive the keys used in the downstream process. A quick Google search on how this can be done leads me to the Rfc2898DeriveBytes class. The description for this class is
Implements password-based key derivation functionality, PBKDF2, by using a pseudo-random number generator based on HMACSHA1.
Unfortunately we need to the use SHA256 hash in our generator so the default of SHA1 won’t produce the right key that we need. There’s some constructors that have a HashAlgorithmName parameter so that looks like what we need.
What the, the docs says there is!
So that didn’t work, when reading the docs around that constructor I found this gem
Version Information
.NET Framework
Available since 4.7.2
So I either need to set a dependency on .NET 4.7.2 to run the cmdlets or find some other way around this limitation, because my goal was to support older hosts I pretty much had to find another way. The fact that a newer version of the .NET framework exposes the parameter that I needed usually means Windows can do it with the native Win32 APIs. After some searching I came across this blog post from Microsoft which used the BCryptKeyDerivation Win32 API. This is close to what I am looking for but unfortunately BCryptKeyDerivation
was only added in Windows 8/Server 2012 and I was hoping for something I can use in Windows 7/Server 2008 R2. Finally I came across the BCryptDeriveKeyPBKDF2 which was added in Windows 7/Server 2008 R2.
So now we have an API available on Windows that can give us the key required but we need a way to call this within PowerShell. There’s no inbuilt way in PowerShell to do this, but you can use C# code with P/Invoke to call these platform functions and PowerShell can compile/run C# code. This is done through the Add-Type cmdlet which calls csc.exe
to compile the C# code and load it into the current PowerShell scope. For example here is a simple way to add the functions we need in PowerShell;
Add-Type -Namespace Win32 -Name NativeFunctions -MemberDefinition @'
[DllImport("Bcrypt.dll")]
public static extern uint BCryptOpenAlgorithmProvider(
out IntPtr phAlgorithm,
[MarshalAs(UnmanagedType.LPWStr)] string pszAlgId,
[MarshalAs(UnmanagedType.LPWStr)] string pszImplementation,
UInt32 dwFlags);
[DllImport("Bcrypt.dll")]
public static extern uint BCryptDeriveKeyPBKDF2(
IntPtr hPrf,
[MarshalAs(UnmanagedType.LPWStr)] string pbPassword,
UInt32 cbPassword,
byte[] pbSalt,
UInt32 cbSalt,
UInt64 cIterations,
byte[] pbDerivedKey,
UInt32 cbDerivedKey,
UInt32 dwFlags);
[DllImport("Bcrypt.dll")]
public static extern uint BCryptCloseAlgorithmProvider(
IntPtr hAlgorithm,
UInt32 dwFlags);
'@
$algo = [IntPtr]::Zero
$res = [Win32.NativeFunctions]::BCryptOpenAlgorithmProvider([Ref]$algo, "SHA256", $null, 0x00000008)
if ($res -ne 0) {
throw "Failed to open algo provider"
}
$key = New-Object -TypeName byte[] -ArgumentList 80
$salt = New-Object -TypeName byte[] -ArgumentList 32
$pass = "password"
$res = [Win32.NativeFunctions]::BCryptDeriveKeyPBKDF2($algo, $pass, $pass.Length, $salt, $salt.Length, 10000, $key, $key.Length, 0)
if ($res -ne 0) {
throw "Failed to derive key"
}
[Win32.NativeFunctions]::BCryptCloseAlgorithmProvider($algo, 0)
Using Add-Type does have some downsides, it creates a temporary DLL file on disk and it takes more time to compile. None of these are really issues for this scenario but it has been something I wanted to work around for some time. I decided to take the opportunity to find another way around Add-Type and ultimately came across a way of calling these native functions with .NET Reflection. There are some blog posts which I have referenced at the bottom of this post, that helped me to understand how to use reflection. This resulted in the following cmdlet Invoke-Win32Api which can be used to call any Win32 APIs. If you are interested in learning more about this I recommend you look at the code and references to see how it works.
In the end, New-PBKDF2Key is what I ended up with. It calls the native Win32 APIs to produce the key required in a way that works on Windows 7/Server 2008 R2 and newer. In the future, I may add a conditional check to use Rfc2898DeriveBytes if the HashAlgorithmName constructor is available so this works on .NET Core but that wasn’t part of my original goals.
AES in CTR mode
Now that we have solved the PBKDF2 issue we move onto the next one, getting the AesCryptoServiceProvider to work in CTR mode. When looking at the CipherMode enumeration values, there is no option to run in CTR mode which is going to be a problem for us.
Looking at the underlying Win32 functions I was hoping this would be a similar situation as the PBKDF2 with SHA256 but unfortunately that didn’t seem to be the case, I had to implement this mode myself. I was loath to do this and was prepared to scrap this whole idea but luckily CTR mode isn’t that complex to do. From my understanding (please don’t reference me for this), it works like this;
- A counter is initialised from a randomly unique nonce value, this nonce is derived as part of the key from the PBKDF2 function (last 16 bytes)
- The counter is then encrypted with the AES cipher in ECB mode, this AES cipher is based on the 32-byte key from the PBKDF2 function (first 32 bytes)
- The counter is incremented by 1
- The result from step 2 is the XOR’d with the plaintext or ciphertext byte by byte until there are no more bytes left in the output
- Steps 2-4 is repeated until all bytes in the plaintext or cipher text have been XOR’d
Taking this into practice, let’s decrypt the first 32 bytes of our example cipher text, here are the inputs from the example;
# First 16 bytes of our encrypted bytes
e06f2f75342298841a9ed39cd62e9208
# Next 16 bytes of our encrypted bytes
a5ca1ea04e75f79a92148ab0d17ed4e7
# The starting 16-byte counter/nonce (from the PBKDF2 function)
394f9f4a44152b33234cba44c930921b
# The 32-byte AES key used in counter transformations
fc4a21fb71bfaad6a0bbb078f0704721ccad80519fc349c3ff14268fced14203
When using a site like this online AES ECB cipher we can encrypt the current counter value with the AES 256 key to get the input to the XOR function.
When xoring the output 88005c016b52f9f769e9bceeb214b27b
with the next 16 bytes in the encrypted bytes array e06f2f75342298841a9ed39cd62e9208
we get 686f73745f70617373776f72643a2073
which is host_password: s
in UTF-8. Now we have exhausted the XOR input, we need to increment the counter and repeat the process again.
# Previous counter
394f9f4a44152b33234cba44c930921b
# Next counter
394f9f4a44152b33234cba44c930921c
# AES ECB encryption output of this counter
d0ba7bd23d1094e8f760fad1a20de5d5
# Next 16 bytes of our encrypted bytes to XOR
a5ca1ea04e75f79a92148ab0d17ed4e7
# XOR result
75706572736563726574706173733132
# UTF-8 string of the XOR hex string
upersecretpass12
Putting the outputs together we currently have host_password: supersecretpass12
. there are still some encrypted bytes left but the process is still the same; increment the counter, encrypt counter, XOR encrypted result with the next encrypted block. Putting this into PowerShell was relatively simple and the end result is Invoke-AESCTRCycle. Because of the nature of XOR, the encryption and decryption process is exactly the same in CTR mode.
PKCS7 padding
So we have solved the hurdle of PBKDF2 with SHA256 and adding support for AES in CTR mode, the last remaining hurdle is adding a function to pad and unpad our bytes based on the PKCS7 algorithm. Usually padding is used to “pad” the input bytes so it fits the block size in a cipher, e.g. AES uses a block size of 16 so each input to the transformation process must also be 16-bytes. AES in CTR mode is actually a stream cipher so the input block does not need to be the same size, but in Ansible Vault, the data is still padded.
Usually this padding is done as part of the AesCryptoServiceProvider
but because we implemented our own method and CTR mode is not a block cipher we need to manually pad or unpad the data. Luckily PKCS7 is a relatively simple algorithm and easily implement, it appends the same byte that is equal to the number of bytes to add for a complete block until the block is complete. The exception to this is if the input data is currently the size of the block, PKCS7 will still add another block with the value being the number of bytes added.
Here are some examples of padding in action for an 8-byte block size;
# 8 - 1 = 7, the byte with the value 07, is added 7 times
01 == 01 07 07 07 07 07 07 07
# 8 - 3 = 5, the byte with the value 05 is added 5 times
01 02 03 == 01 02 03 05 05 05 05 05
# When the input is the same as the block size, an extra block is added
01 02 03 04 05 06 07 08 == 01 02 03 04 05 06 07 08 08 08 08 08 08 08 08 08
I ended up with 2 cmdlets to achieve this Add-Pkcs7Padding, and Remove-Pkcs7Padding.
AnsibleVault module
Putting this all together, I present AnsibleVault, a PowerShell module that can encrypt and decrypt Ansible vaults on Windows. Earlier I stated that one of my goals was to have a way to easily maintain the script and just creating a module does not fully fit this. I wanted a way to automatically;
- Run sanity checks on the code
- Run unit and integration tests
- Automatically deploy the changes to the PowerShell Gallery
To achieve this, I used a combination of the following PowerShell modules;
- PsScriptAnalyzer – static code checker
- Pester – testing and code coverage tool
- PSDeploy – tool used for simplifying the module deployment to PowerShell Gallery
- Psake – build automation tool
- BuildHelpers – helper tool for running a build in a CI environment like Appveyor
If you interested in how I put all of this together I highly recommend you read through this blog post who is the author of some of those modules. I implemented most of the work that the blog talked through but added a few extra things like the PsScriptAnalyzer and code coverage steps which are two metrics I am interested in.
Ultimately I can test out changes to AnsibleVault and have a system that will reliably run tests and other checks on the changes automatically. My only wish would be a way to test against older PowerShell versions and not just one but that’s probably a project for another time. The other great thing about this workflow is that to deploy a new release to the PowerShell Gallery, all I need to do is create a new tag in GitHub. This will kick off a run in Appveyor which will publish the changes to the gallery.
How to get it
Now that the module is part of the PowerShell Gallery it is quite simple to install, just run the Install-Module module.
Note: this will only work if you are running PowerShell v5 or have PowerShellGet installed for older versions.
You can set -Force to automatically accept prompt
If you don’t want to install the module system wide (or you don’t have admin rights), you can install it just for the current user by setting -Scope CurrentUser
on the install cmdlet. PowerShellGet makes managing modules such a simple thing to do and I highly recommend people install it through the MSI if they can’t upgrade PowerShell to version 5. If you wish to uninstall the module, the cmdlet Uninstall-Module -Name AnsibleVault
can be used to remove it from the system.
If you don’t want to install PowerShellGet you can manually install it on the system by doing the following;
- Download the latest zip from GitHub here
- Extract the zip
- Copy the folder
AnsibleVault
inside the zip to a path that is set in$env:PSModulePath
, e.g.C:\Program Files\WindowsPowerShell\Modules
orC:\Users\<user>\Documents\WindowsPowerShell\Modules
- Trust the downloaded files with
$path = (Get-Module -Name AnsibleVault -ListAvailable).ModuleBase; Unblock-File -Path $path\*.psd1; Unblock-File -Path $path\Public\*.ps1; Unblock-File -Path $path\Private\*.ps1
- Restart PowerShell so the unblock policy is applied to the module
Note: You are not limited to those paths, you can add a new entry to the environment variable PSModulePath
if you want to use another path.
Here I have installed it under the user’s profile
Using the AnsibleVault module
Now that the module is installed, the cmdlets Get-DecryptedAnsibleVault
and Get-EncryptedAnsibleVault
can be used like any other cmdlet. Here is the basic syntax for each module;
Get-DecryptedAnsibleVault `
[-Value] <String> `
[-Password] <SecureString> `
[-Encoding <Encoding>] `
[<CommonParameters>]
Get-DecryptedAnsibleVault `
[-Path] <String> `
[-Password] <SecureString> `
[-Encoding <Encoding>] `
[<CommonParameters>]
Get-EncryptedAnsibleVault `
[-Value] <String> `
[-Password] <SecureString> `
[-Id <String>] `
[<CommonParameters>]
Get-EncryptedAnsibleVault `
[-Path] <String> `
[-Password] <SecureString> `
[-Id <String>] `
[<CommonParameters>]
Here are what each parameter does;
Value
: A string value to encrypt/decrypt, this can also be sent as a pipeline inputPath
: The path to a file whose contents will be encrypted/decryptedPassword
: A secure string that is the password for the vaultEncoding
: When decrypting a vault, this is the final output string encoding of the vault (default if UTF-8). You shouldn’t have to touch this parameter but if the source vault file was not UTF-8 than this can control the final decrypted outputId
: An optional parameter that specifies the ID to assign to the new vault string
You can use a combination of these cmdlets and pipelining to achieve similar results to some common ansible-vault
commands, here are some common examples to replace existing ansible-vault
functionality;
# ansible-vault encrypt C:\temp\vault.yml --ask-vault-pass
Get-EncryptedAnsibleVault -Path C:\temp\vault.yml | Set-Content -Path C:\temp\vault.yml
# ansible-vault encrypt C:\temp\vault.yml --vault-id dev@prompt
Get-EncryptedAnsibleVault -Path C:\temp\vault.yml -Id dev | Set-Content -Path C:\temp\vault.yml
# ansible-vault rekey C:\temp\vault.yml --ask-vault-pass
Get-DecryptedAnsibleVault -Path C:\temp\vault.yml | Get-EncryptedAnsibleVault | Set-Content -Path C:\temp\vault.yml
# ansible-vault view C:\temp\vault.yml --ask-vault-pass
Get-DecryptedAnsibleVault -Path C:\temp\vault.yml
# ansible-vault decrypt C:\temp\vault.yml --ask-vault-pass
Get-DecryptedAnsibleVault -Path C:\temp\vault.yml | Set-Content -Path C:\temp\vault.yml
# ansible-vault encrypt_string --stdin-name 'vault_variable'
$vault_text = Read-Host -Prompt "Enter string to encrypt" | Get-EncryptedAnsibleVault
Write-Output -InputObject "vault_variable: !vault |`n $($vault_text.Replace("`n", "`n "))"
# add a new variable to an existing vault file
$vault_pass = Read-Host -Prompt "Enter vault password" -AsSecureString
$vault_text = Get-DecryptedAnsibleVault -Path C:\temp\vault.yml -Password $vault_pass
$vault_text += "`nanother_host_secret: you'll never guess this"
Get-EncryptedAnsibleVault -Value $vault_text -Password $vault_pass | Set-Content -Path C:\temp\vault.yml
AnsibleVault in Action, the sky is the limit with what you can do
References
I would like to point out a few different blogs/sites that helped me along the way
- PBKDF2 SHA256 C# Implementation
- Lee Holmes and P/Invoke through .NET Reflection
- The Scripting Guys – Another P/Invoke through .NET Reflection
- Hans Wolff and his AES CTR implementation
- Rambling Cookie Monster and his PSake reference