Every lesson in this part so far has worked on the same premise: the secret
is in your repository, encrypted, and the job is to keep it that way and to
stop it escaping once decrypted.
This lesson questions the premise. A secret that is never in the repository
cannot leak from the repository, cannot be committed by accident, cannot
sit in a clone on a laptop that gets stolen, and does not need a password
you have to distribute.
The property that makes this work
Lookups run on the controller, not on the managed node. That is the
whole architecture in one sentence, and it is worth proving rather than
asserting.
Read-only / Safea lookup resolves for a host that is never contacted— Executed on ansible-core 2.21.3. The play targets 192.0.2.10, documentation space, unreachable by design. The lookup value is the controller user.
$ ansible-playbook -i inv.ini lk.yml
TASK [Show the lookup value on the controller before any connection] ****
"msg": "lookup resolved to: ebrandi"
TASK [Now actually connect] ********************************************
fatal: [web1.example.com]: UNREACHABLE! => {"changed": false,
"msg": "Task failed: Failed to connect to the host via ssh:
ssh: connect to host 192.0.2.10 port 22: Connection timed out",
"unreachable": true}
The lookup produced a value; the host was never reachable. Whatever the
lookup did, it did on the controller.
Three consequences follow, and they are the reason this pattern is
practical at all:
Managed nodes need no access to the secret store. They do not need
network reachability to it, credentials for it, or any client software.
Four hundred hosts do not become four hundred secret store clients.
One credential fetches for the whole fleet. The controller
authenticates once and distributes what it retrieves. That is one identity
to manage, audit and revoke rather than one per host.
The controller becomes even more sensitive. It now holds the identity
that can retrieve production credentials on demand. The previous lesson’s
conclusion applies with more force: a controller that can decrypt or
retrieve production secrets is a production system.
Retrieving a secret
The community.hashi_vault collection is the reference implementation.
Every cloud provider ships an equivalent collection, and the shape is the
same in each.
Read-only / Safethe KV v2 lookup, with the return structure the docs describe
- name: Deploy with a credential fetched at run time
hosts: appservers
vars:
# Runs on the controller. Nothing is stored in this repository.
db_creds: "{{ lookup('community.hashi_vault.vault_kv2_get',
'billing/database',
engine_mount_point='secret',
url='https://vault.example.com:8200',
auth_method='token',
token=lookup('env', 'VAULT_TOKEN')) }}"
tasks:
- name: Write the application credentials
ansible.builtin.template:
src: credentials.j2
dest: /etc/billing/credentials
owner: billing
mode: '0600'
vars:
db_password: "{{ db_creds.secret.password }}"
no_log: true
Two details from the plugin documentation worth getting right first time.
The return is a dictionary, not a string. The documented accessible
keys are secret — “The data field within the data field. Equivalent to
raw.data.data” — plus data, metadata and raw. So the value you
want is db_creds.secret.<key>, and a lookup returning something that
looks nothing like a password is almost always this.
engine_mount_point defaults to secret. If your KV engine is mounted
elsewhere, every path is wrong in a way that presents as “the secret does
not exist”.
The collection provides a family of lookups — hashi_vault,
vault_kv1_get, vault_kv2_get, vault_read, vault_list,
vault_login, vault_write, vault_token_create and
vault_ansible_settings — plus modules for managing the store itself,
including dynamic database credentials. Read the one you need; do not
extrapolate parameter names from a sibling.
Static secrets versus dynamic credentials
Fetching a stored password at run time is an improvement. It is not the
main prize.
The larger change is a credential that did not exist before the run and
expires after it. The store creates a database user on demand with a
lease of, say, one hour, hands it over, and revokes it when the lease ends.
Encrypted file in the repository
Fetched static secret
Dynamic credential
In the repository
yes, encrypted
no
no
Lifetime
until someone rotates it
until someone rotates it
minutes to hours
Revocation
rotate and redeploy everywhere
rotate at the store
expires on its own
A leaked run log
exposes a live credential
exposes a live credential
exposes an expired one
Attribution
none
which identity fetched it
which identity, and this exact lease
Works when the store is down
yes
no
no
The fourth row is the one that reframes the whole part. Every leak route in
this part’s first lesson — the log file, the CI artifact, the pasted
terminal, the --diff output — leaks a credential that has already expired
if the credential lived for an hour. The exposure does not disappear, but
its value decays to nothing without anybody doing anything.
Note also that dynamic credentials do not remove the need for no_log,
Vault or output discipline. They shorten the window during which a leak
matters. That is a different kind of protection from the others in this
part, and it composes with them rather than replacing them.
Knowledge check
Knowledge check · 4 questions
Q1. Why does retrieving secrets with a lookup not require the managed nodes to reach the secret store?
Q2. What does a dynamic credential with a one-hour lease change about the leak routes enumerated at the start of this part?
Q3. Which are genuine costs of moving secrets to an external store? Select all that apply.
Q4. Because the community.hashi_vault KV lookups return the secret value directly as a string, it can be used wherever a password is expected without further indexing.
Passing score: 75%. Answers are checked in this browser.