I have deployed an Azure Linux VM and installed the AADSSHLoginForLinux VM extension. This allows me to login to the VM using my Azure credentials, and allows me to configure role based access for my team using EntraID (Active Directory) groups. Any team member can log into the VM using the following command, without needing to mess with SSH keys, etc, and they will get the appropriate access (sudo or regular user) depending on what EntraID group they belong to, using the azure-cli ssh wrapper:
az ssh vm --ip 1.2.3.4
(Assuming they have previously run as login
at some point.)
Now I want to use this as the ssh
command for Ansible, which will automatically allow anyone in the admin security group to deploy the playbook.
az ssh vm
will pass through ssh arguments supplied following --
.
For example:
az ssh vm --ip 1.2.3.4 -- -p 23
So it should be possible to wrap az ssh vm
up in such a way that Ansible can use it.
I have found the ssh_executable option for Ansible, but this expects a command, not a command with arguments, so I can't simply set it to az ssh vm ...
.
So a wrapper script will be necessary.
I have also determined that Microsoft do not supply an equivalent wrapper for scp
, so this will need to be worked around.
How can I put all the pieces together to easily deploy Ansible playbooks using my Azure credentials and az ssh
?
I've slightly adapted the Python wrapper below and published it on PyPI as az-ssh-wrapper. After installing, it provides the SSH wrapper command az-ssh
, which can be used exactly like regular SSH, but will use az ssh vm
behind the scenes.
TL;DR:
pipx install az-ssh-wrapper
az-ssh
See original answer below for details about using this with Ansible.
The first step is to create an SSH wrapper which will intercept the SSH arguments from Ansible and pass them through to az ssh
.
This blog post describes how to do this for GCP IAP. I have adapted it to work with az ssh
:
I am using the following wrapper, adapted from the blog post above:
#!/bin/bash
# ssh-wrapper.sh
#
# SSH Wrapper for az ssh
#
# We are using `az ssh vm --ip 1.2.3.4` to authenticate to VMs with
# Azure Role Based Athentication. However, Ansible doesn't directly
# support this.
#
# `az ssh vm` accepts additional SSH arguments, so this wrapper takes
# arguments from Ansible, and passes them onto `az ssh vm`.
# Wrapper adapted from here:
# https://blg.robot-house.us/posts/ansible-and-iap/
# Get last two arguments
host="${*: -2: 1}"
cmd="${*: -1: 1}"
# Filter out hard-coded Ansible SSH options.
# At least one of these seems to break az ssh, but I haven't figured out which.
# But it seems to work if you filter them all out as described in the blog linked above.
# This may cause problems if you need to pass your own SSH arguments,
# but for our use case it's OK.
# Only accept the options starting with '--'
declare -a opts
for s_arg in "${@: 1: $# -2}" ; do
if [[ "${s_arg}" == --* ]] ; then
opts+=("${s_arg}")
fi
done
exec az ssh vm --ip "${host}" -- "${opts[@]}" "${cmd}"
Save this script as ssh-wrapper.sh
in the same directory as your Ansible playbook.
Now, we need to configure the playbook to use the wrapper. We also need to configure Ansible to use piped
transfer rather than scp
or sftp
. This will pipe files through SSH, so scp
and sftp
are not required.
The start of your playbook should look something like this:
---
- name: Example
hosts: all
vars:
# Need piped transfer because az ssh vm doesn't support scp or sftp
ansible_ssh_transfer_method: piped
ansible_ssh_executable: ./ssh-wrapper.sh
tasks:
- name: Test
ansible.builtin.command: pwd
Assuming you have previously run az login
, and are able to login to the VM with az ssh login --ip 1.2.3.4
, you should be able to simply run the
Ansible playbook as follows:
ansible-playbook playbook.yml -i 1.2.3.4,
The following Python SSH wrapper has several advantages over the bash wrapper above:
az ssh
.#!/usr/bin/env python3
import getopt
import os
import shutil
import sys
def usage():
usage = """
SSH wrapper for az ssh
Usage:
This wrapper emulates OpenSSH, but translates the arguments and
supplies them to az ssh so that it can be used by Ansible as the
ansible_ssh_executable.
It also filters out ControlMaster and ControlPersist options, as these
appear to block for the duration of the specified ControlPersist value
when passed to az ssh. See the following GitHub issue for details:
https://github.com/Azure/azure-cli-extensions/issues/7285
This wrapper supports all arguments supported by OpenSSH 8.9p1:
az-ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
[-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
[-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
[-i identity_file] [-J [user@]host[:port]] [-L address]
[-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
[-Q query_option] [-R address] [-S ctl_path] [-W host:port]
[-w local_tun[:remote_tun]] destination [command [argument ...]]
See the ssh man page for details.
Example:
az-ssh 1.2.3.4
"""
print(usage)
def opt_filter(opt):
"""Filter out options which cause problems for az ssh
For some reason, setting the Control* options causes az ssh to hang
for the duration of ControlPersist"""
return not (opt[0] == "-o" and opt[1].startswith("Control"))
def main():
options = "46AaCfGgKkMNnqsTtVvXxYyB:b:c:D:E:e:f:I:i:J:L:l:m:O:o:p:Q:R:S:W:w:"
try:
opts, args = getopt.getopt(sys.argv[1:], options)
destination = args.pop(0)
except (getopt.GetoptError, IndexError) as err:
print(f"Error: {err}")
usage()
sys.exit(2)
az_path = shutil.which("az")
# Work around quoting issue on Windows (still works on Linux)
az_path = f'"{az_path}"'
ssh_opts = []
for opt in filter(opt_filter, opts):
# Only keep truthy options. e.g.
# ('-t', '') --> ('t',)
# (Yes, this is important.)
ssh_opts += filter(lambda o: o, opt)
exec_args = [az_path, "ssh", "vm", "--ip", destination]
if ssh_opts or args:
exec_args.extend(["--", *ssh_opts, *args])
os.execvp("az", exec_args)
if __name__ == "__main__":
main()