Back to Home
Writing a Custom Ansible Credential Plugin for HashiCorp Vault - Part 1

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:

  1. Defining what AAP Credential Plugins are and their use cases
  2. Writing a custom plugin
  3. 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:
  1. 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
)
  1. 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. example AAP credential plugin creation page

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.

example AAP 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.

example AAP credential using custom AAP credential plugin metadata

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”. finished AAP credential configuration page


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”

AAP (aka AWX or Tower) custom 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.

custom aap credential type example

The credential is added to the job template:

custom aap credential type example 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
And lastly, the job output. Notice the username and password being inserted into the playbook environment from the custom credential plugin & type!

custom aap credential type examplec job template


(Coming soon) - Let’s take a deep dive into how to write a custom AAP credential plugin in Part 2!


Related Posts