Skip to main content
RunBook Academy

AnsibleVII · Ad-Hoc ExecutionAd-hoc execution

What an ad-hoc change leaves behind

Intermediate⏱ ~17 minansible

What you'll learn

  • Enumerate what an ad-hoc run does and does not record by default
  • Configure a controller log path and result capture as compensating controls
  • Explain why shell history is not an audit trail
  • State the course position on ad-hoc inspection versus ad-hoc change

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Three weeks after the change, someone asks: who reconfigured the web tier on the 14th, what exactly did they change, and was it reviewed?

If the change was a playbook, the answer is a commit hash. If it was an ad-hoc command, the honest answer is that nobody knows, and the reconstruction that follows will take a day and end in a guess.

This lesson establishes the position the rest of the course takes: an ad-hoc inspection is fine forever. An ad-hoc change is a debt, and the interest is paid during the incident review.

What is actually recorded

By default, an ad-hoc run writes nothing anywhere. Enumerate the places you might look:

Where you might lookWhat is there
A log on the controllerNothing, unless log_path is set. It is unset by default.
A log on the managed nodeNothing from Ansible. sudo logs the escalation if -b was used.
Version controlNothing. There was no file.
A change ticketOnly if a human wrote one.
The terminalEverything, until the terminal closes.
Shell historyThe command line, on one workstation, for one user.

The last row is the one people lean on, and it is worth being precise about why it does not work.

The four questions a change record must answer

An incident review asks these. Test any record you have against all four.

  1. Who ran it?
  2. What did it do — not what was typed, but what changed on each host?
  3. Where — which hosts, exactly, and which of them actually changed?
  4. What was the previous state, so it can be restored?

A playbook in Git answers 1 and 2 from the commit, 3 from the recap and the inventory at that revision, and 4 from the diff plus whatever the change replaced.

Shell history answers a corrupted version of 2 and nothing else. Note particularly that question 4 is unanswerable by any ad-hoc mechanism. No module you ran recorded the prior value; lineinfile replaced a line and did not keep the old one unless you passed backup=yes, which nobody does at a prompt.

The two compensating controls

If you are going to run ad-hoc changes — and every estate does, sometimes — these are the mechanisms that make it partially defensible. Neither is on by default.

log_path: the controller writes a log

Read-only / Safelog every ad-hoc run
$ ANSIBLE_LOG_PATH=./adhoc.log ansible localhost -m ansible.builtin.ping
Read-only / Safewhat it wrote
$ cat ./adhoc.log
2026-08-11 21:01:05,787 p=4037136 u=opsuser n=ansible WARNING| [WARNING]: No inventory was parsed, only implicit localhost is available

2026-08-11 21:01:05,989 p=4037136 u=opsuser n=ansible INFO| localhost | SUCCESS => {
  "changed": false,
  "ping": "pong"
}

Illustrative output

Set it permanently in the project ansible.cfg so every operator on the controller gets it without thinking:

[defaults]
log_path = /var/log/ansible/ansible.log
Read-only / Safeconfirm it is in effect
$ ansible-config dump | grep DEFAULT_LOG_PATH
DEFAULT_LOG_PATH(/home/opsuser/estate/ansible.cfg) = /var/log/ansible/ansible.log

Illustrative output

What this buys: attribution and timing, for runs made on that controller by that config. It records u= — the local user on the controller — and the per-host results.

What it does not buy: anything about a run made from a laptop, from a different directory with a different ansible.cfg, or with ANSIBLE_LOG_PATH unset in the environment. It is a controller-local convention, not an enforcement mechanism, and an operator who bypasses it leaves no trace of having done so.

--tree: the results become files

Read-only / Safecapture per-host results
$ ansible production -i inventory.ini -m ansible.builtin.setup -a "filter=ansible_kernel" --tree ./run-2026-08-11

This is the only ad-hoc mechanism that captures the outcome rather than the intent. One file per host that answered, containing exactly what the module returned — including changed, so you can tell which hosts actually moved.

It answers question 3 properly, which shell history cannot touch. It still says nothing about question 4.

What the auditor is actually asking

Compliance regimes phrase it differently, but the question is the same one an incident review asks, and it has a shape:

Demonstrate that changes to production systems are authorised, recorded, and attributable to an individual.

Take that clause apart against an ad-hoc change:

  • Authorised — by whom? There was no review step. The authorisation and the execution were the same keystroke.
  • Recorded — where? See the table at the top of this lesson.
  • Attributable — to the shell history of a workstation, if it still exists, if the account was not shared, and if the timestamps were enabled.

None of the three survives. And the failure is not that the change was wrong. It may have been exactly right. The failure is that you cannot demonstrate anything about it, which under most regimes is treated identically to not having controlled it at all.

Knowledge check

Knowledge check · 4 questions

  1. Q1. An ad-hoc change was made with `-b` three weeks ago. Which record, present by default, comes closest to attributing it?

  2. Q2. Setting log_path in the project ansible.cfg means every ad-hoc change against this estate will be logged.

  3. Q3. Which questions can --tree output answer that shell history cannot? Select all that apply.

  4. Q4. Why does the course treat ad-hoc inspection as permanently acceptable while treating ad-hoc change as a debt?

Passing score: 75%. Answers are checked in this browser.