13

I have enabled two factor authentication for ssh using duosecurity (using this playbook https://github.com/CoffeeAndCode/ansible-duo ).

How can I use ansible to manage the server now. The SSH calls fail at gathering facts because of this. I want the person running the playbook to enter the two factor code before the playbook is run.

Disabling two factor for the deployment user is a possible solution but creates a security issue which I would I like to avoid.

Rico
  • 48,741
  • 12
  • 84
  • 107
Harshil Mathur
  • 166
  • 1
  • 3
  • 7
  • 1
    In almost every case I can think of, it'd be a better design choice to lock down a bastion host inside your network with 2 factor auth, and allow Ansible to ssh without it within that network. As the (technically on point!) answer below says, any solution involving that much heavy lifting to set up is gonna be so painful at scale it'l remove much of the advantage of using Ansible in the first place. – nikobelia Jul 27 '16 at 22:12

3 Answers3

7

It's a hack, but you can tunnel a non-2fac Ansible SSH connection through a 2fac-enabled SSH connection.

Overview

We will setup two users: ansible will be the user Ansible will use. It should be authenticated in a way that's supported by Ansible (i.e., not 2fac). This user will be restricted so it cannot connect from anywhere but 127.0.0.1, so it is not accessible from outside the machine.

The second user, ansible_tunnel will be open to the outside world, but will be authenticated by two factors, and will only allow tunneling of SSH connections to the local machine.

You must be able to configure 2-factor authentication only for some users (not all).

Some info on SSH tunnels.

On the target machine:

  1. Create two users: ansible and ansible_tunnel
  2. Put your public key in ~/.ssh/authorized_keys of both users
  3. Set the shell of ansible_tunnel to /bin/false, or lock the user - it will be used for tunneling exclusively, not running commands
  4. Add the following to /etc/ssh/sshd_config:

    AllowTcpForwarding no
    
    AllowUsers ansible@127.0.0.1 ansible_tunnel
    
    Match User ansible_tunnel
      AllowTcpForwarding yes
      PermitOpen 127.0.0.1:22
      ForceCommand echo 'This account can only be used for tunneling SSH sessions'
    
  5. Setup 2-factor authentication only for ansible_tunnel
  6. Restart sshd

On the machine running Ansible:

  1. Before running Ansible, run the following (on the Ansible machine, not the target):

    ssh -N -L 8022:127.0.0.1:22 ansible_tunnel@<host>
    

    You will be authenticated using two factors.

  2. Once the tunnel is up (check with netstat), run Ansible with ansible_ssh_user=ansible, ansible_ssh_port=8022 and ansible_ssh_host=localhost.

Recap

  • Only ansible_tunnel can connect from the outside, and it will be authenticated using two factors
  • Once the tunnel is set up, connecting to port 8022 on the local machine is the same as connecting to sshd on the remote machine
  • We're allowing ansible to connect over SSH only when it is done through the localhost, so only connections that are tunneled are allowed

Scale

This will not scale well for multiple server, due to the need to open a separate tunnel for each machine, which requires manual action. However, if you've chosen 2-factor authentication for your servers you're already willing to do some manual action to connect to each server, and this solution will only add a little overhead with some script-wrapping.

[EDITED TO ADD]

Bonus

For convenience, we may want to log into the maintenance account directly to do some manual work, without going through the process of setting up a tunnel. We can configure SSH to require 2fac authentication in this case, while maintaining the ability to connect without 2fac through the tunnel:

# All users must authenticate using two factors
AuthenticationMethods publickey,keyboard-interactive

# Allow both maintenance user and tunnel user with no restrictions
AllowUsers ansible ansible_tunnel

# The maintenance user is allowed to authenticate using a single factor only
# when connecting from a local address - it should be impossible to connect to
# this user using a single factor from the outside (the only way to do that is
# having an existing access to the machine, or use the two-factor tunnel)
Match User ansible Address 127.0.0.1
  AuthenticationMethods publickey
duvduv
  • 609
  • 6
  • 10
  • Great answer, it is kind of a hack but this seems like a better solution than anything I've been able to find on the ansible mailing lists or github issues. What's the benefit of having separate users, with the ansible_tunnel user set to tunnel only? Seems like you could allow every user to use only publickey auth from 127.0.0.1, then there's only one user involved and the actual maintenance commands run as yourself. – danny Feb 13 '17 at 06:39
  • I think you can set up a single user to allow 2fac-only from outside, but pubkey-only from 127.0.0.1, and use that user for both the tunneling and ansible (i.e., tunnel that user to itself, haha). I just like to keep the roles separate - in case I misconfigured SSH for the tunnel user, it still isn't allowed to run commands. – duvduv Feb 14 '17 at 09:40
3

Solution using a Bastion Host

Even using an ssh bastion host it took me quite a while to get this working. In case it helps anyone else, here's what I came up with. It uses the ControlMaster ssh config options and since ansible uses regular ssh it can be configured to use the same ssh features and re-use the connection to the bastion host regardless of how many connections it opens to remote hosts. I've seen these Control options recommended in general (presumably for performance reasons if you have a lot of hosts) but not in the context of 2FA to a bastion host.

With this approach you don't need any sshd config changes, so you'll want AuthenticationMethods publickey,keyboard-interactive as the only authentication method setting on the bastion server, and publickey only for all your other servers that you're proxying through the bastion to get to. Since the bastion host is the only one that accepts external connections from the internet, it's the only one that requires 2FA, and internal hosts rely on agent forwarding for public key authentication but don't use 2FA.

On the client, I created a new ssh config file for my ansible environment in the top-level directory that I run ansible from (so sibling of ansible.cfg) called ssh.config. It contains:

Host bastion-persistent-connection
  HostName <bastion host>
  ForwardAgent yes
  IdentityFile ~/.ssh/my-key
  ControlMaster auto
  ControlPath ~/.ssh/ansible-%r@%h:%p
  ControlPersist 10m

Host 10.0.*.*
  ProxyCommand ssh -W %h:%p bastion-persistent-connection -F ./ssh.config
  IdentityFile ~/.ssh/my-key

Then in ansible.cfg I have:

[ssh_connection]
ssh_args = -F ./ssh.config

A few things to note:

  • My private subnet in this case is 10.0.0.0/16 which maps to the host wildcard option above. The bastion proxies all ssh connections to servers on this subnet.

  • This is a bit brittle in that I can only run my ssh or ansible commands in this directory, because of the ProxyCommand passing the local path to this config file. Unfortunately I don't think there's an ssh variable that maps to the current config file being used so that I could pass the same config file to the ProxyCommand automatically. Depending on your environment it might be better to use an absolute path for this.

The one gotcha is it makes running ansible more complex. Unfortunately, from what I can tell ansible has no support whatsoever for 2FA. So if you have no existing ssh connection to the bastion, ansible will print out Verification code: once for every private server it's connecting to, but it's not actually listening for the input so no matter what you do the connections will fail.

So I first run: ssh -F ssh.config bastion-persistent-connection

This creates the socket file in ~/.ssh/ansible-*, and the ssh agent locally will close & remove that socket after the configurable time (what I have set to 10m).

Once the socket is open I can run ansible commands like normal, e.g. ansible all -m ping and they succeed.

danny
  • 8,684
  • 10
  • 47
  • 56
  • Minor tweak: I ended up using `ControlPath ~/.ansible/cp/ansible-ssh-%h-%p-%r`, which corresponds to Ansible's default `control_path`. http://docs.ansible.com/ansible/intro_configuration.html#control-path – Aidan Feldman Mar 10 '17 at 04:30
3

I can use ansible with ssh and 2FA using the ControlMaster feature of ssh and ansible.

My local ssh client is configured to dump a ControlPath socket for multiplexing connection. Ansible is configured to use the same socket.

Local ssh client

This configuration enable multiplexing for all connections. I personnaly store this configuration in `~/.ssh/config:

Host *
    ControlMaster auto
    ControlPath ~/.ssh/master-%r@%h:%p.socket
    ControlPersist 1m

When a connection is established, a socket appears in the $HOME/.ssh directory. This sockets persists during one minute after disconnection.

Configure ansible

Ansible is configured to re-use the local socket.

Add this in your ansible configuration file (for instance, ~/.ansible.cfg):

[ssh_connection]

control_path=~/.ssh/master-%%r@%%h:%%p.socket

Note the double % for varialble subsitution.

Usage

  1. Connect to your server using ssh regular command (ssh user@server), and performs 2FA;
  2. Launch your ansible command as usual.

The step 2 must be performed within the ControlPersist configuration, or keep an ssh connection in a terminal when you launch ansible command in another one.

You can also force to close connection when you do not need it, using: ssh -O exit user@server.

Note that, if you open a third terminal and run ssh user@server, you will not be asked for credentials: the connection established in 1. will be re-used.

Drawbacks

In case of bad network conditions

Sometimes, when you loose connection, the socket persists. Every further connection hangs. You must manually disconnect this connection, using ssh -O exit user@server. This is the only known drawback for this method.

References:

Julien Fastré
  • 828
  • 10
  • 18