Managing Secrets in Red Hat Ansible Automation Playbooks

A Detailed Ansible Secrets Management Tutorial With Conjur Open Source

Ansible is an agentless management tool that can manage provisioning, configuration, and deployment of applications. RedHat acquired Ansible in 2015 and has gained widespread acceptance amongst DevOps engineers since then.  This growth was unquestionably due to Ansible’s straightforward setup and its ability to scale from small development shops to large enterprises.  In 2019, Ansible overtook both Puppet and Chef to become the most widely adopted configuration tool, and it was featured third on the list of most wanted DevOps skills behind GitHub and Docker.

As amazing as Ansible is, however, it’s important to remember that it’s not designed to be a secrets management tool, and while you might be able to find playbooks with workarounds to protect sensitive data like passwords, it’s best to entrust your confidential data to tools designed to manage access through platform independent secrets management.

In this article, we’re going to discuss how you can use Conjur open source secrets management provider to secure secrets within Ansible. We’ll discuss what makes Conjur the perfect partner for Ansible and show you how to get started.

Why Conjur?

Before we go through the process of configuring Ansible and Conjur together, let’s look at what makes Conjur the right choice for secrets management. Like Ansible, Conjur is an OpenSource project, and CyberArk, the organization that maintains Conjur, provides security solutions for more than half the Fortune 500 Companies.

By using Conjur to manage your secrets, you have access to a centralized platform with comprehensive auditing and a one-stop solution that can provide secrets management across your organization.  People and applications alike are managed by role-based access control and the concept of policy-as-code allows you to store the current state of the service in version control.

Getting Started with Conjur and RBAC

We’ll walk through how to get a test instance of Conjur set up in the next section, but first, let’s talk about how Conjur handles permissions. Conjur operates on the principle of least privilege: an individual or an application can only access the secrets that they have been explicitly granted access to in policy.  Access rights can be defined as read-only, write-only, or read and write together.

Human users provide credentials which allow them to access the secrets allowed by the policy. Non-human users are assigned a machine identity. The Conjur authenticator certifies that the client has the necessary credentials and that the native characteristics of the client match those expected from a valid client. Once certified by the authenticator, the client can only access secrets based on its defined role.

Prerequisites

Conjur Quick Start tutorial completed.  This is a great way to get started with Conjur!

Install Dependencies

$ sudo apt update
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
$ sudo apt update
$ sudo apt install docker-ce
$ sudo systemctl status docker
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.23.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version

Install Ansible:

$ sudo apt install software-properties-common

$ sudo apt-add-repository --yes --update ppa:ansible/ansible

$ sudo apt install ansible

How to install other than Ubuntu OS: https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html

Now let’s create a simple ansible playbook:

create a file called playbook.yml and have the contents of this file be:

---
# This playbook prints a simple debug message
- name: Echo
  hosts: 127.0.0.1
  connection: local

  tasks:
  - name: Print debug message
    debug:
      msg: Hello, world!

Lets run this playbook by executing:

$ ansible-playbook playbook.yml

You should see the output of:

ok: [127.0.0.1] => {
  "msg": "Hello, world!"
}

Connecting Ansible and Conjur

Now, all we need to do is switch to the directory that has your docker-compose.yml file and start it all up.

$ docker-compose up -d

Storing a Secret with Policy

We’ll use the Conjur CLI to create a new policy and store our secret value. The CLI comes as a Docker container or as a Ruby Gem. You can find more about both as well as installation instructions in the Conjur Documentation.

The admin password is the API key that Conjur generated when we initialized it.

$ docker-compose conjur authn login admin
Please enter admin's password (it will not be echoed):
Logged in

The next step is to create a policy description. Policy documents can be checked into source control, allowing you to manage policies better and maintain permanent access to the latest version of the policy.

This policy will create a user who can set the password, a host that can read the password, and the password itself. It’s in YAML format and should be self-explanatory.

Database_Password.yml

- !policy
  id: Database
  body:
    # Define users, hosts and secret variable
  - !user DatabaseAdmin
  - !host Ansible
  - !variable DatabasePassword
  - !permit
    # Give read and write permissions to the administrator.
    role: !user DatabaseAdmin
    privileges: [read, update, execute]
    resource: !variable DatabasePassword
  - !permit
    # Give read permissions to the Ansible host account to fetch the password.
    role: !host Ansible
    privileges: [read, execute]
    resource: !variable DatabasePassword

We will use the Conjur CLI to load this policy into our Conjur account:

$ docker cp Database_Password.yml conjur_client:Database_Password.yml
$ docker-compose conjur policy load root Database_Password.yml
Loaded policy 'root'
{
"created_roles": {
"demo:user:DatabaseAdmin@Database": {
"id": "demo:user:DatabaseAdmin@Database",
"api_key": "366zkcj3b3xtm2awzv9c1hegqzg374gacf2qgtnh92ye9xbz207tt16"
},
"demo:host:Database/Ansible": {
"id": "demo:host:Database/Ansible",
"api_key": "3w6xbfx2qj7frm20e94q62nf5h7s4aybge3eh6srp3nykpnh3w19rfv"
}
},
"version": 1
}

Now, we can store the password for our database in the secure store. Conjur should respond with Value added. An added benefit of using Conjur is that if we ever need to change the password, we only need to update it in Conjur.

$ docker-compose conjur variable values add Database/DatabasePassword S3cr3tP4ssw0rd
Value added

At this point, we’ve got the Database Password securely stored in Conjur. We need a couple of additional policies to support the process of interacting with Ansible. The first policy adds a layer. In Conjur, a layer can be used to represent an application and its running environment.

Layer.yml

- !layer
  id: Applications

We’ll also define a Host Factory for that layer. A host factory allows us to create unforgeable tokens which we can assign to new hosts within a layer. This approach is especially useful when you’re managing a fleet of VM’s to support an application, as it saves you from having to define an identity for each one in the policy.

HostFactory.yml

- !host-factory
  id: demoFactory
  annotations:
    description: Factory to create identities for new application servers
  layers: [ !layer Applications ]

We’ll use the Conjur CLI to load both of these files into the root policy in our Conjur Instance:

$ docker cp Layer.yml conjur_client:Layer.yml
$ docker-compose conjur policy load root Layer.yml
Loaded policy 'root'
{
"created_roles": {
},
"version": 2
}
$ docker cp HostFactory.yml conjur_client:HostFactory.yml
$ docker-compose exec client conjur policy load root HostFactory.yml
Loaded policy 'root'
{
"created_roles": {
},
"version": 3
}

The final step before we turn our attention to Ansible is to generate a host factory token. We’ll create a token which lives for 365 days. The token allows machines within the Application layer to identify themselves and gain access to the resources permissible through its role, such as the Database password that we defined above. We’ll use the Conjur CLI to do this and set the token as an environment variable on the Ansible node.

$ docker-compose conjur hostfactory tokens 
create --duration-days 365 demoFactory
[
  {
    "token": "bqwktnxa3sjn1jjwr192p1r0c31s8bs07dzd70g24vp21sn02a4m",
    "expiration": "2020-09-12T05:29:24+00:00",
    "cidr": [

      ]
  }
]

Leveraging Ansible to Configure the Host Machines

The first thing that we want to do is create an environment variable on the Ansible node and set it to equal the token that we generated from the host factory.

$ export FACTORY_TOKEN=bqwktnxa3sjn1jjwr192p1r0c31s8bs07dzd70g24vp21sn02a4m

I defined a collection called servers in the Ansible hosts file (/etc/ansible/hosts) and then used ansible-galaxy to download the Conjur Ansible Role. This role allows Ansible to create an identity on a host machine and installs Summon, which can be used together with the identity to retrieve defined information from Conjur.

$ ansible-galaxy install cyberark.conjur-host-identity

The final thing that we need is the public certificate from the Conjur server. The certificate is downloaded when you initialize the Conjur client. In my case, it was downloaded as conjur_demo.pem

Using the Conjur Ansible Role, we can now build a basic Ansible Playbook to create identities, and then we can install Summon on each host and configure it to access our Conjur instance. The playbook that I created is below. As with previous configurations, you’ll need to replace ***HOST-NAME-HERE*** with the host-name of your Conjur instance.

Server_Conjur_Install.yml

- hosts: localhost
  become: yes
  roles:
    - role: cyberark.conjur-host-identity
      conjur_appliance_url: '***HOST-NAME-HERE***'
      conjur_account: 'demo'
      conjur_host_factory_token: "{{lookup('env', 'FACTORY_TOKEN')}}"
      conjur_host_name: "{{inventory_hostname}}"
      conjur_validate_certs: false
      conjur_ssl_certificate: "~/conjur_demo.pem"

So now, all we need to do is execute the playbook using a command similar to the one shown below:

$ ansible-playbook Server_Conjur_Install.yml

Ansible then loops through all of the hosts in the servers group, creates a Conjur identity for each one, and installs and configures Summon.

Retrieving Secrets From Conjur

We can use a service manager like SystemD along with Summon to retrieve the database password each time that one of the application servers is started and then store it /usr/local/bin/application. Below is an example of a SystemD file which would do just that:

[Unit]
Description=Application
After=network-online.target

[Service]
User=Database/Ansible
ExecStart=/usr/local/bin/summon --yaml 'DATABASE_PASSWORD: !var root/Database/DatabasePassword' /usr/local/bin/application

Learning More

If you would like to learn more about the Ansible Integration for Conjur, the Ansible Documentation is an excellent place to start. You can also visit the GitHub repository for the Conjur Ansible Role. I would highly recommend joining the CyberArk Commons to network with other engineers and ask the experts for helpful tips and best practices.