Use `az ssh vm` (AADSSHLoginForLinux) with Ansible

180 views Asked by At

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?

1

There are 1 answers

0
daviewales On BEST ANSWER

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,

Edit: Python version of wrapper

The following Python SSH wrapper has several advantages over the bash wrapper above:

  1. You can wrap it in a Python package and pip install it
  2. It knows what the valid SSH options are
  3. It only removes options which are known to cause problems with az ssh.
#!/usr/bin/env python3
# ssh-wrapper.py

import getopt
import os
import shutil
import sys


def usage():
    usage = """
SSH wrapper for az ssh

Usage:

az ssh accepts ssh arguments passed after --.
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.

This wrapper supports all arguments supported by OpenSSH 8.9p1:
$ ssh
usage: 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.
"""
    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)
    except getopt.GetoptError as err:
        print(err)
        usage()
        sys.exit(2)

    destination = args.pop(0)

    az_path = shutil.which("az")
    ssh_opts = [" ".join(opt) for opt in filter(opt_filter, opts)]
    exec_args = [az_path, "ssh", "vm", "--ip", destination, "--", *ssh_opts, *args]

    os.execvp("az", exec_args)


if __name__ == "__main__":
    main()