lock and key illustration

Loading Your Database Credentials at Runtime with Conjur

Back when I first became a programmer, it was a common practice to include database credentials right in the code where we established the database connection. I may have been wearing neon-colored socks around that time as well.

Fortunately, along with improvements in my fashion sense, the process of managing database credentials has improved a great deal since then. There are now much safer and better ways of handling database credentials.

To show what modern database credentials management should look like, let’s walk through the process of using Conjur to load your database credentials at runtime. This approach not only provides better security for your database, but also empowers you with the tools for improved management of your data and database credentials.

Secrets Management and Why It’s Important

Secrets management refers to the processes and systems that we use to manage our access credentials. Modern secrets management requires some or all of the following abilities:

  • Centralized storage and encryption of all passwords, encryption keys, access keys, and tokens
  • Access to credentials validated with each request
  • Audit reports for credential usage
  • Periodic rotation of credentials and encryption keys
  • Scalable and easy to integrate

Conjur provides all of this functionality and more. We’ll start by looking at role-based access control (or RBAC). Then we’ll set up an instance of Conjur on a local workstation, create a role and add our database credentials as a secret, and then we’ll use RBAC to load database credentials into our application at runtime.

RBAC in Conjur

Role-based access control has two responsibilities within Conjur. The first is that it provides a place to create and maintain authorization rules. The second is that it can use those rules to answer questions about authorization. RBAC refers to these questions as an RBAC transaction. A transaction consists of a role, a privilege, and a resource.

role, privilege, resource illustration

Role: a role is an entity defined within Conjur. This entity could be a user, a group, a system, or a host.

Privilege: a privilege is an authorization to act. This action could be to create a resource, read a resource, update a resource or delete a resource.

Resource: a resource can have actions performed on it. A resource could be a user or any of the other roles mentioned above. A resource could also be a secret, a document or other resource stored in Conjur.

Using the example of database credentials, the question or transaction that RBAC must process is: Does an application have the privilege of reading the credentials for the database? Let’s install Conjur and create a policy to address this transaction.

Getting Started with Conjur

You can download and run Conjur Open Source as a Docker image. The same image could also be deployed on your preferred cloud container provider, or you can use a provided CloudFormation template to set up Conjur within your AWS Account.  Complete installation instructions are provided in the Conjur Documentation. The documentation recommends the Docker CE Engine, but if that isn’t an option, you can also use Docker Toolbox, which is an older version.

Once your Docker environment is set up, you have a couple of options available to set up a local dev environment. The following step worked well for me, but you can also clone the Conjur Git repository, and follow the instructions provided in the README.md file.

I’ll be setting up a Conjur installation with the account name of demo. Follow the commands below to get an instance of Conjur running on your local workstation.

$ curl -o docker-compose.yml https://developer.cyberark.com/get-started/docker-compose.quickstart.yml

Next, execute docker-compose with the pull command.

$ docker-compose pull

The next step is to generate a data key and load it in as a system variable.

$ docker-compose run --no-deps --rm conjur data-key generate > data_key
$ export CONJUR_DATA_KEY="$(< data_key)"

With the data key created, we can start up our Conjur server, client and database. Each entity exists as an independent container.

  • Cyberark_database: A Postgres database as a data store for Conjur
  • Cyberark_conjur: The Conjur server
  • Cyberark_client: A CLI client which allows us to interact with the Conjur server.
$ docker-compose up -d

We’ll create our new account called demo. The command returns a public key and an API key for the admin user (you’ll want to back these up in a secure location).

$ docker-compose exec conjur conjurctl account create demo
Token-Signing Public Key: -----BEGIN PUBLIC KEY-----
Nsa28BbezoaYVnADAcquonBGNsa28BbezoaYVnADAcquonBGNsa28BbenADAcquonBGNsa28BbezoaYVnADAcquonBGNsa28BbezoaYVnADAcquonBGNsa28BbezoaYVcquonBGNsa28BbezoaYVnADAcquonBGNsa28BbezoaYVnADAcquonBGNsa28BbezoaYVnADABGNsa28BbezoaYVnADAcquonBGNsa28BbezoaYVnADAcquonBGNsa28BbezoaYVnADAcquon28BbezoaYVnADAcquonBGNsa28BbezoaYVnADoaYVnADAcquonBG
-----END PUBLIC KEY-----
API key for admin: 3at4pqsd7319ye262mggbe007ezfnxhs3dd3ezj1de0t9q

Let’s log in to the Conjur client.

$ docker-compose exec client bash
root@8f2250eace10:/#

Next, we’ll connect to the account we created, and log in as the admin user. The password is the API key we received when we created the demo account.

root@8f2250eace10:/# conjur init -u conjur -a demo
Wrote configuration to /root/.conjurrc
root@8f2250eace10:/# conjur authn login -u admin
Please enter the admin’s password (it will not be echoed):
Logged in
root@8f2250eace10:/#

Now that we’re logged in, let’s use the whoami command to validate that we’re logged in.

root@8f2250eace10:/# conjur authn whoami
{"account":"demo","username":"admin"}

Storing and Updating Secrets in Conjur

The first thing we’re going to do is add a policy to the account. Conjur uses the term variable to reference data elements in the database. We’re going to create two variables to store the username and password for the database. We’ll also create two groups to control access to the variables. We grant access privileges as follows:

  • Database-admins receive create, update and read privileges on the variables.
  • Database-users only receive read access to the variables.

You’ll notice that we don’t define the username and password in the policy file. We’ll define these values in the next step.

---
- !policy
 id: root
 body:
   #Create DBA group, and DBA admin and DBA users
 - !user dsmith
 - !user ejones
 - !group database-admins
 - !group database-users
 - !host demo-app
   #variables
 - !variable db/username
 - !variable db/password
   #Add users and set permissions to the DBA group
 - !grant
   role: !group database-admins
   member: !user dsmith
   member: !user ejones
 - !permit
   role: !group database-admins
   privileges:
     - create
     - update
     - read
   resources:
     - !variable db/username
     - !variable db/password
   #Add users and set permissions to the DB Users group
 - !grant
   role: !group database-users
   member: !host demo-app
 - !permit
   role: !group database-users
   privileges:
     - read
   resources:
     - !variable db/username
     - !variable db/password

I created this file in the home directory on the Docker container and then loaded it into Conjur with the following command.

root@8f2250eace10:/# conjur policy load root ~/root.yml
{
  "created_roles": {
    "demo:user:dsmith@root": {
      "id": "demo:user:dsmith@root",
      "api_key": "2d3mym32kmcehn2gmvh41eqdzx420tc7be3rb17em1sc4xgs3pra17r"
    },
    "demo:user:ejones@root": {
      "id": "demo:user:ejones@root",
      "api_key": "13v8gwn5xae9c1m13wvg2xny4514p7d1ya1p4a2ab3cxn3gzvb9g"
    },
    "demo:host:root/demo-app": {
      "id": "demo:host:root/demo-app",
      "api_key": "smzqbc31zk7gh2svfv8h3cvzy9a2059c399366jgk651343de79z6"
    }
  },
  "version": 1
}

Store the api_key for each of the users and the demo-app in a safe place until they’re needed.

Adding the Secrets to Conjur

We’re going to use the Conjur API to add the secrets to the database. Before we can do that, we need to have an access token. The easiest way to generate this token is by running the following command from the CLI.

root@8f2250eace10:/# conjur authn authenticate -H
Authorization: Token token="eyJwcm90ZWN0ZWQiOiJleUpoYkdjaU9pSmpiMjVxZFhJdWIzSm5MM05zYjNOcGJHOHZkaklpTENKcmFXUWlPaUkzTjJVeE5EazVOekEyWm1JMFl6ZzNOekF5WVdaak56Vm1ORGxoWXpnek9TSjkiLCJwYXlsb2FkIjoiZXlKemRXSWlPaUpoWkcxcGJpSXNJbWxoZENJNk1UVTFNek01TWpNeE9IMD0iLCJzaWduYXR1cmUiOiJCTl9LSUU1c1B6OHd2eENSZl9WOWhYWWo5U1IwOFlMWHBGc3ZrRWN5SzlwS0hUZjhDUU5SYU8xLTh0SXJxd3VnQkl1R0VESG1ONjc3VklqYTZWVlc0c0FDSktiVkNodUlnTDk2RkVBQmowcEY0U01Nd3JNRUlXWXNDYUxEMVhxRXQtNXhSc05uZkJVelVNa2ZFcno2bVoxRFBlQk14RzZaYnk2RzlJQTJ4TzduT0k0c0xoeXZ6eXAwSktRbFdRVkU4dU1KR2pvd1ZLUXZXVHE2YlUxdjVqLXo1LS1fUFRXYVRkYWcwZ2NodmN2MmhLbVd1bTJ1NnV5WjFpQ2prXzE5NlFDVmNrbVllZUhnSjB0UjA5cUdaRjJLaHNralZXOS1Ub3ZFRnUxVmRFMExNWlNaZ1Fnb0gyN3Q1UEpKc1pkd1psVFZDNXdDR2hOUlRnU2VMVk1RWGwxbWkweGY3TktsRHJEOUY0UV9XczlaMGNnWkF5enA2VWJnSjRReDJ1bWYifQ=="

Using that access token, we can execute a POST request against the Conjur API. I’m executing these from the CLI container, but you can execute from any device with access to the Conjur instance. I’ll be abbreviating the access token to <<<ACCESS_TOKEN>> to enhance readability. I’ll be setting the following variables:

  • root/db/password to supersecretpassword
  • root/db/username to dbusername
root@8f2250eace10:/# curl --request POST
--url http://cyberark_conjur_1/secrets/demo/variable/root%2Fdb%2Fpassword   --header 'authorization: Token token="<<<ACCESS_TOKEN>>>"' --data "supersecretpassword"
root@8f2250eace10:/# curl --request POST
--url http://cyberark_conjur_1/secrets/demo/variable/root%2Fdb%2Fusername   --header 'authorization: Token token="<<<ACCESS_TOKEN>>>"' --data "dbusername"

We can validate these operations by executing a series of GET requests against the same API.

root@8f2250eace10:/# curl http://cyberark_conjur_1/secrets/demo/variable/root%2Fdb%2Fpassword  --header  'authorization: Token token="<<<ACCESS_TOKEN>>>"'
supersecretpassword
root@8f2250eace10:/# curl http://cyberark_conjur_1/secrets/demo/variable/root%2Fdb%2Fusername  --header  'authorization: Token token="<<<ACCESS_TOKEN>>>"'
dbusername

Now we can use Conjur to load our DB credentials at runtime from a trusted machine.

Accessing Secrets from Your Application

Conjur provides APIs for the following languages:

We’ll be using the Java API with a SpringBoot application, and using Conjur to load the credentials for a MySQL database connection at runtime. You can use the Java API by cloning the GitHub repository, building the library, and including it as a dependency in your application.

You can add the configuration for the connection to Conjur with environment variables on the host machine. The API Key is the api-key value which Conjur returned after we ran the root policy update.

CONJUR_ACCOUNT=demo
CONJUR_AUTHN_LOGIN=host/demo-app
CONJUR_AUTHN_API_KEY=smzqbc31zk7gh2svfv8h3cvzy9a2059c399366jgk651343de79z6
CONJUR_APPLIANCE_URL=http://cyberark_conjur_1/api

These environment variables can be added to the host machine as part of the build and deploy process. We can reference them from our application, and use them to establish a connection to the Conjur database to retrieve our database credentials.

@Value("${CONJUR_AUTHN_LOGIN}")
private String conjurHostId;
@Value("${CONJUR_AUTHN_API_KEY}")
private String conjurAPIKey;
@Value("${spring.datasource.url}")
private String datasourceUrl;
@Value("${spring.datasource.driver-class-name}")
private String datasourceDriverClass;

@Bean
public DataSource dataSource() {
    Conjur conjur = new Conjur(conjurHostId, conjurAPIKey);
    String datasourceUsername =   
                conjur.variables().retrieveSecret("db/username");
    String datasourcePassword =
                conjur.variables().retrieveSecret("db/password");

    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setUsername(datasourceUsername);
    dataSource.setPassword(datasourcePassword);
    dataSource.setUrl(datasourceUrl);
    dataSource.setDriverClassName(datasourceDriverClass);

    return dataSource;
}

Conclusion

Conjur from CyberArk provides a scalable and straightforward solution to the challenge of secrets management. Storing your secrets in Conjur allows you to limit access to authorized users and hosts, and makes it easier to update, rotate and manage credentials for your datastores and other devices with sensitive information.