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 !!!