Managing Users and SSH Keys in a Hybrid World

Managing users is one of the contrived (but applicable) examples we use in Chef Fundamentals training to help onboard new Chefs to the idea of writing data-driven cookbooks. Whenever I run a training class, I typically end that module by talking around other ways to manage users and SSH keys (hopefully you’re not still using passwords). Outside of training classes, this topic comes up quite a bit as one of the first problems folks new to Chef are trying to solve.

User management never ceases to be a popular topic of interest. Managing user authentication is a difficult problem to solve and one that has plagued IT since the inception of computing. So it’s not surprising that, even today, there aren’t a ton of great options out there for doing so.

Active Directory and Kerberos? LDAP and a few patches? These each bring their own set of curiosities to the mix. They can also be especially hairy yak shaves when considering service availability in the distributed world of cloud computing. Many Enterprise admins even find themselves fighting uphill battles against archaic security policies forbidding external access to internal services only to later find themselves architecting a time consuming and brittle distributed directory service that requires special feeding and care.

Often times when I meet users looking to solve this problem, all they really want is a simple way to get consistent users and ssh keys everywhere. Is that so much to ask?

Managing Authentication On-Premise vs. Public Cloud

It’s easy for SSH keys to get out of sync. IaaS providers typically rely on having their own SSH keys to provide administrative access to newly provisioned VM instances. Some providers allow you to import existing ssh key pairs, but the focus of these keys is to provide post-provisioning admin access. Maybe you’re lucky enough to work with a provider that has a solution for multi-user authentication management built into their product. But what happens when you want to use a different provider? The onus of managing authentication in a simple and unified manner still falls on the end-user in a hybrid world.

Let’s look at a step-by-step way of managing users and SSH keys in a consistent and distributed manner for both public cloud and on-premise infrastructure using Chef.

If you’re running on-premise infrastructure, you might be lucky enough to already have this problem solved with an existing directory service. If that describes you and you’re also looking to solve that problem the same way in the cloud, you likely have some additional work to do. Follow this guide and decide the right solution path for yourself. But if you haven’t yet solved that problem or are willing to accept a new solution, read on: this should get you up and running fast.

This is intended as a starter guide that most anyone new to Chef can follow.

User and SSH Key Management with Chef

Chef offers a flexible and easy way to ensure that users can access their systems regardless of location and that administrators can easily grant or remove access as needed. Natively, Chef provides functionality to add and remove users to a system through the user resource. Chef also provides several ways to manage files, so it’s easy to pair these resources together to create a user auth and ssh key management system.

Chef gives you the ability to store data sets (or “Data Bags”) that model infrastructure configuration. Data Bags can be used to provide a central store for data required to manage users and their respective keys.

We will use two existing community cookbooks that leverage these constructs to quickly achieve our goals.

A Step by Step Guide

We are going to detail a step-by-step way to manage users, ssh keys, and grant certain users ‘sudo’ access. This guide presumes basic familiarity with Chef, a working knife config already connected to a Chef server, and existing infrastructure to manage. For more information on achieving these minimal base steps, visit Learn Chef.

In this example, we are going to use the community ‘users’ cookbook along with a ‘users’ data bag we create. Together, they act in tandem as a centralized user directory service. We will also use the community ‘sudo’ cookbook to manage admin rights.

Preparing our ingredients

For the sake of simplicity, we will presume you’re working in a monolithic chef-repo similar to the repo you would create in the Learn Chef tutorials, the Learn Chef Webinar Series, or that you would get from the Starter Kit when you use Hosted Enterprise Chef. In order to quickly leverage Community Cookbooks from Supermarket, we will also add a cookbooks directory (in case it’s not already in your repo), create a blank cookbook README file (in case your repo has no cookbook content), and initialize a git repo so that we may use the knife cookbook site install command. The use of git is optional with Chef, but it speeds up the process of downloading cookbooks a bit for the purposes of this guide.

$ cd ~/chef-repo
$ mkdir cookbooks
$ touch cookbooks/
$ git init
$ git add .
$ git commit -a -m 'initial commit'

Start by downloading the community ‘users’ and ‘sudo’ cookbooks from Supermarket. Then upload both to your Chef server.

$ knife cookbook site install users
$ knife cookbook site install sudo
$ knife cookbook upload -a

Next, create a Data Bag to store data about your users. You can easily create this Data Bag via the Chef Web UI or via the command line. Because we are Awesome Chefs, we’ll use the command line. First, create a directory in our chef-repo to store user data we’ll be managing by hand. Then, issue a command to create and index a new Data Bag named ‘users’ on your Chef Server.

$ mkdir -p data_bags/users
$ knife data_bag create users

Create a Data Bag item for each user you want to manage. In this example, we will manage four distinct users to demonstrate management granularity: han, leia, chewbacca, and vader. They each require their own data bag item. Create these data bag items as flat files in the data_bags/users directory.


  "id"       : "han",
  "comment"  : "Han Solo",
  "home"     : "/opt/carbonite",
  "groups"   : ["rebels", "scoundrels", "sysadmin"],
  "ssh_keys" : [
    " foo",
    "AAA456...uvw== bar"


  "id"       : "leia",
  "groups"   : ["rebels", "your_worship"],
  "action"   : "remove"


  "id"       : "chewbacca",
  "comment"  : "What A Wookie!",
  "home"     : "/home/kashyyyk",
  "groups"   : ["rebels", "sidekicks"],
  "action"   : ["create", "lock"]


  "id"       : "vader",
  "comment"  : "Anakin was a crybaby",
  "groups"   : ["empire", "siths", "sysadmin" ],
  "ssh_keys" : [
    " baz"

We should note what these items are defining:

If ‘han’ belongs to any group we tell our node to manage, we’ll ensure his account is created, belongs to that group, and allows login via multiple ssh keys.

If ‘leia’ belongs to any group we tell our node to manage, we’ll ensure that she does not have a user account created–even if someone has manually created her account without our consent.

If ‘chewbacca’ belongs to any group we tell our node to manage, we’ll ensure his account is created, belongs to that group, and has his account locked.

If ‘vader’ belongs to any group we tell our node to manage, we’ll ensure his account is created, belongs to that group, and allows login via a single ssh key.

$ tree data_bags/
└── users
    ├── chewbacca.json
    ├── han.json
    ├── leia.json
    └── vader.json
1 directory, 4 files

After these data bag items have been created, upload them to your Chef server.

$ knife data_bag from file users data_bags/users/*

Creating a recipe with our ingredients

At this point, we have the building blocks we need to start managing these users across our infrastructure. We will bring those together by creating a my_users cookbook that describes exactly how to manage the users we want in its default recipe.

$ knife cookbook create my_users

We will depend on the two community cookbooks we downloaded earlier for these actions. Edit your new cookbook’s metadata (found in cookbooks/my_users/metadata.rb) and add two ‘depends’ statements so that the metadata.rb file appears like this:

name             'my_users'
maintainer       'YOUR_COMPANY_NAME'
maintainer_email 'YOUR_EMAIL'
license          'All rights reserved'
description      'Installs/Configures my_users'
long_description, ''))
version          '0.1.0'
depends 'users'
depends 'sudo'

Next, edit the my_users::default recipe (found in cookbooks/my_users/recipes/default.rb) and use a custom resource named users_manage, that is provided by the community ‘users’ cookbook, to easily manage your users. Add the resource, as stated in the example below, so that the default recipe file appears like this:

 # Cookbook Name:: my_users
 # Recipe:: default
 # Copyright 2014, YOUR_COMPANY_NAME
 # All rights reserved - Do Not Redistribute

users_manage "rebels" do group_id 1138 action [ :remove, :create ] end

You can read more about the custom users_manage resource from the users cookbook README. This handy resource searches a data bag named ‘users’ (by default, but the name is configurable) for any users that belong to the group it should manage (in this case: ‘rebels’). The custom resource then leverages the existing user, group, directory, and template resources in the core Chef framework to ensure the ‘rebels’ group is created, that the group ‘rebels’ is set to gid ‘1138’, that any users in the ‘rebels’ group with action remove are indeed removed, that any users with action create are indeed created and belong to the ‘rebels’ group, and that any specified ssh keys for those users are present.

Now that you know what the recipe in this cookbook will do, upload the cookbook to your Chef server.

$ knife cookbook upload my_users

Cooking your Infrastructure On-Premise and in the Public Cloud

Let’s look at a simple example of using our new ‘my_users’ default recipe across our infrastructure.


For existing on-premise infrastructure, we can use the knife bootstrap command (with many other configuration options) to take a node that was previously unmanaged by Chef and make it manageable by Chef.

The bootstrap command will login with supplied credentials, automatically install & configure chef, then invoke chef-client to apply a specified run_list. A command to bootstrap existing infrastructure and configure our users could look like this:

$ knife bootstrap -r 'recipe[my_users]' --ssh-user root -P password

Public Cloud

Knife also has several cloud plugins that enable you to simultaneously provision new infrastructure and make it manageable by Chef. Each cloud plugin has its own provider specific settings you must pass, but the real meat of configuration (putting recipe[my_users] in the node’s run_list) remains the same.

For example, if you have the knife-ec2 plugin installed and configured you can apply this same recipe to newly provisioned instance running in EC2 as it boots.

$ knife ec2 server create -r 'recipe[my_users]' --image ami-2a3bc456 --flavor m1.small --groups default,mysecuritygroup --ssh-user ec2-user --ssh-key my_aws_ssh_key --identity-file ~/.ssh/my_aws_ssh_key.pem

Regardless of where you decide to deploy this recipe, as long as your infrastructure can talk to your Chef server you get a centralized user directory service for free. There is no need to architect another distributed solution.

Granular Control

You may have already noticed that not everything we defined has been configured on our target node.

Examine the state of the node you just bootstrapped or provisioned and you’ll notice that not all of our defined groups are present. A peek at /etc/group on our node reveals only this additional configuration:


Isn’t ‘han’ also a scoundrel? And what happened to ‘vader’? If you look at /etc/passwd, you’ll see ‘vader’ is not configured even though he has the action ‘create’. What gives?

The users_manage resource only configures groups you specify should be managed on this node. In our recipe, we specified that we should find any ‘rebels’ and only configure those users on this particular node. Although any of our ‘rebels’ may also be configured to belong to other groups, they will only belong to the group we specified should be managed on this node. Notice that there is no ‘sidekicks’ group for ‘chewbacca’ either.

If we want all of our defined groups and all of their associated users to be configured on this node, we must create a users_manage resource for each group we intend to manage. Edit your recipe so it appears like so:

 # Cookbook Name:: my_users
 # Recipe:: default
 # Copyright 2014, YOUR_COMPANY_NAME
 # All rights reserved - Do Not Redistribute

users_manage "rebels" do group_id 1138 action [ :remove, :create ] end
%w( scoundrels sidekicks siths empire sysadmin ).each do |group| users_manage group do action [ :remove, :create ] end end

Here we use a quick ruby shortcut to avoid repetition of resource declarations and make our recipe more readable. We create a users_manage resource for each of our desired groups in only a few short lines.

Now that you know what the updated recipe does, re-upload your cookbook.

$ knife cookbook upload my_users

Then run chef-client on your target node. A peek at /etc/group now reveals all of our expected groups and their associated users.


Admin Rights

There’s one other common component we typically manage with centralized user directory services: admin rights. Let’s finish this example by utilizing the ‘sudo’ cookbook we downloaded earlier to give ‘han’ and ‘vader’ (and any other member of our ‘sysadmin’ group) root access.

We already have the ingredients we need. We’ll just grow our recipe a bit to use them.

PRO-TIP: If you are running this example on infrastructure you care about, ensure that you are using real ssh keys in the data bag items for ‘han’ and ‘vader’, or that you have the current non-root user you’re logging in as (e.g. ‘fedora’, ‘ec2-user’, ‘ubuntu’, etc) defined in the ‘users’ data bag as part of the ‘sysadmin’ group. Otherwise, you may lock yourself out of having admin rights on your node.

 # Cookbook Name:: my_users
 # Recipe:: default
 # Copyright 2014, YOUR_COMPANY_NAME
 # All rights reserved - Do Not Redistribute

users_manage "rebels" do group_id 1138 action [ :remove, :create ] end
%w( scoundrels sidekicks siths empire sysadmin ).each do |group| users_manage group do action [ :remove, :create ] end end
node.default['authorization']['sudo']['passwordless'] = true include_recipe "sudo"

For a full list of configuration options, see the sudo cookbook README. Since we’ve forsaken passwords in favor of ssh keys, we set a node attribute that tells the sudo cookbook to allow passwordless sudo. We then use an include_recipe statement to ensure the default recipe in the ‘sudo’ cookbook gets processed after we have configured our users. The default recipe in the ‘sudo’ cookbook ensures anyone in the ‘sysadmin’ group is able to sudo as root.

Now that you know what the updated recipe does, re-upload the my_users cookbook, and run chef-client on your target node. SSH in as ‘han’ or ‘vader’ and verify they can ‘sudo’ all the things without a password.


While also a contrived example, this guide clearly illustrates how a few simple primitives in Chef can construct a powerful and granular user management system in about a dozen lines of code. You can decide which of your nodes need access by ‘dba’, ‘appdev’, or ‘qa’ groups and manage adding, removing, or modifying users/keys within those groups with ease by simply altering a bit of data.

There are other ways to accomplish these same goals. There are other ecosystem tools you could choose to use to suit the needs of your particular application or workflow. This is not the only way to manage your SSH users and keys with Chef. If there is functionality you need that is not addressed by this example, the beauty of Chef is that is very easy to change and make it your own or submit a Pull Request and collaborate with the community on these existing cookbooks.

Hopefully this step-by-step guide provides you a basis for managing users and SSH keys in a consistent and distributed manner on-premise, off-premise, or in a hybrid model using the existing Chef infrastructure and tools you already have in place.

George Miranda

Former Chef Employee