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.
Mike Mackrory is a Global citizen who has settled down in the Pacific Northwest — for now. By day he works as a Lead Engineer on a DevOps team, and by night, he writes and tinkers with other technology projects. When he’s not tapping on the keys, he can be found hiking, fishing and exploring both the urban and rural landscape with his kids. Always happy to help out another developer, he has a definite preference for helping those who bring gifts of gourmet donuts, craft beer and/or single-malt Scotch.