AnsibleXLVII · Controller SecurityController security
Keys that are only good for automation
What you'll learn
- Separate automation keys from human keys, and state what each separation buys
- Apply authorized_keys restrictions and explain why they are the only control that survives controller compromise
- Choose between a passphrase-protected key with an agent and an unattended key, on evidence
- Recognise the single shared root key as the anti-pattern it is, and cost its removal
- Explain how SSH certificates change key distribution and revocation at fleet scale
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
Part XX covered how Ansible uses SSH keys: ansible_ssh_private_key_file
per group, the agent settings that make an unattended run possible, and
why separating keys by what they may reach is better than separating
them by who is using them.
This lesson is about the key as a credential to be governed: where it comes from, what it may do once it exists, and how you take it away. Those questions are answered on the managed node, not on the controller, which is the entire reason they matter after lesson 1.
One key, one purpose
An automation key should have exactly one job and no human should ever use it.
That sounds like a hygiene rule. It is actually an accountability one.
The managed node’s sshd log records the fingerprint of the key that
authenticated, not the name of a person. If one key is used both by the
scheduler and by three engineers logging in to look at something, that
fingerprint means “somebody, doing something”. If it is used only by the
scheduled run, the same log line is a fact.
So the estate wants at least these separations:
Automation from human. Engineers authenticate with their own keys or the organisation’s identity system; automation uses keys no person holds.
Environment from environment. A staging key that cannot open a production host is the control that makes a staging compromise survivable. Part XX establishes the mechanism, lesson 3 the organisational case; this is where it becomes a property of the key material itself.
Purpose from purpose, where the purposes differ in blast radius. A key used by a read-only compliance scan is not the key used by the patch run. Constraining the first (see below) is easy and worth doing; the second cannot be meaningfully constrained, which is itself informative.
Constraining the key at the far end
authorized_keys supports options that prefix a key entry and restrict
what a session authenticated by it may do. These are enforced by sshd
on the managed node.
That sentence is the important one. Everything else in this part is
configuration the controller applies to itself, and an attacker holding
the controller changes it. authorized_keys options are read by
software on the target that does not trust the controller, so they are
the only constraint in this course that survives the compromise lesson 1
describes.
The options worth knowing:
| Option | Effect |
|---|---|
from="pattern-list" | The key is only accepted from matching source addresses or hostnames |
command="..." | Any session using this key runs this command, ignoring what the client asked for |
restrict | Disables all forwarding, agent forwarding, port forwarding, X11 and PTY allocation |
no-agent-forwarding | Individually disables agent forwarding |
no-port-forwarding | Individually disables port forwarding |
expiry-time="YYYYMMDD" | The key stops being accepted after this time |
A restricted entry for a read-only monitoring key looks like this:
restrict,from="192.0.2.10",command="/usr/local/bin/collect-facts" ssh-ed25519 AAAA...REPLACE_ME monitoring@controller
restrict is the right default because it is a deny-by-default: it
turns off everything and you re-enable what you need with
pty, port-forwarding and similar. Listing individual no-* options
instead means every future OpenSSH feature is enabled on your key until
somebody adds another no-.
Passphrase, agent, or unattended
The trade is genuine and the honest framing is that both options are defensible for different runs.
Passphrase plus agent. The key on disk is useless without the
passphrase. An engineer unlocks it once per session into ssh-agent,
and Ansible uses the agent. This is correct for interactive work and
costs nothing operationally.
It does not solve scheduled runs, because there is no human present to
type a passphrase at 03:00. Part XX covers what ansible-core 2.21
offers here — the ssh_agent, private_key and private_key_passphrase
settings on the ssh connection plugin — which give a real answer to
running unattended without a passphrase-free key sitting on disk.
Unattended key with no passphrase. The key on disk is sufficient by itself. This is what most scheduled automation actually uses, and pretending otherwise produces advice nobody follows.
If you are here, be clear about what protects the key: it is filesystem
permissions and the security of the controller, and nothing else. That
is precisely why the previous three lessons exist, and it is the
strongest argument for restricting the key with from= — because the
one thing you can add is that a stolen copy does not work elsewhere.
Certificates, and why they are the scalable answer
Everything above manages access by distributing public keys to every host. That approach has a hard ceiling, and the ceiling is revocation.
To remove a key you must edit authorized_keys on every host that has
it. Hosts that are down during that run keep the key. There is no
central record of which hosts currently trust which keys, other than by
asking all of them.
SSH certificates invert this. A certificate authority signs a user
certificate; hosts are configured once to trust the CA
(TrustedUserCAKeys in sshd_config); and the certificate carries its
own constraints — a validity period, a list of principals, and
force-command or source-address restrictions embedded in it by the CA
rather than by an entry on each host.
The consequences at fleet scale:
Revocation becomes expiry. An automation certificate valid for twenty-four hours does not need to be revoked; it needs to not be renewed. That converts the hardest operation in key management into the absence of an operation.
Adding a host is one configuration line, not a distribution run. The host trusts the CA and therefore trusts every current certificate.
The constraints travel with the credential. Principals and validity are signed into the certificate, so they cannot be edited on one host and forgotten on another.
The cost is a CA to run, protect and make highly available, because a
CA outage means nobody can obtain a certificate. That cost is real and
it is why certificates are a fleet-scale answer rather than a starting
point. On thirty hosts, authorized_keys and a rotation runbook are
proportionate. On three thousand, the rotation runbook is the problem
and certificates are the answer.
Knowledge check
Knowledge check · 4 questions
Q1. Why are authorized_keys options the only control in this part that survives compromise of the controller?
Q2. Which authorized_keys restriction applies usefully to a general Ansible automation key?
Q3. What changes when a fleet moves from distributed authorized_keys to SSH certificates? Select all that apply.
Q4. Adding restrict to the first entry of an authorized_keys file constrains every key in that file.
Passing score: 75%. Answers are checked in this browser.