Authorized_keys Recipe
Please feel free to offer your ideas and suggestions for improvement.
Changes
2008-07-03
- Add a shell parameter because, really, do we all want bash?
2007-06-02
- Updated to comply with Style Guide.
2007-03-30
- $homeroot now replaces hard-coded home directory locations. Defaults to /home if not used.
- authorized-keys() now uses generate() to check if the public key has been shared before attempting to use it.
2007-03-20
- Complete overhaul of this doc
Features
- Central administration of non-privileged local accounts.
- SSH key pair generation (master accounts only).
- Dynamic propagation of user's public key from central location.
- Ability to add/revoke any user's public key to/from any user's authorized_keys file (key sharing).
- authorized_keys file is secured to prevent tampering.
- authorized_keys2 is symlinked for backwards compatibility.
- Separate revocation facility that overrides key sharing.
Assumptions
- Puppet version is 0.22.2 or greater.
- The directory holding the public keys is symlinked under the templates directory.
- Private key is not secured with a passphrase since that would require user intervention. This may not be a good idea for all environments. However, users can add a passphrase with ssh-keygen.
Implementation
2 server roles exist:
| ROLE | DESCRIPTON |
| Key Server | Key pair generation occurs here when a user's "Master Account" is created. Due to a dependency on templates, this should be the server running puppetmasterd. |
| Puppet Client | Any other server in the puppet environment. User's account is optional here |
Several classes and definitions have been developed for the purpose of managing accounts and enabling key-based authentication:
| NAME | TYPE | USED BY | DESCRIPTION |
| key-server | class | Key Server | Creates the key share location and calls the accounts-master class |
| accounts-master | class | key-server class | Subclasses accounts class and overrides keygen => true |
| accounts | class | Very similar to accounts-master but defaults to keygen => false. Also holds additional user elements. | |
| account | definition | accounts-master and accounts classes | Creates the account. If keygen => true, call ssh-keygen |
| sshkeygen | definition | account definition | Creates the user's key pair and copies public key to the share defined in key-server class |
| authorized-keys | definition | account definition or any node | A wrapper to add or delete a key from a user's ~/.ssh/authorized_keys file |
| line | definition | authorized-keys | Add/remove a key to/from user's authorized_keys file |
Adding an Account
- Based on what servers the account should be authorized to use, identify the appropriate class or classes that should be updated with the account information.
Deleting an Account
Set ensure => absent in the call to whatever definition is responsible for maintaining the account.
Enabling SSH Key Authentication
If we want the user to authenticate with ssh keys:
- Add the userid to the corresponding -master classes (For example: accounts-master if the accounts class was previously updated).
- On the Key Server, set the user's password after verifying the account was created by puppet. If you are impatient, you can run puppetd manually and the account should be created. Doing so will provide the user with a means to authenticate and retrieve their newly created private key from ~/.ssh.
- Each server that inherits whatever class or classes that were updated in step 2 of the Adding an Account section will append the contents of the user's shared public key to the user's authorized_key file the next time puppetd runs (Every 30 minutes by default).
Assigning Keys
At times, it may be desirable to allow a user A to authenticate as user B by adding user A's public key to user B's authorized_keys file. This is possible on any given host as long as the following conditions are true:
- User A has had an both an account and key pair created as a result of an appropriate "master" class assigned to the key server.
- User B has an account on the node in question.
In the appropriate node definition, add or append the call to the authorized-keys definition with ensure set to present or absent. Here's an example:
node foo {
include accounts
authorized-keys { "Management of authorized_keys for foo":
[ key_owner => "kdiamond", auth_user => "rwilliams", ensure => present;
key_owner => "cmccoy", auth_user => "rsmith", ensure => present;
key_owner => "kking", auth_user => "rhalford", ensure => present; ]
}
}
Revoking a Key
At times, it may become necessary to revoke a key from an authorized_keys file without deliberately affecting the account. Two approaches can be used:
If the key is enabled in the appropriate node definition's call to authorized-keys, simply change ensure -> present to ensure => absent.
If not, add the string to be removed to the user's corresponding .revoke file on the Key Server. At the time of this writing, the file should be located in the /var/keys directory. On the next puppet run, the client will remove the string if it is found in the user's authorized_keys file.
Also worth noting, is that the contents of the .revoke file appear to override the existence of ensure -> present as described above.
Code
key-server class
class key-server {
file { "/var/keys":
owner => "root",
group => "root",
mode => 644,
ensure => directory,
}
include sudo
include accounts-master
}
accounts-master class
class accounts-master inherits accounts {
Account ["akosmin"]
{ keygen => true }
}
accounts class
class accounts {
$group = "users"
$homeroot = "/home"
$groups = "users2"
$keygen = false
file { $homeroot:
ensure => directory,
owner => "root",
group => "root",
mode => 755
}
account {
"akosmin":
keygen => $keygen,
homeroot => $homeroot,
group => $group,
ingroups => $groups,
fullname => "Adam Kosmin";
"windowsrefund":
keygen => $keygen,
homeroot => $homeroot,
group => $group,
ingroups => $groups,
fullname => "Windows Refund";
"peter":
keygen => $keygen,
homeroot => $homeroot,
ingroups => "wheel users",
shell => "/bin/zsh",
fullname => "Peter Abrahamsen
}
}
account definition
define account($homeroot, $group=$name, $ingroups="users", $fullname, $keygen=false, $shell="/bin/bash", $ensure=present) {
# This case component will allow us to avoid a dependency when/if we attempt
# to disable the account by passing ensure => absent
case $ensure {
present: {
$home_owner = $name
$home_group = $name
}
default: {
$home_owner = "root"
$home_group = "root"
}
}
group { $name:
ensure => present
}
user { $name:
gid => $name,
comment => $fullname,
home => "${homeroot}/$name",
shell => $shell,
ensure => $ensure,
#groups => $ingroups,
allowdupe => false,
require => Group[$name],
}
file {
"${homeroot}/$name":
ensure => directory,
owner => $home_owner,
group => $home_group,
mode => 750,
require => User[$name];
"${homeroot}/$name/.ssh":
ensure => directory,
owner => $home_owner,
group => $home_group,
mode => 700,
require => File["${homeroot}/$name"];
"${homeroot}/$name/.ssh/authorized_keys":
ensure => present,
owner => "root",
group => "root",
mode => 644,
require => File["${homeroot}/$name/.ssh"];
"${homeroot}/$name/.ssh/authorized_keys2":
ensure => "${homeroot}/$name/.ssh/authorized_keys",
require => File["${homeroot}/$name/.ssh/authorized_keys"],
}
if $keygen {
# Below will only execute on the key server
file { "/var/keys/${name}.revoke":
ensure => present,
owner => "root",
group => "root",
mode => 644,
require => [ User[$name], Group[$name], File["${homeroot}/$name"], File["${homeroot}/$name/.ssh"] ],
}
sshkeygen { $name: homeroot => $homeroot}
} else {
# Everything here runs on all clients except the key server
case $ensure {
present: {
authorized-keys { $name:
homeroot => $homeroot,
require => [ User[$name], Group[$name], File["${homeroot}/$name/.ssh"] ],
}
}
}
}
}
sshkeygen class
define sshkeygen($homeroot="/home") {
$key_share = "/var/keys"
$private_key = "${homeroot}/${name}/.ssh/id_rsa"
$public_key = "${homeroot}/${name}/.ssh/id_rsa.pub"
$authorized_keys = "${homeroot}/${name}/.ssh/authorized_keys"
$shared_key = "${key_share}/${name}_id_rsa.pub"
$revoke_key_file = "${key_share}/${name}.revoke"
file { $public_key:
checksum => md5,
require => [ User[$name], File["${homeroot}/$name/"], File["${homeroot}/$name/.ssh"] ],
}
exec { "Creating key pair for $name":
command => "ssh-keygen -t rsa -C 'Provided by Puppet for $name' -N '' -f $private_key",
creates => $private_key,
require => [ User[$name], File["${homeroot}/$name/"], File["${homeroot}/$name/.ssh"] ],
user => $name,
notify => Exec["Publishing $public_key"],
before => Exec["Building $authorized_keys"],
}
exec { "Publishing $public_key":
command => "cp $public_key $shared_key",
creates => $shared_key,
subscribe => File[$public_key],
require => File[$key_share],
}
exec { "Building $authorized_keys":
command => "cp $public_key $authorized_keys",
creates => $authorized_keys,
subscribe => File[$public_key],
require => [ User[$name], File[$public_key], File["${homeroot}/$name/"], File["${homeroot}/$name/.ssh"] ],
}
}
authorized-keys definition
define authorized-keys($homeroot="/home", $key_owner=$name, $auth_user=$name, $ensure=present) {
$shared_key_file = "${key_owner}_247_id_rsa.pub"
$read_shared_key = file("/var/keys/$shared_key_file", "/dev/null")
case $read_shared_key {
"": { }
default: {
$key = template("keys/$shared_key_file")
# Anything in this file will be wiped from authorized_keys
line { "add-key-${name}":
ensure => $ensure,
file => "${homeroot}/$auth_user/.ssh/authorized_keys",
line => $key,
require => [ User[$key_owner], User[$auth_user], File["${homeroot}/$auth_user/.ssh"] ],
}
}
}
$revoked_key_file = "${key_owner}.revoke"
$read_revoked_file = file("/var/keys/$revoked_key_file", "/dev/null")
case $read_revoked_key {
"": {}
default: {
# if the file exists, we're here
$revoked_key = template("keys/$revoked_key_file")
line { "revoke-key-${name}":
ensure => absent,
file => "${homeroot}/$auth_user/.ssh/authorized_keys",
line => $revoked_key,
require => [ User[$key_owner], User[$auth_user], File["${homeroot}/$auth_user/.ssh"] ],
}
}
}
}
line definition
# Taken from https://reductivelabs.com/trac/puppet/wiki/SimpleTextRecipes
define line($file, $line, $ensure = 'present') {
case $ensure {
default : { err ( "unknown ensure value ${ensure}" ) }
present: {
exec { "/bin/echo '${line}' >> '${file}'":
unless => "/bin/grep -Fx '${line}' '${file}'"
}
}
absent: {
exec { "/usr/bin/perl -ni -e 'print unless /^\\Q${line}\\E\$/' '${file}'":
onlyif => "/bin/grep -Fx '${line}' '${file}'"
}
}
}
}