Ansible Tricks: Windows Hosts with MFA or Pre-Authentication

In order to use your OTP to access Windows Hosts you have to use Kerberos.

In addition, this allows the passwords to be manually entered by the admin prior to execution.

💡
Your management console does not have to join the domain but must have access to at least on KDC (domain controller) on port 88.

Installing Kerberos Tools

  1. First, install krb5-user. This will install other associated packages.
    sudo apt install krb5-user
    1. During installation, it will ask for a default realm. This should be the domain you will normally use, in all caps.
  2. Configure /etc/krb5.conf file, if needed
    1. By default, DNS will be used to find the domain controllers so you aren't required to add your domain(s)
    2. You can specify the domain and associated KDCs manually in this config file.

Retrieving a TGT

This part is simple, just make sure the domain is in ALL CAPS:

  • kinit username@DOMAIN.COM
  • If using authlite this will be kinit <otp>@DOMAIN.COM
  • After you run the command, you will enter a password for the account

I use Authlite so my username is really the OTP (modified here slightly for anonymity)

paul@corp-ansible1:~/tamingitansible$ kinit lkajsidijenabduhfdhtvufjhlcvtektrtltlucndgvheceddhjvjfttcdlellhg@INTERNAL.TAMINGIT.INFO

Password for lkajsidijenabduhfdhtvufjhlcvtektrtltlucndgvheceddhjvjfttcdlellhg@INTERNAL.TAMINGIT.INFO:

You can use klist to verify that a token was received.

#List the Tickets:
paul@corp-ansible1:~/tamingitansible$ klist

Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: lkajsidijenabduhfdhtvufjhlcvtektrtltlucndgvheceddhjvjfttcdlellhg@INTERNAL.TAMINGIT.INFO

Valid starting       Expires              Service principal
07/12/2024 12:39:43  07/12/2024 22:39:43  krbtgt/INTERNAL.TAMINGIT.INFO@INTERNAL.TAMINGIT.INFO
        renew until 07/13/2024 12:39:35

Handling the OTP username issue

Windows will abstract away the OTP using the TGT. The encrypted ticket contains any group memberships and makes all this work. The only complication is that the username has changed due to the OTP code so you have to let ansible know.

There are options!

  1. You can simply modify the config file every time you use kinit to reflect the new username. This is a bit cumbersome.
  2. You can override ansible_user from the command line like this:
    ansible-playbook pb-configurehyperv.yml -e "ansible_user=<username here>"
    There are downsides to this, namely that you can't mix usernames if you are working with linux or non-MFA hosts.
  3. How I handle this... in the inventory configuration, where I'm setting the ansible connection type, I use a variable to represent the username and pass that variable on the command line. It works like this:

You can put it anywhere, but I have a group that I put all the domain hosts in and have the configuration in group_vars. It looks like this:

ansible_connection: winrm
ansible_winrm_transport: kerberos
ansible_winrm_server_cert_validation: ignore
ansible_user: "{{ otp_username }}"

Then I specify the value, which I always have to end up getting from kinit:

ansible-playbook pb-configurehyperv.yml -e "otp_name=lkajsidijenabduhfdhtvufjhlcvtektrtltlucndgvheceddhjvjfttcdlellhg@INTERNAL.TAMINGIT.INFO"

The value here MUST match the value in your keytab file. Do NOT regenerate OTP to get this value.

Multiple Domains

Ansible doesn't currently multiple keytabs

At this time I don't know if it is possible to tell Ansible to use a specific keytab file. I'm going to figure out how to fix that and get it made part of the official ansible but in the mean time, I have a work around.

Make it Work

First, it is important to have a group for each domain so things can be filtered

Inventory File

---
dmz_tamingit_info:
  hosts:
    192.168.1.101:
  children:
    webservers:

internal_tamingit_info:
  hosts:
    192.168.1.151:

nondomain:
  hosts:
    192.168.1.201:

Sample Inventory

In this file I have a host in internal.tamingit.info, a host that is not kerberos, such as linux or a non-domain windows server, and all webservers plus another host in dmz.tamingit.info. the webservers grouping is not shown so we have a shorter example to see.

I recommend you stick to using groups and not placing individual hosts in here. This also makes it simple to make domain-specific settings in group_vars.

My playbook file is unimportant but simply runs a win_ping against all hosts.

Keytabs

You can request your TGT into a keytab file like so:

kinit -c ~/dmz.tamingit.info.keytab <OTP>@DMZ.TAMINGIT.INFO
kinit -c ~/internal.tamingit.info.keytab <OTP>@INTERNAL.TAMINGIT.INFO

But keep in mind that Ansible will only use the default keytab location for these files. For most implementations that will be located at /tmp/krb5cc_$UID where $UID is your linux-based user number. So in order to run ansible for a domain you have to copy it there.

cp ~/dmz.tamingit.info.keytab /tmp/krb5cc_$UID

Finally, you need to run the playbook with a filter for the relevant domain using the -l parameter.

ansible-playbook test.yml -l dmz_tamingit_info -e "otp_name=<otp>@DMZ.TAMINGIT.INFO"

Scripting It

Here's a full script for running everything. Please note that I get Default Principal from the klist command dynamically for the sake of simplicity in using the script.

You can easily add or remove domains. Please stay aware of kerberos timeouts. If you're running a script that will take hours then it might cause an issue as they don't autorenew when doing it this way.

read -p "Enter the OTP for dmz.tamingit.info and press enter: " otp
echo "Enter the password for this account:"
kinit -c ~/dmz.tamingit.info.keytab $otp@DMZ.TAMINGIT.INFO

read -p "Enter the OTP for internal.tamingit.info and press enter: " otp
echo "Enter the password for this account:"
kinit -c ~/internal.tamingit.info.keytab $otp@INTERNAL.TAMINGIT.INFO

#Clear OTP.
export otp="" 

#Run non-domain items:
ansible-playbook test.yml -l nondomain

#Copy over the keytab for dmz.tamingit.info and run those.
cp ~/dmz.tamingit.info.keytab /tmp/krb5cc_$UID
otp=`klist | grep "Default principal:" | cut -d' ' -f3` #gets the OTP principal from the klist command.
ansible-playbook test.yml -l dmz_tamingit_info -e "otp_name=$otp"

#Copy over the keytab for internal.tamingit.info and run those.
cp ~/internal.tamingit.info.keytab /tmp/krb5cc_$UID
otp=`klist | grep "Default principal:" | cut -d' ' -f3` #gets the OTP principal from the klist command.
ansible-playbook test.yml -l internal_tamingit_info -e "otp_name=$otp"

#Cleanup the keytabs
rm ~/dmz.tamingit.info.keytab
rm ~/internal.tamingit.info.keytab
rm /tmp/krb5cc_$UID

You can also create the keytabs and create a cron job to run the rest of the script. Please be aware that kereberos has an expiration that you can view with klist

Resulting Keytab File

After doing all of this, your keytab file will have some new entries. Of course if you run my script they'll be deleted. I'm showing this so you can see how Ansible uses the keytab.

For example, I ran a playbook that checked several servers. After execution this is the result of running the klist command against my keytab:

Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: lkajsidijenabduhfdhtvufjhlcvtektrtltlucndgvheceddhjvjfttcdlellhg@INTERNAL.TAMINGIT.INFO

Valid starting       Expires              Service principal
07/12/2024 12:39:43  07/12/2024 22:39:43  krbtgt/INTERNAL.TAMINGIT.INFO@INTERNAL.TAMINGIT.INFO
        renew until 07/13/2024 12:39:35
07/12/2024 12:44:54  07/12/2024 22:39:43  HTTP/paul-vmtest.internal.tamingit.info@
        renew until 07/13/2024 12:39:35
        Ticket server: HTTP/paul-vmtest.internal.tamingit.info@INTERNAL.TAMINGIT.INFO
07/12/2024 13:50:08  07/12/2024 22:39:43  HTTP/server1.internal.tamingit.info@
        renew until 07/13/2024 12:39:35
        Ticket server: HTTP/server1.internal.tamingit.info@INTERNAL.TAMINGIT.INFO
07/12/2024 13:50:08  07/12/2024 22:39:43  HTTP/server2.internal.tamingit.info@
        renew until 07/13/2024 12:39:35
        Ticket server: HTTP/server2.internal.tamingit.info@INTERNAL.TAMINGIT.INFO
07/12/2024 13:50:08  07/12/2024 22:39:43  HTTP/server3.internal.tamingit.info@
        renew until 07/13/2024 12:39:35
        Ticket server: HTTP/server3.internal.tamingit.info@INTERNAL.TAMINGIT.INFO
When done, delete your keytab file. It is a key to the domain that does not require your password! It is protected so that only the user that ran kinit can modify the file by default, but don't let it hang around.