Lesson 2 established that lookups run on the controller. That is the
machine holding SSH keys to the whole fleet and the vault password that
decrypts your secrets — so anything that reads secrets, or executes
code, does it there, with your privileges.
This lesson covers the five ansible.builtin lookups that handle
credential material, and it spends most of its length on the two that
behave in ways people do not expect.
Right for anything the pipeline puts in the environment. Two
properties to hold on to: a variable that is not set yields an empty
string rather than failing, and the environment read is the
controller’s — including on CI runners, where the environment is
whatever the pipeline injected and may include far more than you
intended a playbook to see.
file and unvault
file returns bytes as they are on disk. Point it at a
vault-encrypted file and you get the ciphertext, headed
$ANSIBLE_VAULT;1.1;AES256 — which is a surprisingly common way to
deploy an encrypted blob to a host by accident, since it does not
error.
unvault decrypts first, using whatever vault password the run has
available:
Read-only / Safereading an encrypted file's plaintext
no_log: true is not optional there. Part XXII covers why in full; the
short version is that without it the decrypted value appears in -vvv
output and in the task result.
Configuration changeit generates once and remembers— Runs two password lookups against the same path. This writes a file on the controller, which is why it is not a read-only operation.
The second call returns the same password as the first. It has to, or
every run would rotate every credential. And the mechanism by which it
remembers is the part people miss:
Read-only / Safethe side effect— Lists the path given to the password lookup. The generated password is stored there in plaintext.
$ ls -la pw/ && wc -c pw/demo
total 4
drwxr-xr-x 2 opsuser opsuser 60 Aug 11 22:45 .
drwxr-xr-x 13 opsuser opsuser 540 Aug 11 22:45 ..
-rw------- 1 opsuser opsuser 21 Aug 11 22:45 demo
21 pw/demo
The path argument is not a key or an identifier. It is a file, and
the password is written into it in plaintext, mode 0600, on the
controller.
pipe — the one that is a shell
Read-only / Safepipe runs on the controller, as you— Runs two pipe lookups. The first reports the controller user; the second demonstrates that shell operators are honoured.
$ ansible-playbook -i localhost, pipe.yml
TASK [pipe executes on the controller] *****************************************
ok: [localhost] => {
"msg": "opsuser"
}
TASK [pipe with a shell metachar] **********************************************
ok: [localhost] => {
"msg": "one\ntwo"
}
echo one && echo two produced both lines. The && was interpreted,
which means this is a shell — not an execve of one binary with
arguments. Everything the Linux course says about shell quoting,
command substitution and set -o pipefail applies, on the controller,
in the security context of the person who ran the playbook.
External secret managers
Every major secret store ships a lookup in its collection —
HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, CyberArk,
and others. They are all the same shape and all subject to the same
three properties, which is more useful to know than any one of their
option lists:
1. The controller authenticates, not the target. The credential
that unlocks the secret store lives on the controller. That is usually
right — the managed node should not be able to read the store — and it
concentrates yet more privilege on the controller.
2. The fetch happens during templating. Before the task runs, on
every host the expression is evaluated for. A lookup inside a task
looped over 300 hosts can be 300 calls to your secret store, and rate
limits are real. Fetch once into a variable with set_fact or a
play-level vars, and reuse it.
3. no_log is your responsibility. The lookup returns a string.
Nothing marks it secret; nothing redacts it downstream. A task whose
parameters contain a fetched secret and no no_log: true prints it at
-vvv.
Read-only / Safefetch once, use many
- name: Fetch the credential once for the whole play
ansible.builtin.set_fact:
db_password: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/data/app:password') }}"
run_once: true
delegate_to: localhost
no_log: true
- name: Use it
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
owner: app
mode: '0400'
no_log: true
Part XXIII covers external secret managers as an architecture. Here the
point is narrower: whichever one you use, it is a lookup, so it runs
on the controller and everything in this lesson applies.
Knowledge check
Knowledge check · 4 questions
Q1. What does lookup('ansible.builtin.password', 'credentials/db_password length=20') do with its path argument?
Q2. Why does permitting lookup('pipe', ...) in a repository change who may approve changes to it?
Q3. Which are true of secret-store lookups such as the HashiCorp Vault one? Select all that apply.
Q4. lookup('file', ...) pointed at a vault-encrypted file returns ciphertext, and deploying that ciphertext to a host raises no error.
Passing score: 75%. Answers are checked in this browser.