Skip to content

Instantly share code, notes, and snippets.

@haplo
Created April 7, 2026 09:53
Show Gist options
  • Select an option

  • Save haplo/5a6a1194b6f84ec3dad2429d1b3af58b to your computer and use it in GitHub Desktop.

Select an option

Save haplo/5a6a1194b6f84ec3dad2429d1b3af58b to your computer and use it in GitHub Desktop.
Wrapper script for ansible-playbook that pre-establishes reusable SSH connections
#!/usr/bin/env bash
# ansible-ssh-masters.sh
#
# Establish one SSH master connection per host, keep it alive with a
# remote sleep, run ansible-playbook with all original arguments, then
# clean up only the masters this script started.
#
# Useful when SSH authentication has a per-connection human cost:
# hardware security keys, passphrase prompts, FIDO2/U2F, etc.
#
# You need to configure SSH and Ansible to use these connections.
#
# Add this to ~/.ssh/config:
#
# Host *
# ControlMaster auto
# ControlPersist 10m
# ControlPath ~/.ssh/sockets/%h-%r-%p
# ForwardAgent no
# ForwardX11 no
# GatewayPorts no
#
# Add this to ansible.cfg:
#
# [ssh_connection]
# ssh_args = -o ControlMaster=auto
# control_path_dir = ~/.ssh/sockets
# control_path = %(directory)s/%%h-%%r-%%p
# pipelining = True
#
# Usage (timeout in seconds):
# ANSIBLE_SSH_MASTERS_TIMEOUT=3600 ./ansible-ssh-masters.sh playbook.yml [args...]
set -euo pipefail
TIMEOUT_SECONDS="${ANSIBLE_SSH_MASTERS_TIMEOUT:-3600}"
SOCKET_DIR="${HOME}/.ssh/sockets"
PIDS=()
STARTED_HOSTS=()
ARGS=("$@")
INVENTORY="inventory"
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
err() { echo -e "${RED}[-]${NC} $*" >&2; }
parse_inventory_arg() {
local i
for ((i=0; i<${#ARGS[@]}; i++)); do
case "${ARGS[$i]}" in
-i|--inventory)
if (( i + 1 >= ${#ARGS[@]} )); then
err "Missing value after ${ARGS[$i]}"
exit 1
fi
INVENTORY="${ARGS[$((i + 1))]}"
;;
--inventory=*)
INVENTORY="${ARGS[$i]#--inventory=}"
;;
esac
done
}
cleanup() {
echo
log "Closing master connections..."
local i pid host
for i in "${!PIDS[@]}"; do
pid="${PIDS[$i]}"
host="${STARTED_HOSTS[$i]}"
if kill -0 "$pid" 2>/dev/null; then
if kill "$pid" 2>/dev/null; then
wait "$pid" 2>/dev/null || true
log "Closed master for ${host} (pid ${pid})"
else
err "Failed to kill master for ${host} (pid ${pid})"
fi
else
warn "Master for ${host} was already gone (pid ${pid})"
fi
done
log "Done."
}
trap cleanup EXIT INT TERM
parse_inventory_arg
mkdir -p "$SOCKET_DIR"
chmod 0700 "$SOCKET_DIR"
# Remove stale sockets (no process behind them)
find "$SOCKET_DIR" -maxdepth 1 -type s ! -exec fuser -s {} \; -delete 2>/dev/null || true
if ! ansible-inventory -i "$INVENTORY" --graph >/dev/null 2>&1; then
err "Inventory could not be parsed: $INVENTORY"
exit 1
fi
mapfile -t HOSTS < <(
ansible-inventory -i "$INVENTORY" --graph |
awk '
/@/ { next }
{
line = $0
gsub(/^[[:space:]|]+/, "", line)
sub(/^--/, "", line)
if (line != "") {
print line
}
}
' |
sort -u
)
if (( ${#HOSTS[@]} == 0 )); then
err "No hosts found in inventory: $INVENTORY"
exit 1
fi
log "Found ${#HOSTS[@]} hosts in inventory: $INVENTORY"
log "Per-host connection lifetime: ${TIMEOUT_SECONDS} seconds"
log "Establishing master connections (authentication may be required)..."
echo
count=0
for host in "${HOSTS[@]}"; do
((count+=1))
if ssh -O check -o "ControlPath=${SOCKET_DIR}/%h-%r-%p" "$host" >/dev/null 2>&1; then
log "[$count/${#HOSTS[@]}] ${host}: reusing existing master"
continue
fi
warn "[$count/${#HOSTS[@]}] ${host}: connecting..."
ssh \
-o ControlMaster=yes \
-o "ControlPath=${SOCKET_DIR}/%h-%r-%p" \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ForwardAgent=no \
-o ForwardX11=no \
-o ClearAllForwardings=yes \
"$host" "sleep ${TIMEOUT_SECONDS}" &
pid=$!
for _ in $(seq 1 15); do
if ssh -O check -o "ControlPath=${SOCKET_DIR}/%h-%r-%p" "$host" >/dev/null 2>&1; then
break
fi
sleep 1
done
if ssh -O check -o "ControlPath=${SOCKET_DIR}/%h-%r-%p" "$host" >/dev/null 2>&1; then
log "[$count/${#HOSTS[@]}] ${host}: master established (pid ${pid})"
PIDS+=("$pid")
STARTED_HOSTS+=("$host")
else
err "[$count/${#HOSTS[@]}] ${host}: failed to establish master"
if kill -0 "$pid" 2>/dev/null; then
if kill "$pid" 2>/dev/null; then
wait "$pid" 2>/dev/null || true
else
err "[$count/${#HOSTS[@]}] ${host}: failed to stop failed SSH process (pid ${pid})"
fi
fi
fi
echo
done
echo
log "Running: ansible-playbook $*"
set +e
ansible-playbook "$@"
ANSIBLE_EXIT=$?
set -e
exit "$ANSIBLE_EXIT"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment