Articles‎ > ‎Opscode Chef‎ > ‎

Managing ssh known hosts with chef cookbook.

The original Opscode recipe dated 2009 is half broken due to syntax change and might put your Chef server at risk of being overloaded with search queries (unless you use Opscode platform of course which I don't), also chef client keeps full search output in memory making chef-client to be a little memory hungry.... So I wrote a different Chef recipe which generates ssh_known_hosts only on one server and distributes it using web server since I was unable to figure out how to store dynamic files in chef not using databags/search. Probably this will get obsolete when search gets more robust. But right now this one works like a charm.

First you designate a server which runs Apache and generates known_hosts file then assign this recipe to that server (the recipe is tweaked to remove extra stuff I use here, so there can be some typos):

#
# Cookbook Name: gen_known_hosts
# Recipe:: default
#

nodes = []
# We do it this way until CHEF-1205 is fixed
search(:node, "*:*", "X_CHEF_id_CHEF_X asc",0,10000) { |n|
   if n['keys']['ssh'].has_key?('host_rsa_public') && n['keys']['ssh']['host_rsa_public'].length > 0
      t=Hash.new
      t['keys'] = Hash.new
      t['keys']['ssh'] = Hash.new
      t['keys']['ssh']['host_rsa_public']=n['keys']['ssh']['host_rsa_public']
      t['fqdn']=n['fqdn']
      t['ipaddress']=n['ipaddress']
      t['hostname']=n['hostname']
      nodes << t
   end
   n=nil
}

# Need to make own file write script because chef file operations are not atomic yet
s = bash "gensshknownhosts" do
  user "root"
  cwd "/tmp"
  action :nothing
  case node[:datacenter]
  code <<-EOH
#!/bin/sh

chefsshfile=/etc/ssh/ssh_known_hosts.chef
outputfile=/var/www/ssh/ssh_known_hosts
toutputfile=/var/www/ssh/.ssh_known_hosts

rm -rf "${toutputfile}"
cp /etc/ssh/ssh_known_hosts.chef "${toutputfile}"
if [ $? -ne 0 ] ; then
   echo "Cp error $?" >&2
   exit 1
fi
if [ ! -e "${toutputfile}" ] ; then
   echo "Cp didnt transfer file ${toutputfile}" >&2
   exit 1
fi
# Sanity check - file needs to have at least some hosts
if [ 0`ls -s "${toutputfile}" | cut -f1 -d' '` -le 4 ] ; then
   echo "The ${toutputfile} file is too small" >&2
fi
# See if file changed
if [ 0`cksum "${toutputfile}" | cut -f1 -d' '` \
    -ne 0`cksum "${outputfile}" | cut -f1 -d' '` ] ; then
   mv -f "${toutputfile}" "${outputfile}"
fi
#EOF
EOH
  end
end

template "/etc/ssh/ssh_known_hosts.chef" do
  source "known_hosts.erb"
  mode 0444
  owner "root"
  group "root"
  backup false
  variables(
     :nodes => nodes
  )
  notifies :run, resources(:bash => "gensshknownhosts")
end
s.run_action(:run)
nodes=nil
GC.start

The template known_hosts.erb looks like this:

#THIS FILE IS MAINTAINED BY CHEF, DO NOT MODIFY AS IT WILL BE OVERWRITTEN
<% @nodes.each do |n| -%>
<% if n['keys']['ssh'].has_key?('host_rsa_public') && n['keys']['ssh']['host_rsa_public'].length > 0 -%>
<%= n['hostname'] %>,<%= n['fqdn'] %>,<%= n['ipaddress'] %> ssh-rsa <%= n['keys']['ssh']['host_rsa_public'] %>
<% end -%>
<% end -%>

Now the client Chef recipe looks very lightweight and does not use much of resources:

#
remote_file  "/etc/ssh/ssh_known_hosts" do
        mode "644"
        owner "root"
        group "root"
        action :create
        backup false
        source  "http://#{node[:utilityweb]}/ssh/ssh_known_hosts"
end

All you need is to define "node[:utilityweb]" attribute (where your Web server is which contains generated known_hosts file) via role or as default attribute to the recipe.




Comments