At LifeChurch.tv, we are building a new platform for managing our Linux servers, and key to our strategy is the use of puppet. The nice thing about puppet is that you can define certain policies for your servers, and puppet will go through the process of bringing your member servers (nodes) into compliance.
This means you can setup your infrastructure by describing it. Describe what you want in a configuration file, and puppet ensures that your nodes match your description continuously. It even works across distributions and OSes, though this is not as relevant for our environment because we will be running entirely on Debian.
Puppet is not the only tool that does this. Other similar tools include cfengine, Chef, and Capistrano.
Puppet Setup
There are some really nice guides on how to setup puppet. One excellent quick-start guide is on the Debian Administration site. Follow that and you will have a working puppet network. The only thing missing are the actual rules you want to enforce, and so let’s talk about that!
Why Use Puppet to Manage User Accounts? (and not NIS, LDAP, etc)
One of the benefits to managing user accounts in puppet is the fact that it is decentralized. Each user account is just a normal user account on the managed server. There is nothing special about the user accounts puppet creates other than the fact they were created by puppet and not by a human administrator.
The nice thing about this is that if the main host dies, we do not lose authentication. Which means that our puppetmaster server (or NIS/LDAP server) need not have any special uptime requirements. If an emergency happens, we can focus on getting our production servers up, and focus on getting the puppetmaster up on an “as needed” basis.
The downside to this is that puppet is not necessarily really designed to manage “normal” login user accounts (as opposed to system accounts). The biggest way this comes up is that, although you can set the password in puppet, puppet continually monitors system settings (good) and if it notices that the password has changed, will reset it. (bad)
I do not want to monitor user passwords on our network, so there needs to be a way to set a password and have puppet stop monitoring this password. Fortunately, once you figure out the trick, this is actually really quite easy. But first, let’s get some definitions out of the way.
User Setup Definition
So I created a file in /etc/puppet/manifests/classes/user.pp which will hold all the user definition rules. Here’s what our add_user definition says:
define add_user ( $email, $uid ) { $username = $title user { $username: comment => "$email", home => "/home/$username", shell => "/bin/bash", uid => $uid } group { $username: gid => $uid, require => user[$username] } file { "/home/$username/": ensure => directory, owner => $username, group => $username, mode => 750, require => [ user[$username], group[$username] ] } file { "/home/$username/.ssh": ensure => directory, owner => $username, group => $username, mode => 700, require => file["/home/$username/"] } exec { "/narnia/tools/setuserpassword.sh $username": path => "/bin:/usr/bin", refreshonly => true, subscribe => user[$username], unless => "cat /etc/shadow | grep $username| cut -f 2 -d : | grep -v '!'" } # now make sure that the ssh key authorized files is around file { "/home/$username/.ssh/authorized_keys": ensure => present, owner => $username, group => $username, mode => 600, require => file["/home/$username/"] } }
The define is like defining a function. And each statement in the definition is a resource that puppet can manage. (list of resources and their options) The first user statement creates the actual user with all the passed variables. I like to specify the UID so they can be consistent across nodes. Next statement creates a group of the same name and id as the user. We want the group to be created after the user, so we have a require statement to setup this dependency.
Next we setup the home folder and ~user/.ssh folder so that we can manage ssh keys as well. Then we have the exec statement which executes a script I created which gives the user a random password and emails them to let them know what it is. Pretty simple. It is in Narnia, which is our custom toolkit that is synced to each server. (setup using another puppet rule… for another blog post)
Now, normally puppet will run this script at every refresh which means that users would have their passwords reset every 30 minutes. Not good. So there are three key lines:
refreshonly: this will only execute this rule on “refresh”–whenever the subscribed event changes. In our case, this should only happen when the user is created.
subscribe: the event we’re interested in. In our case, the creation of the user.
unless: I am not sure if this is completely necessary in our case, but this prevents the rule from executing if a certain condition returns true. So I wrote a quick one liner that opens the shadow file, finds the appropriate user, and returns the password hash if it does not contain an exclamation mark. This is key because when puppet creates the user, it creates it with a blank password which shows up as a ! in the shadow file. This one liner will not return that, allowing the reset script to run and to set a password. But if a password has already been set (there is no !), this returns true and prevents puppet from resetting the password. This may not be necessary, but acts as a failsafe in case I do not properly understand how refreshonly and subscribe work.
The nice thing about resetting a password with a custom script is that even if things go awry and this script is executed at the wrong times, we can add error checking in the script itself.
So that is how users are setup and their passwords managed. How about ssh keys?
SSH Key Management
If you don’t know about them… SSH keys are awesome. It uses asymmetric public-key encryption, which I do not understand the mathematics of, but basically you get two big numbers that are somehow mathematically related. They are related in such a way that you can generate the public key if you know the private key, but it’s impossible to get the private key if you only know the public key.
Crazy, right? I don’t know how that works either. But the cool thing is that it enables password-less logins. If I setup my SSH keys, I can login to servers all over the world without entering a password. And it’s still more secure than using passwords.
Anyway. I want to be able to take advantage of these properties, and I want to be able to use puppet to manage it. So in the same users.pp file as above, I have some definitions related to ssh keys:
define add_ssh_key( $key, $type ) { $username = $title ssh_authorized_key{ "${username}_${key}": ensure => present, key => $key, type => $type, user => $username, require => file["/home/$username/.ssh/authorized_keys"] } }
ssh_authorized_keys is a built-in type in puppet, which just means that puppet has a bunch of code to handle everything we need, we just have to tell it what to do.
So we create an authorized key, and we name it: [Username of user we’re logging in as]_[Full Text of Key]. In many types, the name is important and used somewhere, but not in ssh_authorized_keys. In this case, puppet just wants a unique name, and this is an easy way to give it a unique name. (since it doesn’t make sense to have puppet manage the same key multiple times for one user)
The key things here (ha!) are that we need to give it the actual key, the key type (ssh_rsa or ssh_dsa?), and the user we want to make this an authorized key for. In our case, I want to make sure that this runs only after the authorized_keys file is created. This is probably not strictly necessary.
I have an identical function for root keys, it just checks to ensure that there is a rule for creating the /root/authorized_keys file. We can’t use the definition above, because the root home folder is /root, not /home/root/.
Putting It All Together!
Nothing we’ve done so far will do anything on any of the nodes. They’re just Lego building blocks. Now let’s put them together and build something cool. Like a giant Statue of Liberty. Or not.
The cornerstone for any puppet configuration rests in the /etc/puppet/manifests/site.pp file. So first of all, we want to make sure our /etc/puppet/manifests/classes/*.pp classes get included:
import "classes/*.pp"Then we need to specify node rules. In our case, I just want a default rule that applies to all nodes. (node default)
node default {
add_user { rrunner:
email => "road.runner@acme.com",
uid => 5001
}We call the add_user function we defined waaaaay up there like 200 paragraphs ago. In this case, the name is important. It becomes the username. Then we add their email address (which goes in the gecos/comment field in the /etc/passwd file). This isn’t strictly necessary, but we do need a way to get the user’s email so we can send them their randomly generated password, so we just need to include it.
Next, we want to add an ssh key for road runner:
add_ssh_key { rrunner:
key => "AAAAB3NzaC1yc2EAAAABIwAAAQEAwtnN3Nmkn8WKfBUs4/AwCmthfsY6TXmEe63d2Okeo3QtpUvceXj83bVqerK6h62bEGb7LtE2oW8utiE8ZlWmeViMdIZo6OQVOMj69HgPZu3IKSIYW5hTVWhb5FmQOOtGk5m1uxJyeBI5ivmVJtQIrH6gOkoOP1X23PqLCUnb1Wur9J6NCAOOLtJQEl+CMTSRqNZ6VU/4Kvu0FxSiAqHdi5i4zpob3HIWXSC0Ugh664jqvjjJI7ZLuC4Ym3BFK+uZKVX3yKIx0sbiZm+IMBvuUJzmpfPTMPrMyZuq7FxSUjIv+TX4XKwxv8ysU39k1WllOYT5kDwkOnJ5NLt4zqJQVQ==",
type => "ssh-rsa"
}
}And… that’s it. Save and at the next puppet refresh, puppet will generate user accounts for road runner on each node, include road runner’s public key, and email road runner that his account is ready.
I hope this has been helpful. If you have any questions on our setup, please leave a comment. I setup a github project with the example configuration I used in this post. It also includes more explanatory comments.
Meep meep.
Helpful Resources

I'm not sure if it's correct, but the ssh-key is being re-added on every other catalog run. Have you experienced this as well?
I'm using puppet version 0.24.5 (on Debian 5.0).
For people who want to get up and running with Puppet from scratch, with no prior experience, I've written a simple tutorial:
http://bitfieldconsulting.com/puppet-tutorial
Any feedback would be appreciated!
Thanks!
-alex
Yes, you can set the password in puppet. But as I explained in my post, that would mean that 1) your password is in the configuration file, ready to be read by anyone who happened to have access to it, 2) puppet would ensure that the password was always set to this password, so users can't change their own password, and 3) the password would be the same in each case.
A separate script helps me balance the need that user's passwords be random and secure with the need for people to be able to change their own passwords.
The password field is great for system accounts, but not accounts used by regular people. At least not for my purposes.
As for the group question, I honestly couldn't tell you, I really don't use groups to much effect.
Thanks for your comment.
AJ
err: Could not apply complete catalog: Could not back up /home/testuser/.ssh/authorized_keys: Permission denied - /var/lib/puppet/clientbucket/d
Any ideas? I'm running puppetd on the client with sudo, so it should have permission to do whatever it wants...
exec { "set editor vi":
path => "/bin:/usr/bin",
command => "su - -c 'echo \"export EDITOR=vi\" >> /etc/profile'",
unless => "su - -c 'env | grep EDITOR=vi'"
}
/usr/sbin/puppetd --test --server myserver.mydomain.com
Everything is working perfectly, except that the authorized_keys file is always 0 bytes. The command above outputs this error:
err: Could not apply complete catalog: Could not back up /home/testuser/.ssh/authorized_keys: Permission denied - /var/lib/puppet/clientbucket/d
Oh, I should note for future reference that I had to delete /etc/puppet/ssl in order to register a new certificate with the server.
Thank god it's working! That was a really annoying error.
You must be careful to remove the comment section (just after the ==) of your key as add_ssh_key already generates a comment for your key. Due to puppet's use of the comment field to detect duplicates ( http://projects.puppetlabs.com/issues/1531 ) your extra comment will cause puppet to parse the line incorrectly and so the duplication won't be detected and another key will be added.
I might have this slightly wrong as I've only looked at this problem from the surface, however if you remove the comment your problem will go away.
Folks the setuserpasswords,sh is a script HE created and lives in the puppet file server.
On a separate note, I would not suggest to anyone to use ssh-keys without a pass-phrase... (what some people call password less)
You should always use a pass-phrase to secure your ssh-keys. If you don't want to put your password every-time you ssh to a host, then use a ssh-agent to load your keys and pass-phrase once then forget about it while your current session is still active.
I have a issue.. I install all (server and client) all work without problems... but I want to change the password of my users from puppet server.
My problem is... I trigger the signal but if the user doesn't exists y create with the password. How can make that if only the user is created he will change the password?? I tried with ensure and require. That he ask the user exists?? if exists change the password if not.. cancel. Thank's men. Sorry about my english..
greetings from cape town, south africa
riaan
"cat /etc/shadow | grep $username" wins a useless use award, try grep $username /etc/shadow
also, what happens, if $username where containing "." ?
Greetings
Still wondering though, how your user gets to read the mail with the random password ;)
Good point. Obviously this wouldn't work if you're also setting up email accounts on the same system. For us, email is remote. Could be Gmail, Corporate Email or something else.
http://pastebin.com/2TxNJjRd
The pp file look like this:
add_ssh_key { bajs:
key =>
"AAAA....[parsed]++098a6SQWdhNcpOqnaamUXt5qQ6omazOcz+8WjrQ==",
type => "ssh-rsa"
}
Yes, I've added this:
define disable_user () {
$username = $title
exec { "/usr/bin/passwd -l $username":
path => '/bin:/usr/bin',
unless => "grep $username /etc/shadow | cut -f 2 -d : | grep ^!"
}
exec { "/usr/bin/chsh -s /sbin/nologin $username":
path => '/bin:/usr/bin',
unless => "grep $username /etc/passwd | cut -f 7 -d : | grep nologin"
}
}
Then when I want to disable a user, I comment out or remove all the add_user and add_key stuff, and leave just something like this:
define john {
disable_user { "john": }
}
Now, my commands above are simply disabling the account and setting the shell to "/sbin/nologin", but you could just have it remove the account and home directory instead.
Chris
1000 thanks in advance,
/Kalle Larsson
Leave a comment...