Create ssh keys with puppet on a server + pubkey exchange

There are a few solutions to generate ssh keys on a puppet master/server or copy them from hiera to a box. I have got several boxes and every box needs to have ssh access to every other box. I don’t care which key it is in particular, it just have to work. I don’t want to copy the keys from somewhere, transferring private data is a unnecessary security risk so I want to create them on the node. My idea was to have a solution with four parts:

  • defined resource which can create ssh pub/priv keys for me
  • a generic fact that exports public keys
  • exported resource to export the public key as ssh_authorized_key resource
  • collect every exported pubkey except for the own one

Here is my defined resource (which is a 99% copy, I just added a top scope to it):

# this is based on https://github.com/maestrodev/puppet-ssh_keygen/blob/master/manifests/init.pp
define base::ssh_keygen (
  $user     = undef,
  $type     = undef,
  $bits     = undef,
  $home     = undef,
  $filename = undef,
  $comment  = undef,
  $options  = undef,
) {
  Exec { path => '/bin:/usr/bin' }
  $user_real = $user ? {
    undef   => $name,
    default => $user,
  }
  $type_real = $type ? {
    undef   => 'rsa',
    default => $type,
  }
  $home_real = $home ? {
    undef   => $user_real ? {
      'root'  => "/${user_real}",
      default => "/home/${user_real}",
    },
    default => $home,
  }
  $filename_real = $filename ? {
    undef   => "${home_real}/.ssh/id_${type_real}",
    default => $filename,
  }
  $type_opt = " -t ${type_real}"
  if $bits { $bits_opt = " -b ${bits}" }
  $filename_opt = " -f '${filename_real}'"
  $n_passphrase_opt = " -N ''"
  if $comment { $comment_opt = " -C '${comment}'" }
  $options_opt = $options ? {
    undef   => undef,
    default => " ${options}",
  }
  exec { "ssh_keygen-${name}":
    command => "ssh-keygen${type_opt}${bits_opt}${filename_opt}${n_passphrase_opt}${comment_opt}${options_opt}",
    user    => $user_real,
    creates => $filename_real,
  }
}

And here is my custom fact to scan root + postgres user + all normal users for pubkeys and creates facts with comment + the key itself:

Dir.glob(["/root/.ssh/id_*.pub", "/home/*/.ssh/id_*.pub"]).each do |glob|
  # maybe our regex fails, so jump ahead if so
  user = /\w+(?=\/\.ssh)/.match(glob).to_s
  next if user.empty?
  file = File.open(glob)
  line = file.gets.chomp
  type = line.split[0].split('-')[1]
  pubkey = line.split[1]
  comment = line.split[2]

  Facter.add("#{user}_#{type}_pubkey") do
    setcode do
      pubkey
    end
  end
  Facter.add("#{user}_#{type}_comment") do
    setcode do
      comment
    end
  end
end
Dir.glob("/var/lib/pgsql/.ssh/id_*.pub").each do |glob|
  file = File.open(glob)
  line = file.gets.chomp
  type = line.split[0].split('-')[1]
  pubkey = line.split[1]
  comment = line.split[2]

  Facter.add("postgres_#{type}_pubkey") do
    setcode do
      pubkey
    end
  end
  Facter.add("postgres_#{type}_comment") do
    setcode do
      comment
    end
  end
end

Now this allows us to use the following puppet profile:

class profiles::myawesomesshkeyexchange {
  ## create ssh key for root
  base::ssh_keygen{root:
    type  => 'ed25519',
  }
  ## export it
  if $::root_ed25519_comment and $::root_ed25519_pubkey {
    @@ssh_authorized_key{$::root_ed25519_comment:
      ensure  => 'present',
      type    => ed25519,
      options => ['no-port-forwarding', 'no-X11-forwarding', 'no-agent-forwarding' ],
      user    => $sshuser,
      key     => $::root_ed25519_pubkey,
      tag     => 'bla',
    }
  }
  # collect it
  Ssh_Authorized_Key <<| tag == 'bla' and title != $comment |>>
}

this works for the root user, but we have to accept the fingerprint on the first connect because the key isn’t present in the known_hosts file. Also we maybe want to do this for multiple users on the system:

class profiles::myevenmoreawesomesshkeyexchange {
  # we need ssh key exchange for two users
  $type = 'ed25519'
  $myhash = {root => '/root', postgres => '/var/lib/pgsql'}
  $myhash.each |$sshuser, $homepath| {
    ## create ssh key for $sshuser
    base::ssh_keygen{$sshuser:
      type  => $type,
      home  => $homepath,
    }
    ## export it
    $pubkey = getvar("::${sshuser}_${type}_pubkey")
    $comment = getvar("::${sshuser}_${type}_comment")
    if $pubkey and $comment {
      @@ssh_authorized_key{$comment:
        ensure  => 'present',
        type    => $type,
        options => ['no-port-forwarding', 'no-X11-forwarding', 'no-agent-forwarding' ],
        user    => $sshuser,
        key     => $pubkey,
        tag     => 'bla',
      }
    }
    # collect it
    Ssh_Authorized_Key <<| tag == 'bla' and title != $comment |>>
  }

  ## export host key
  if $::sshecdsakey {
    @@sshkey{$::fqdn:
      host_aliases  => $::ipaddress,
      type          => 'ecdsa-sha2-nistp256',
      key           => $::sshecdsakey,
      tag           => 'bla',
    }
  }
  ## import host key
  Sshkey <<| tag == 'bla' and title != $::fqdn |>>
}

Now we’ve a setup that can automatically create, export and exchange ssh keys for multiple users on multiple servers without any manual interaction. Thanks to exported resources this works even when new nodes join the setup, when somebody deletes a key or accident or if boxes get removed. If you manage all entries in the authorized_keys file you should ensure that puppet removes all unknown keys:

user {'root':
  purge_ssh_keys  => true,
}
This entry was posted in General, IT-Security, Linux, Puppet. Bookmark the permalink.

One Response to Create ssh keys with puppet on a server + pubkey exchange

  1. Pingback: Create a simple streaming replication for postgres with puppet | the world needs more puppet!

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload CAPTCHA.