« Back to blog
 

Using Puppet to Manage Users, Passwords and SSH Keys

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

Posted
28 Comments
Jun 24, 2010
Dan Ackerson said...
Worked for me! Thanks for the easy guide.

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).

Aug 09, 2010
John Arundel said...
Great tutorial! And I love the way you've presented the screen snippets.

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!

Oct 07, 2010
Alex said...
Where can you find the setuserpassword.sh script???
Thanks!
-alex
Nov 05, 2010
msallee said...
Great article. A few corrections and questions: caps needed on all require => File and subscribe => User lines. Q: How would you add groups to this? Found that adding password => '$abc1' works so you don't need the setpasswd script.
Nov 05, 2010
AJ Bourg said...
Hi msallee,

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

Nov 05, 2010
msallee said...
Could you post part of your password script? I think this behavior will work for keeping passwords same on cluster nodes. Thanks!
Feb 10, 2011
chris said...
I'm having trouble with the add_ssh_key part. I followed the example above precisely, but I'm running into permissions issues. The /home/user/.ssh/authorized_keys file is created perfectly, except that it's always a 0 length file (empty) and puppetd on the client spits out this error:

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...

Feb 10, 2011
Dan Ackerson said...
Chris, I also had problems with sudo and I figured out "su - -c '<cmd>'" worked:

exec { "set editor vi":
path => "/bin:/usr/bin",
command => "su - -c 'echo \"export EDITOR=vi\" >> /etc/profile'",
unless => "su - -c 'env | grep EDITOR=vi'"
}

Feb 10, 2011
chris said...
I'm having trouble with the add_ssh_key part, and creating the key is a built-in puppet function, so I don't know where to add the "su - -c" unfortunately. FWIW, I also tried running puppet as the root user on the client, but it didn't help. Basically, I've got the exact configuration detailed above on my puppetmaster server, and then on the client I'm running this:

/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

Feb 10, 2011
chris said...
OK wow, I figured this out. It was a bug. I was using the version available in the EPEL repository since that was most convenient. I realized it was significantly older than the current one on the Puppet website. So I removed it, downloaded the latest tarball, and ran puppetd --test again. It immediately worked. Grr!

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.

May 13, 2011
pbwebguy said...
Thanks - it is a good article. Can you post the /narnia/tools/setuserpassword.sh script?
Jun 09, 2011
Gerry said...
@Dan Ackerson and anybody else experiencing key duplication in authorized_keys:
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.

Jul 21, 2011
Ollie said...
Hi, great work, i am really looking forwards to using this. But i have a small problem, where could i find instructions for the setuserpassword.sh file? Thank you :) :)
Aug 01, 2011
Toky said...
Bringing this post back-from the dead ;-)

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.

Aug 01, 2011
Toky said...
Oops, great post about managing the keys.
Sep 14, 2011
Leonardo said...
Great post men...
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..
Oct 24, 2011
philippe said...
Thank you so much for this article !! Exactly what I wanted to do unix users management, so clearly explained!
Nov 04, 2011
star3am said...
thanks for this great write up, i digg the way you pull this off, nice one

greetings from cape town, south africa
riaan

Dec 22, 2011
Dude said...
yo nice tut, some cosmetic issue:
"cat /etc/shadow | grep $username" wins a useless use award, try grep $username /etc/shadow
also, what happens, if $username where containing "." ?
Greetings
Dec 29, 2011
Nisse said...
Nice inded, im looking for a way to keep my users in a specall file (user.pp) and then include then on nodes manual. Any idea?
Jan 02, 2012
Hans said...
Great article, thanks!

Still wondering though, how your user gets to read the mail with the random password ;)

Jan 02, 2012
AJ Bourg said...
Hans,

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.

Jan 05, 2012
Anton said...
I also have the problem with the authorized_file, this is how it will look
http://pastebin.com/2TxNJjRd
The pp file look like this:
add_ssh_key { bajs:
key =>

"AAAA....[parsed]++098a6SQWdhNcpOqnaamUXt5qQ6omazOcz+8WjrQ==",
type => "ssh-rsa"

}

Jan 10, 2012
Anton said...
Is there any smooth way to build a script that removes the user that has been created, and deletes his public keys on all the machines.
Jan 10, 2012
chris said...
@Anton

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

Jan 11, 2012
Anton said...
Thanks!
Feb 28, 2012
Kalle Larsson said...
I know it has been asked before, but is it possible to have a peek at: setuserpassword.sh ?

1000 thanks in advance,
/Kalle Larsson

Apr 24, 2012
An in-depth article about puppet configuration set up and its usability and multi-platform based usage. Though every aspect of it is covered here. But what really makes it different than other password management software is its server based application.

Leave a comment...

Original theme by Cory Watilo, banner and small tweaks by me. :)
More great Posterous themes at themes.posterous.com.