GitHub: https://github.com/faustobranco/devops-db/tree/master/knowledge-base/ansible/secrets
In this post, I want to show how to use Ansible Vault to encrypt sensitive content in playbooks. For example, username, password, some key, IP, etc.
Of course, in more complex environments, managing this content becomes a little complicated, even if versioned in some Git. In these cases, the adoption of a dedicated Vault, such as the HashiCorp Vault, must be taken into consideration.
But even so, on certain occasions, we won’t have to escape having some content in our playbooks that need to be encrypted.
Before we begin, I want to make it clear that there are several ways to protect this content, even with Ansible Vault. One example is encrypting an entire file (Ansible – File Encryption in Ansible Vault), which is excellent, for example for certificates or key files. But in this example, I will only explain how to encrypt variable contents.
The password for encryption and decryption can also be in several ways, being requested at each execution from the operator/user running the playbook (which I think is unfeasible), key within a protected file (I will explain and apply this further below) or even inside another Vault.
Here is a brief explanation of how string encryption works, the ansible-vault command receives as parameters a password, which will be used as a key in encryption/decryption (it will be requested at run time), the string that will be encrypted and a variable name, which is the name of the variable to which the encrypted content will be applied… it will make sense when I show it.
Syntax:
ansible-vault encrypt_string <password_source> '<string_to_encrypt>' --name '<string_name_of_variable>'
So in the example below, you will encrypt any password/string ‘1234qwer‘ which would be the value of a variable ‘pass_database‘. In Ansible, something like this:
pass_database: 1234qwer
On the Ansible host, run the command below and when asked for the password, enter one of your choice, this will also be your key to decrypt.
ansible-vault encrypt_string '1234qwer' --name 'pass_database'
Return:
New Vault password:
Confirm New Vault password:
Encryption successful
pass_database: !vault |
$ANSIBLE_VAULT;1.1;AES256
38316233346432383532633265303934373339313266356338333934383461306330613066643165
3832316137313832653464643134313736393563363832300a396436303038393339393866343732
39623965623364623661313132303237626239653437306261346533326261313635346335613430
3864393339303132310a303139633338383635303461646430326430643133393131333931376333
3934
So the content above, from line 4 onwards, is your variable that must be placed in the playbook files.
Now, let’s decrypt:
echo '$ANSIBLE_VAULT;1.1;AES256
38316233346432383532633265303934373339313266356338333934383461306330613066643165
3832316137313832653464643134313736393563363832300a396436303038393339393866343732
39623965623364623661313132303237626239653437306261346533326261313635346335613430
3864393339303132310a303139633338383635303461646430326430643133393131333931376333
3934' | ansible-vault decrypt && echo
When Ansible asks, type the password again and the decrypted result will appear.
Vault password:
Decryption successful
1234qwer
Password file
Another way to encrypt and decrypt is to use a file with the password. I personally find it much more practical, and with some care in a protected environment, it remains safe.
For this lab, I created a Docker to serve as a specific Ansible server, as most of the scripts, in the case of this Lab, will be run by Jenkins and the Ansible sources in GitLab.
In this container, docker, I will then create the file with the password, simple, in plain text.
An interesting point is that this file can contain several passwords identified by a tag, which can be used according to your criteria, environment, systems, etc.
In this example, I will use two tags: dev and prod.
This file should be protected mode: ‘0644’, owner: root, group: root
/root/.vault_password.sec
dev:SnxvwDsh365DAnId1gfojBSKzfHc8x1vmvyNiaC1dkrlcE5NNe
prod:nBODrPRuespQUOmpEUHFGtynIjoBvjfrA1zl8YF7hVTU9fOIrr
If you want to do encryption tests with the file:
ansible-vault encrypt_string '1234qwer' --name 'pass_database' --vault-id prod@/root/.vault_password.sec
Encryption successful
pass_database: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
35386434336435653233316437306639653163653462363664626261346136656233346164613333
3631656261323261356137643932393736303264313834360a323330373734383739383333366337
36386561323761386630633065613637646236626164646339653137343334393465353939366266
6362663465373762640a663738393430336339663064303635383065343763393437643533353833
3761
Decrypt
echo '$ANSIBLE_VAULT;1.2;AES256;prod
35386434336435653233316437306639653163653462363664626261346136656233346164613333
3631656261323261356137643932393736303264313834360a323330373734383739383333366337
36386561323761386630633065613637646236626164646339653137343334393465353939366266
6362663465373762640a663738393430336339663064303635383065343763393437643533353833
3761' | ansible-vault decrypt --vault-id prod@/root/.vault_password.sec && echo
Decryption successful
1234qwer
Encrypt the variables.
The playbook that I am going to create is, as always, on my GitHub (https://github.com/faustobranco/devops-db/tree/master/knowledge-base/ansible/secrets), but the structure is as described below and basically it is a file for the playbook (secrets.yaml), the test_secret role with a task, a jinja template and a variables file and a global variables file, at the playbook level.
The host is the least important in this case, but it is an Ubuntu VM created in Vagrant, which I use only for testing.
|-- hosts
|-- roles
| `-- test_secret
| |-- tasks
| | `-- main.yaml
| |-- templates
| | `-- secrets
| | `-- secrets.j2
| `-- vars
| `-- main.yaml
|-- secrets.yaml
`-- vars
`-- global_vars.yaml
For the global variables file (global_vars.yaml) we will encrypt the contents:
Original values:
ansible_ssh_user=usr_linuxadmin
ansible_ssh_pass=sg8BnHlWpkoQlLwmAZPePZ9JnLgwDffjIKTcLwP1NXCN5L2tms
ansible_sudo_pass=sg8BnHlWpkoQlLwmAZPePZ9JnLgwDffjIKTcLwP1NXCN5L2tms
global_database
username=usr_database_admin
password=IgzuZ2MSOsdtOT9JMovkrQATACla3ro6xtL2BHcvNOhr9BohJI
Encrypt:
ansible-vault encrypt_string 'usr_linuxadmin' --name 'ansible_ssh_user' --vault-id prod@/root/.vault_password.sec
ansible-vault encrypt_string 'sg8BnHlWpkoQlLwmAZPePZ9JnLgwDffjIKTcLwP1NXCN5L2tms' --name 'ansible_ssh_pass' --vault-id prod@/root/.vault_password.sec
ansible-vault encrypt_string 'sg8BnHlWpkoQlLwmAZPePZ9JnLgwDffjIKTcLwP1NXCN5L2tms' --name 'ansible_sudo_pass' --vault-id prod@/root/.vault_password.sec
ansible-vault encrypt_string 'usr_database_admin' --name 'username' --vault-id prod@/root/.vault_password.sec
ansible-vault encrypt_string 'IgzuZ2MSOsdtOT9JMovkrQATACla3ro6xtL2BHcvNOhr9BohJI' --name 'password' --vault-id prod@/root/.vault_password.sec
And the result of the above commands is added to the file, remember that with each execution, the result is always different, so don’t worry about it being the same as the result below, but it must be in the same format.
vars/global_vars.yaml
ansible_ssh_user: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
62303766613665316364363963333266623835626534366635373163336233666464336235376439
6537633738356535643337393962306137303739643563350a353838363231323763333038356331
31336535363161623564383933363734343436326438393837646336373364323234613263383333
6137303031623731650a323034333537646262393935663863353233623835646335333536383866
6664
ansible_ssh_pass: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
31646463303962666233653431343733623732633735356335376163346433623431653133373062
6432366262336533306135313838386635336162396661610a636665313838326236323962346335
64643831383934643566343064636130313063616332393435346632646437393734313961356139
3865303332653432390a343835393765383733393162333962313965356538393965326633663664
62613366393139343234646231653265383263356564623165316631623663396666636630366431
66633736333262613365656532356433626661643735313236656531376266653464656564653361
623636663265613239376366356162656561
ansible_sudo_pass: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
35303437313531373733316562666234623264623533323962323034373330666430626330626339
3737643033323637633661626664363764343539306531650a393532643633383364616362373064
30383235653131366366363933373863313738366531373562663763396331333161313532396363
6133333364636139650a333830333230383763646139393130366137316433646663306465323766
38623134616361366139663134313465303561613039626539613962623062313236663036616635
30363938346234303232356366396435626531313439663062653835613962396365393537633733
393231363230626339333239633837336137
global_database:
username: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
61323730333638656364383263383665343461303533663363623038363637623064383362643630
3536323831306235326365663939653264313831623565300a383362313465373633393464613635
39616463623033336531663633313365386261653533313335613839373962396466303234633162
3030343032626639630a343133656437613136383537653636643432343537666333383665316264
32653363333637393362663066333539656361366638353966363237623166393161
password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
35633535333664633835333138333733646533373736653831656631366366366563326162353864
3330306232363639376366616439383164396433393666660a353932313431383834313464386362
65373133343261326434373764363131626234303533386363663936363066363564343632646236
3431386163383738610a306564303331316433366164376337646636353238623031316464343935
38326431376466653537366531343633313962626137303333666433353932343731373764373562
31383563376662353766346163303337353132343030336662623166666535313064643761613730
353731633534623066353437323563366633
Next, we will encrypt the contents of the variables in the role variables file in the same way.
Original values:
devopsdb_share:
username=devopsdbshare
password=6abtaPLIEgx1pp7V7tX7rLzsw7UkdMvEUHa3vZOsLMnAK21Kg1
Encrypt:
ansible-vault encrypt_string 'devopsdbshare' --name 'username' --vault-id prod@/root/.vault_password.sec
ansible-vault encrypt_string '6abtaPLIEgx1pp7V7tX7rLzsw7UkdMvEUHa3vZOsLMnAK21Kg1' --name 'password' --vault-id prod@/root/.vault_password.sec
roles/test_secret/vars/main.yaml
Yes, there are unencrypted variables in the middle, to demonstrate that there are no problems.
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
devopsdb_share:
username: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
32636134313633366232306431653736396461313432326564386238346561363831666462356631
3235646134623661356439623865373730373138636535350a616338363962636465656333376666
61623131666265336362323739326237343835303263336431643561353235306664336134383165
3438343136363263630a346239353862616435356531303232323666313238626236376466323134
3139
password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
36303966393831653130316234336634333966636264306132326333363462666565366330336664
6463376232343730626339303364333161653334666437340a303665386538613465613163643534
36386535343738363531643337636231363566396138663433376337306134646366346661613337
3731306533333135660a653262333163653033623335633039313333663431336462653961316264
36646436363963346261633836656530336330623266316435376263303331356430633933363062
38663761333739323732663039373365323632343065346462653762653134636661366563376662
653730373233646130663930613630613831
And now, finally, a templates file in Jinja2, which will be created in real-time and copied to the host. with the And now, finally, a templates file in Jinja2, which will be created in real-time and copied to the host. already with the decrypted information created above.
roles/test_secret/templates/secrets/secrets.j2
username={{ devopsdb_share.username }}
password={{ devopsdb_share.password }}
Playbook
The playbook is very simple, just to show the values in the terminal, to demonstrate that the values were successfully decrypted, and finally, a cat in the file created on the host.
(playbook) secrets.yaml
- hosts: "tests_Hosts"
become: true
roles:
- test_secret
vars_files:
- vars/global_vars.yaml
gather_facts: True
(role/task) roles/test_secret/tasks/main.yaml
- name: output the global playbook 'ansible_ssh_user' variable
ansible.builtin.debug:
var: ansible_ssh_user
- name: output the global playbook 'ansible_ssh_pass' variable
ansible.builtin.debug:
var: ansible_ssh_pass
- name: output the global playbook 'ansible_ssh_common_args' variable
ansible.builtin.debug:
var: ansible_ssh_common_args
- name: output the global playbook 'global_database.username' variable
ansible.builtin.debug:
var: global_database.username
- name: output the global playbook 'global_database.password' variable
ansible.builtin.debug:
var: global_database.password
- name: Create secrets dir
file:
path: "/root/secrets"
state: directory
- name: Add secrets mount with local variables - jinja
ansible.builtin.template:
dest: /root/secrets/secrets
src: secrets/secrets.j2
mode: '0644'
owner: root
group: root
Let’s run the playbook. To do this we have to pass, in addition to the standard parameters, also the file with the passwords.
But if the file has more than one password, which one will Ansible choose? Note that in variable encryptions, this information already has this information, the tags are in the instructions, example: $ANSIBLE_VAULT;1.2;AES256;prod
ansible-playbook -i /work/secrets/hosts /work/secrets/secrets.yaml --vault-id /root/.vault_password.sec
The return, as Ansible standard:
PLAY [tests_Hosts] ***********************************************************************************************************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01]
TASK [test_secret : output the global playbook 'ansible_ssh_user' variable] **************************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01] => {
"ansible_ssh_user": "usr_linuxadmin"
}
TASK [test_secret : output the global playbook 'ansible_ssh_pass' variable] **************************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01] => {
"ansible_ssh_pass": "sg8BnHlWpkoQlLwmAZPePZ9JnLgwDffjIKTcLwP1NXCN5L2tms"
}
TASK [test_secret : output the global playbook 'ansible_ssh_common_args' variable] *******************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01] => {
"ansible_ssh_common_args": "-o StrictHostKeyChecking=no"
}
TASK [test_secret : output the global playbook 'global_database.username' variable] ******************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01] => {
"global_database.username": "usr_database_admin"
}
TASK [test_secret : output the global playbook 'global_database.password' variable] ******************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01] => {
"global_database.password": "IgzuZ2MSOsdtOT9JMovkrQATACla3ro6xtL2BHcvNOhr9BohJI"
}
TASK [test_secret : Create secrets dir] **************************************************************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01]
TASK [test_secret : Add secrets mount with local variables - jinja] **********************************************************************************************************************************************************************************************************************************************************
ok: [srv-infrastructure-test-master-01]
PLAY RECAP *******************************************************************************************************************************************************************************************************************************************************************************************************************
srv-infrastructure-test-master-01 : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
There on the target host:
root@srv-infrastructure-test-master-01:/home/vagrant# cat /root/secrets/secrets
username=devopsdbshare
password=6abtaPLIEgx1pp7V7tX7rLzsw7UkdMvEUHa3vZOsLMnAK21Kg1