
Writing a Custom Ansible Credential Plugin for HashiCorp Vault - Part 1
Info
This series was based on a plugin developed for Ansible Tower. However, it is still applicable to both AWX and AAP. I’ll come back later and make updates for AAP specifically.
Overview
Ansible Automation Platform (previously Ansible Tower) is an amazing tool for automating repeatable tasks against a large set of hosts. One feature is the ability to create Credentials in AAP that a job template can use. There are quite a few predefined credential types installed with AAP. For most cases, using one of the credentials defined here or even including an Ansible vault file within your playbook, is enough.
However, I ran across a scenario where neither of these options were sufficient. If you’re reading this article, you’ve probably found yourself in the same situation I was a few weeks ago and have realized that there is next to no documentaiton or examples out there on how to create a custom credential plugin. In this post we’ll take a deep dive into the process I took developing my own plugin in the hopes it provides at least a starting point for you as well!
In this multi-part series we will walk through:
- Defining what AAP Credential Plugins are and their use cases
- Writing a custom plugin
- Analyzing my specific use case for a credential plugin (HashiCorp Vault + AAP)
Tip
Custom Credential plugins in AAP are great when you want to perform a set of actions to generate or retreieve the credential that’s passed to a playbook. As with all plugins within AAP, they are written in Python which means that what you can accomplish within a plugin is quite extensive and flexible.
Custom Plugin Deep Dive
To analyze how AAP Credential Plugins work under the hood, let’s take a look at the AWX (AAP’s upstream project) example plugin.
This can be found here.
Credential plugins have two main sections:
- The plugin object definition. This is the ingress point into the plugin.
example_plugin = CredentialPlugin(
'Example AWX Credential Plugin',
# These are the inputs into the plugin. The "fields" are the input fields
# presented to the user when creating the credential in AAP.
inputs={
'fields': [{
'id': 'url',
'label': 'Server URL',
'type': 'string',
}, {
'id': 'token',
'label': 'Authentication Token',
'type': 'string',
'secret': True,
}],
# Metadata are fields the user sees when creating a custom Credential Type in AAP.
'metadata': [{
'id': 'identifier',
'label': 'Identifier',
'type': 'string',
'help_text': 'The name of the key in My Credential System to fetch.'
}],
# Required fields
'required': ['url', 'token', 'secret_key'],
},
backend = some_lookup_function # <--- Note that the name of the backend here is the function that returns the credential string
)
- Any functions needed to return back the credential. You can have as many functions you want defined in here, but the goal is to have a single string returned.
def some_lookup_function(**kwargs):
url = kwargs.get('url')
token = kwargs.get('token')
identifier = kwargs.get('identifier')
...
if identifier in value:
return value[identifier] # The value returned is the credential that will be exposed to the tower job.
raise ValueError(f'Could not find a value for {identifier}.')
Let’s take a closer look at the inputs section…
This is where things are a little hard to understand at first. Using a Credential Plugin is two parts:
- Defining the required fields by the plugin to connect to the backend credential store
- Using the credential plugin to insert the returned value into a field inside another credential
Using the Example AWX Credential Plugin as an example, here is the screen when you create the credential.
Here you can see the “Server URL” field would be the url to the backend system to retrieve the credential from.
”Authentication Token” would be the way the Credential Plugin logs into the backend credential system to retrieve whatever credential the playbook requires.
Both fields are defined in the plugin within the “fields” input section.
But at this point, the plugin doesn’t have any information about who or what credential it is retrieving from the backend.. To show that, I’ll create a basic Machine Credential.
To get to this popup window I clicked on the magnifying glass icon in the USERNAME field and selected the Example AWX Credential we just created above. This is how we tell this credential to populate the USERNAME field with the value returned from our Credential Plugin.
Ah! See the Identifier field here? That’s what is defined in the “metadata” section of the Plugin inputs and is how we’re telling the plugin what we want to retrieve from the backend credential system.
Ok I see where the fields are coming from… but how does the plugin know what to return back?
Let’s look at the lookup function closer now that we know how the inputs into the credential work.
Here are the two fields we set when we added the Credential Plugin plugin in AAP.
def some_lookup_function(**kwargs):
url = kwargs.get('url')
token = kwargs.get('token')
...
It’s getting the identifier from the Metadata fields..
identifier = kwargs.get('identifier')
Validating that the token to the backend system is valid..
if token != 'VALID':
raise ValueError('Invalid token!')
Since this is an example plugin, there is just a definition of a “value” dict that the plugin is evaluating against to represent some data in a backend system.
value = {
'username': 'mary',
'email': 'mary@example.org',
'password': 'super-secret'
}
And then it is checking to see if the identifier (in our example it was “username”) is present in the value dict.
if identifier in value:
return value[identifier]
Here is a Machine credential. The “USERNAME” field is pointing to the Example AWX Credential with the identifier set to “username”. The “PASSWORD” field is pointing to the Example AWX Credential with the identifier set to “password”.
AAP would login to the machine with the username “mary” and the password “super-secret”.
Custom Credential Types
We’ve talked about using a credential plugin with a pre-defined Credential type in AAP (i.e. Machine). However, it’s very possible that you want a custom credential plugin that exposes some key or secret to the playbooks via a field name you want control over. That’s where custom credential types come into play.
I’m not going to go into too much detail here since the AAP docs are quite comprehensive, but let’s show an example using the AWX Example plugin.
Creating a new Credential Type called “Example Credential Type”
Input Configuration:
fields:
- id: username
type: string
label: Username
- id: password
type: string
label: Password
secret: true
required:
- username
- password
Injector Configuration:
env:
USERNAME: "{{ username }}"
PASSWORD: "{{ password }}"
Now we can create a new Credential in AAP, set the Type to be Example Credential Type, and we can retrieve the fields using the Credential Plugin. If we add this to a Job Template, the playbooks would be able to access the USERNAME & PASSWORD environment variables which would bet set to “mary” and “super-secret” respectivly.
Putting it all together…
To show using the new credential type let’s create a credential using this new type and run an extremely basic play that will just print out the credentials (obviously not a good idea to do this with real creds!).
Username field is set to the “username” identifier”. Password field is set to the “password” identifier.
The credential is added to the job template:
Here’s the playbook:
- name: Example credential plugin use
hosts: localhost
gather_facts: yes
tasks:
- name: Print credentials
debug:
msg:
- "Username = {{ lookup('env', 'USERNAME') }}"
- "Password = {{ lookup('env', 'PASSWORD') }}"
verbosity: 1