Skip to main content
RunBook Academy

AnsibleXX · SSH Architecture and ConnectivitySSH architecture and connectivity

Pipelining and its preconditions

Advanced⏱ ~22 minansible-coreopenssh-client

What you'll learn

  • Explain the -tt decision the ssh connection plugin makes and why pipelining changes it
  • Identify use_tty as a second, independent route to the same requiretty failure
  • Establish whether requiretty is set across a fleet before enabling pipelining, not after
  • Roll pipelining out per group with ansible_ssh_pipelining and a reversible plan

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.

The architecture part of this course established what pipelining does: instead of writing the module wrapper to a file on the target and then executing that file, Ansible feeds the wrapper to the remote Python interpreter on standard input. The mkdir, PUT, chmod and rm disappear, and one task becomes one round trip.

This lesson is about the precondition — because “pipelining conflicts with privilege escalation” is repeated everywhere and is, as usually stated, slightly wrong in a way that makes it hard to act on.

What the documentation actually says

Read-only / Safethe setting, in its own words
$ ansible-doc -t connection ansible.builtin.ssh
   pipelining  Pipelining reduces the number of connection operations
             required to execute a module on the remote server, by
             executing many Ansible modules without actual file
             transfers.
             This can result in a very significant performance
             improvement when enabled.
             However this can conflict with privilege escalation
             ('become'). For example, when using sudo operations you
             must first disable 'requiretty' in the sudoers file for
             the target hosts, which is why this feature is disabled
             by default.
      default: false

Two things to take from that. The default is false because of the requiretty interaction, not because pipelining is risky in itself. And the words “for example” are doing real work — sudo is an instance of the problem, not the problem.

The actual mechanism: one line in the connection plugin

The conflict is not between pipelining and sudo. It is between pipelining and a pseudo-terminal, and the decision is made explicitly, here, in ansible/plugins/connection/ssh.py on 2.21.3:

# we can only use tty when we are not pipelining the modules. piping
# data into /usr/bin/python inside a tty automatically invokes the
# python interactive-mode but the modules are not compatible with the
# interactive-mode ("unexpected indent" mainly because of empty lines)

# -tt can cause various issues in some environments so allow the user
# to disable it as a troubleshooting method.
use_tty = self.get_option('use_tty')

args: tuple[str, ...]
if not in_data and sudoable and use_tty:
    args = ('-tt', self.host, cmd)
else:
    args = (self.host, cmd)

in_data is the pipelined payload. So the condition reads: allocate a pseudo-terminal only when there is no payload on stdin, the command is one that might need to escalate, and use_tty is on.

Follow the causal chain, because the order matters:

  1. By default, Ansible passes -tt to ssh, which forces allocation of a pseudo-terminal on the target.
  2. sudo with Defaults requiretty refuses to run without a TTY. With -tt present, there is one, so it runs.
  3. Pipelining puts the module on stdin, so in_data is set, so the condition is false, so -tt is not passed.
  4. sudo now has no TTY and refuses.

The comment explains why step 3 has to work that way, and it has nothing to do with sudo at all: piping a script into an interactive Python inside a TTY makes Python treat it as interactive input, and the module source — which contains blank lines — gets rejected with unexpected indent. The two features are mutually exclusive for a reason internal to Python, and sudo is a bystander that happens to depend on the flag that gets dropped.

Establishing the precondition before you change anything

The architecture part described what happens when a team enables pipelining globally and a third of a mixed-age fleet stops working. The reason that scenario is so common is that people establish the precondition retrospectively, from the failures.

You can establish it in advance, read-only, in one run.

Read-only / Safesurvey requiretty across the fleet
# playbooks/preflight-requiretty.yml
# READ-ONLY: reads sudo policy and reports it. Changes nothing.
# Uses become because sudoers is only readable with privilege.
- name: Survey sudo TTY policy before enabling pipelining
hosts: all
gather_facts: false
become: true
tasks:
  - name: Ask sudo for the effective defaults
    ansible.builtin.command:
      argv: [sudo, -n, sudo, -V]
    register: sudo_v
    changed_when: false
    failed_when: false

  - name: Report hosts whose sudo policy requires a tty
    ansible.builtin.debug:
      msg: "requiretty is in effect"
    when: "'requiretty' in sudo_v.stdout"

That is one approach and it has a weakness worth naming: sudo -V reports the compiled defaults and the settings it can see, and a requiretty that applies only to a specific user or command via a Defaults: specification may not show up. The more reliable survey reads the policy files:

Read-only / Safethe direct read
    - name: Search sudoers and its drop-in directory
    ansible.builtin.shell:
      cmd: grep -RIl '^[[:space:]]*Defaults.*requiretty' /etc/sudoers /etc/sudoers.d/ || true
    register: rt
    changed_when: false

  - name: Hosts that will break under pipelining
    ansible.builtin.debug:
      var: rt.stdout_lines
    when: rt.stdout_lines | length > 0

|| true because grep exits non-zero when it finds nothing, and “nothing found” is the answer you are hoping for rather than a failure. The result is a list of hosts, which turns “a third of the fleet broke” into “these 47 hosts need attention first” — the same information, obtained before the outage instead of during it.

Rolling it out per group

pipelining is settable per host and per group, which makes a staged rollout possible without touching sudo policy under time pressure:

Read-only / Safesix places it can be set, and only two of them scope
$ ansible-doc -t connection ansible.builtin.ssh
   pipelining
      set_via:
        env:
        - name: ANSIBLE_PIPELINING
        - name: ANSIBLE_SSH_PIPELINING
        ini:
        - key: pipelining
          section: defaults
        - key: pipelining
          section: connection
        - key: pipelining
          section: ssh_connection
        vars:
        - name: ansible_pipelining
        - name: ansible_ssh_pipelining
      default: false
Configuration changeon by default, off where the survey said so
# inventory/production/group_vars/all.yml
ansible_ssh_pipelining: true

# inventory/production/group_vars/legacy_sudoers.yml
# 47 hosts carrying Defaults requiretty from the 2019 hardening baseline.
# Surveyed 2026-08-11. Remove this group as hosts are rebuilt; the
# replacement images do not set it. Tracked as OPS-4471.
ansible_ssh_pipelining: false

The exception group carries what every exception in this course carries: a count, a reason, a date, an owner, and the condition under which it disappears. An exception with an expiry becomes smaller over time. An exception without one becomes permanent and eventually nobody remembers which hosts depended on it.

Note the direction of the default. Pipelining on for the fleet and off for the known exceptions is better than the reverse, because it makes the exception list visible and shrinking. The reverse — off globally, on for a few groups — hides the fact that most of your fleet is paying for a constraint that applies to 47 hosts.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Why does enabling pipelining break sudo on hosts that set Defaults requiretty?

  2. Q2. A fleet reports the sudo TTY error, but ansible-config dump shows pipelining is off. What should you check next?

  3. Q3. Which are sound reasons to survey requiretty across the fleet before enabling pipelining, rather than enabling it and seeing what breaks? Select all that apply.

  4. Q4. Enabling pipelining removes the module wrapper file from the managed node, which eliminates one of the places a credential passed as a module argument can be left behind.

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