Ansible, Centrify and Kinit

Shell calling playbook calling shell

My current client is a Bank with strict and strong security compliance to various norms and enforced regulations. Therefore, its platforms are not open like what it can be found in public clouds or less strict on self hosted servers.

For example those kind of rules are enforced:

  • Only personal account can remotely log-in on servers, without use of any ssh key, password only.
  • Service account are all locals, and no direct remote log-in possible
  • No privilege escalation to root account
  • Use of Centrify to obtain token which allow remote connection on others servers.

In this context, i was asked to use Ansible to deploy and configure one of their most critical application (payment authorization platform) as a featured project to demonstrate that automation with modern tools is possible.

They already have a little team which played with Ansible. Given short delivery time and small budget, the result is blatantly unsatisfying for anyone experienced with Ansible: Shell script calling ansible-playbook which only relies on shell module.

In short, shell calling Ansible calling shell…

The main reason they are wrapping Ansible into shell script is to initiate the authentication scheme for Centrify withkinit.

But why ? Ansible can deal with this perfectly. Anything a shell-script can do, Ansible can do too.

Centrify in short

In big organisation, identity are often managed by big Windows AD server. This allow smart user management and right control on user desk computer.

But like in 99% of the time, critical services are run on more serious system, such as Unix or Linux. Which basically dont know a clue about Windows Active Directory. This is where Centrify enters the arena and bridges the two worlds.

One interesting feature, is the possibility to generate an authentication token, which allow you to connect without password to servers managed by centrify. ssh is able to use this authentication mechanism by specifying the good options : -o GSSAPIAuthentication=yes

As Ansible relies on ssh for connecting to remote hosts, it is just required to add this line in the Ansible config file ansible.cfg : ssh_args = -o GSSAPIAuthentication=yes -o UserKnownHostsFile=/tmp/$USER/configuration/known_hosts

Another ssh option is provided, because my $USER homedir is not writable (there is no user homedir). So i inform ssh to use a custom known_hosts writeable file.

Ansible managing the ansible bastion.

Most of the time, a bastion is used for running Ansible. This bastion is connected to all sub-network, and only this bastion is allowed to communicate with all of these. This is a SPOF, so often security on the bastion is an hot topic. basically recording everything which goes through it.

Ansible is mostly used to manage remote servers from the bastion, and it is not obvious at first sight, that Ansible can also manage the bastion (run on himself).

But it is fully possible. As far as python is required to run Ansible, it can also being managed by Ansible

So let’s go next point and start to look at the kinit role.

For getting a token from kinit, it is possible to do so : echo '{{ password_kinit }}' | /usr/share/centrifydc/kerberos/bin/kinit

So let’s do this in Ansible:

 ---
- name: kinit | getting a token
  command:  /usr/share/centrifydc/kerberos/bin/kinit
  args: 
    stdin: "{{ password_kinit }}"
  become: no
  when: kinit_action == "init"

- name: Kinit| check token is created
  command: /usr/share/centrifydc/kerberos/bin/klist
  register: result
  failed_when: "'krbtgt' not in result.stdout"
  become: no
  when: kinit_action == "init"

Here we are calling the command module to run the Centrify kinit. We are also asking to use the value of the password_kinit as input of the command (stdin).

We force Ansible to not locally escalate priviledge for this task with become: no.

This task will be run only if the variable kinit_action is defined to init

The second task block is using the command klist from Centrify to see if the token was created, and fails if the token is not present in the result of the executed command.

Once those boths tasks are done, Ansible can connect to remote servers with the help of the ssh options we setup earlier.

But this role is not finished.

Cleaning the mess it better than let the dirt stack.

So once we finished using the token, there is no reason to keep it.

Centrify provides kdestroy for this task.

Here is the code:

- name: kinit | destroying tokens
  command: "/usr/share/centrifydc/kerberos/bin/kdestroy"
  become: no
  when: kinit_action == "destroy"

- name: Kinit| check token is not there anymore
  command: /usr/share/centrifydc/kerberos/bin/klist
  register: result
  failed_when: "'krbtgt' in result.stdout"
  changed_when: result.rc != 1
  become: no
  when: kinit_action == "destroy"

As you can see, the command module is also used to call kdestroy and the second task is checking if no tokens are present in the output of klist. Those tasks are only executed if kinit_action is set to destroy

Calling this kinit role from a playbook

Ansible roles are defining atomic tasks which are called by Ansible playbooks. Also playbooks can be included in other playbook.

So there is two playbook

  • kinit_init.yml
  • kinit_destroy.yml

Which both are using the kinit role, and i insert them at begining and end of any other playbook i wrote to manage the platform.

Let’s see kinit_init.yml:

---
- hosts: localhost
  gather_facts: false
  run_once: True
  any_errors_fatal: true
  tags: always
  vars: 
     kinit_action: init
  tasks:   
  - name: Kinit| get the username running the playbook
    local_action: command whoami
    register: output
    become: no 
  - name: asking for password
    pause: 
      prompt: "enter password for {{ output.stdout }}"
      echo: no
    register: password
  - include_role:
      name: kinit
    vars: 
      password_kinit: "{{ password.user_input }}"

What is important here, is that this playbook always runs on localhost (the famous bastion server).

We don’t need to gather fact about this server, and we want this playbook to run only one time.

Any error are fatal, cause if it fails we cannot connect to remote server anyway. There is a set of two tasks to identify who is running the playbook, and request for his credential.

Once we get the password, we can call the kinit role, with two arguments:

  • the password
  • the action set to init

A little detail here is in tags: always. As i was testing this piece of code on a very big playbook, i was first very unpleased to never see it called. After further investigations, it appeared i was calling the playbook with a tag, and this part of the playbook was outside the scope of the tag. So in the end it was never called. This seems obvious afterward, but in the hot time of coding and testing, it was more frustrating than obvious.

kinit_destroy.yml is pretty the same

---
- hosts: localhost
  gather_facts: false
  connection: local
  run_once: True
  vars:
     kinit_action: destroy
  any_errors_fatal: true
  tags: always
  roles:
   - { role: kinit }

No much need to explain.

So in the end we have two playbooks using the kinit role to obtain an authentication token, and destroy it.

The final point of this article is how to use it in all other playbooks. Pretty easy :

---
- name: authenticating with kinit
  import_playbook: kinit-init.yml
  
...

- name: Removing Token
  import_playbook: kinit-destroy.yml

you import those two playbook at the very begining and the very and of all others playbooks. And we are done.

final note

Asking for a password is not in automation philosophy. Im fighting to have an Ansible-vault file where passwords will be stored encrypted. There are other solutions more complex for managing password for automation, but this is far from the scope of my client.

Happy Hacking !!!