Showing with 470 additions and 113 deletions.
  1. +1 −0 .gitignore
  2. +0 −3 README
  3. +27 −0 README.md
  4. +134 −57 lib/puppet/provider/java_ks/keytool.rb
  5. +65 −8 lib/puppet/type/java_ks.rb
  6. +0 −41 manifests/init.pp
  7. +104 −0 spec/provider/java_ks/keytool_spec.rb
  8. +3 −3 spec/spec_helper.rb
  9. +136 −0 spec/type/java_ks_spec.rb
  10. +0 −1 tests/init.pp
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg
3 changes: 0 additions & 3 deletions README

This file was deleted.

27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
This modules ships a type called java_ks and a single provider named keytool. The purpose is to be able to import arbitrary, already generated and signed certificates into a java keystore for use by various applications. It has a concept of absent, present, and latest. Absent and present are self explanatory but latest will actually verify md5 certificate fingerprints for the stored certificate and the source file. Support for multiple certificates with the same alias but different keystores has been implemented using Puppet's composite namevar functionality. The mapping of title to namevars is $alias:$target (alias of certificate, colon, on disk path to the keystore). If you create dependencies on these resources you need to remember to use the same title syntax outlined for generating the composite namevars. To have a java application server use a specific certificate for incoming connections you will need to import the private key accompanying signed certificate you want to use at the same time, this is a limitation of keytool. As long as you provide the path to the key and the certificate the provider will do the conversion for you.

Note about composite namevars. The way they currently work you must have the colon in the title. YES even if you define name and target parameters. The title can be 'foo:bar' but the name and target parameters be 'broker.example.com' and '/etc/activemq/broker.ks' and it will do as you expect and correctly create an entry in the broker.ks keystore with the alias of broker.example.com...I think you could consider this a bug.

Example Usage:
```puppet
java_ks { 'puppetca:truststore':
ensure => latest,
certificate => '/etc/puppet/ssl/certs/ca.pem',
target => '/etc/activemq/broker.ts',
password => 'puppet',
trustcacerts => true,
}
java_ks { 'puppetca:keystore':
ensure => latest,
certificate => '/etc/puppet/ssl/certs/ca.pem',
target => '/etc/activemq/broker.ks',
password => 'puppet',
trustcacerts => true,
}
java_ks { 'broker.example.com:/etc/activemq/broker.ks':
ensure => latest,
certificate => '/etc/puppet/ssl/certs/broker.example.com.pe-internal-broker.pem',
private_key => '/etc/puppet/ssl/private_keys/broker.example.com.pe-internal-broker.pem',
password => 'puppet',
}
```
191 changes: 134 additions & 57 deletions lib/puppet/provider/java_ks/keytool.rb
Original file line number Diff line number Diff line change
@@ -1,99 +1,176 @@
require 'puppet/util/filetype'
Puppet::Type.type(:java_ks).provide(:keytool) do
desc 'Uses a combination of openssl and keytool to manage Java keystores'

commands :openssl => 'openssl'
commands :keytool => 'keytool'

# Keytool can only import a keystore if the format is pkcs12. Generating and
# importing a keystore is used to add private_key and certifcate pairs.
def to_pkcs12
cmd = [command(:openssl)]
cmd << 'pkcs12' << '-export'
cmd << '-in' << @resource[:certificate]
cmd << '-inkey' << @resource[:private_key]
cmd << '-name' << @resource[:name]
cmd << '-passout' << "pass:#{@resource[:password]}"
raw, status = Puppet::Util::SUIDManager.run_and_capture(cmd)
return raw
output = ''
cmd = [
command(:openssl),
'pkcs12', '-export', '-passout', 'stdin',
'-in', @resource[:certificate],
'-inkey', @resource[:private_key],
'-name', @resource[:name]
]
tmpfile = Tempfile.new("#{@resource[:name]}.")
tmpfile.write(@resource[:password])
tmpfile.flush
output = Puppet::Util.execute(
cmd,
:stdinfile => tmpfile.path,
:failonfail => true,
:combine => true
)
tmpfile.close!
return output
end

# Where we actually to the import of the file created using to_pkcs12.
def import_ks
Tempfile.open("#{@resource[:name]}.pk12.") do |tmpfile|
tmpfile.write(to_pkcs12)
tmpfile.flush
cmd = [command(:keytool)]
cmd << '-importkeystore'
cmd << '-trustcacerts' if @resource[:trustcacerts] == :true
cmd << '-destkeystore' << @resource[:target]
cmd << '-destkeypass' << @resource[:password]
cmd << '-deststorepass' << @resource[:password]
cmd << '-srckeystore' << tmpfile.path.to_s
cmd << '-srcstorepass' << @resource[:password]
cmd << '-srcstoretype' << 'PKCS12'
cmd << '-alias' << @resource[:name]
Puppet::Util.execute(cmd)
tmppk12 = Tempfile.new("#{@resource[:name]}.")
tmppk12.write(to_pkcs12)
tmppk12.flush
cmd = [
command(:keytool),
'-importkeystore', '-srcstoretype', 'PKCS12',
'-destkeystore', @resource[:target],
'-srckeystore', tmppk12.path,
'-alias', @resource[:name]
]
cmd << '-trustcacerts' if @resource[:trustcacerts] == :true
tmpfile = Tempfile.new("#{@resource[:name]}.")
if File.exists?(@resource[:target])
tmpfile.write("#{@resource[:password]}\n#{@resource[:password]}")
else
tmpfile.write("#{@resource[:password]}\n#{@resource[:password]}\n#{@resource[:password]}")
end
tmpfile.flush
Puppet::Util.execute(
cmd,
:stdinfile => tmpfile.path,
:failonfail => true,
:combine => true
)
tmppk12.close!
tmpfile.close!
end

def exists?
cmd = [command(:keytool)]
cmd << '-list'
cmd << '-keystore' << @resource[:target]
cmd << '-alias' << @resource[:name]
cmd << '-storepass' << @resource[:password]
raw, status = Puppet::Util::SUIDManager.run_and_capture(cmd)
if status == 0
cmd = [
command(:keytool),
'-list',
'-keystore', @resource[:target],
'-alias', @resource[:name]
]
begin
tmpfile = Tempfile.new("#{@resource[:name]}.")
tmpfile.write(@resource[:password])
tmpfile.flush
Puppet::Util.execute(
cmd,
:stdinfile => tmpfile.path,
:failonfail => true,
:combine => true
)
tmpfile.close!
return true
else
rescue
return false
end
end

# Reading the fingerprint of the certificate on disk.
def latest
cmd = [command(:openssl)]
cmd << 'x509' << '-fingerprint' << '-md5' << '-noout'
cmd << '-in' << @resource[:certificate]
raw, status = Puppet::Util::SUIDManager.run_and_capture(cmd)
latest = raw.scan(/MD5 Fingerprint=(.*)/)[0][0]
cmd = [
command(:openssl),
'x509', '-fingerprint', '-md5', '-noout',
'-in', @resource[:certificate]
]
output = Puppet::Util.execute(cmd)
latest = output.scan(/MD5 Fingerprint=(.*)/)[0][0]
return latest
end

# Reading the fingerprint of the certificate currently in the keystore.
def current
cmd = [command(:keytool)]
cmd << '-list'
cmd << '-keystore' << @resource[:target]
cmd << '-alias' << @resource[:name]
cmd << '-storepass' << @resource[:password]
raw, status = Puppet::Util::SUIDManager.run_and_capture(cmd)
current = raw.scan(/Certificate fingerprint \(MD5\): (.*)/)[0][0]
output = ''
cmd = [
command(:keytool),
'-list',
'-keystore', @resource[:target],
'-alias', @resource[:name]
]
tmpfile = Tempfile.new("#{@resource[:name]}.")
tmpfile.write(@resource[:password])
tmpfile.flush
output = Puppet::Util.execute(
cmd,
:stdinfile => tmpfile.path,
:failonfail => true,
:combine => true
)
tmpfile.close!
current = output.scan(/Certificate fingerprint \(MD5\): (.*)/)[0][0]
return current
end

# Determine if we need to do an import of a private_key and certificate pair
# or just add a signed certificate, then do it.
def create
if ! @resource[:certificate].nil? and ! @resource[:private_key].nil?
import_ks
elsif @resource[:certificate].nil? and ! @resource[:private_key].nil?
raise Puppet::Error 'Keytool is not capable of importing a private key
without an accomapaning certificate.'
raise Puppet::Error 'Keytool is not capable of importing a private key without an accomapaning certificate.'
else
cmd = [command(:keytool)]
cmd << '-importcert' << '-noprompt'
cmd = [
command(:keytool),
'-importcert', '-noprompt',
'-alias', @resource[:name],
'-file', @resource[:certificate],
'-keystore', @resource[:target]
]
cmd << '-trustcacerts' if @resource[:trustcacerts] == :true
cmd << '-alias' << @resource[:name]
cmd << '-file' << @resource[:certificate]
cmd << '-keystore' << @resource[:target]
cmd << '-storepass' << @resource[:password]
Puppet::Util.execute(cmd)
tmpfile = Tempfile.new("#{@resource[:name]}.")
if File.exists?(@resource[:target])
tmpfile.write(@resource[:password])
else
tmpfile.write("#{@resource[:password]}\n#{@resource[:password]}")
end
tmpfile.flush
Puppet::Util.execute(
cmd,
:stdinfile => tmpfile.path,
:failonfail => true,
:combine => true
)
tmpfile.close!
end
end

def destroy
cmd = [command(:keytool)]
cmd << '-delete'
cmd << '-alias' << @resource[:name]
cmd << '-keystore' << @resource[:target]
cmd << '-storepass' << @resource[:password]
Puppet::Util.execute(cmd)
cmd = [
command(:keytool),
'-delete',
'-alias', @resource[:name],
'-keystore', @resource[:target]
]
tmpfile = Tempfile.new("#{@resource[:name]}.")
tmpfile.write(@resource[:password])
tmpfile.flush
Puppet::Util.execute(
cmd,
:stdinfile => tmpfile.path,
:failonfail => true,
:combine => true
)
tmpfile.close!
end

# Being safe since I have seen some additions overwrite and some just throw errors.
def update
destroy
create
Expand Down
73 changes: 65 additions & 8 deletions lib/puppet/type/java_ks.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
module Puppet
newtype(:java_ks) do
@doc = 'Manages entries in a java keystore.'
@doc = 'Manages entries in a java keystore. Uses composite namevars to
accomplish the same alias spread across multiple target keystores.'

ensurable do

desc 'Has three states, the obvious present and absent plus latest. Latest
will compare the on disk MD5 fingerprint of the certificate and to that
in keytool to determine if insync? returns true or false. We redefine
insync? for this paramerter to accomplish this.'

newvalue(:present) do
provider.create
end
Expand Down Expand Up @@ -35,35 +41,86 @@ def insync?(is)
end
end

false
return false
end

defaultto :present
end

newparam(:name) do
desc ''
desc 'The alias that is used to identify the entry in the keystore. We
are down casing it for you here because keytool will do so for you too.'

isnamevar

munge do |value|
value.downcase
end
end

newparam(:target) do
desc ''
desc 'Destination file for the keystore. We autorequire the parent
directory for conveinance.'

isnamevar
end

newparam(:certificate) do
desc ''
desc 'An already signed certificate that we can place in the keystore. We
autorequire the file for conveinence.'

isrequired
end

newparam(:private_key) do
desc ''
desc 'If you desire for an application to be a server and encrypt traffic
you will need a private key. Private key entries in a keystore must be
accompanied by a signed certificate for the keytool provider.'
end

newparam(:password) do
desc ''
desc 'The password used to protect the keystore. If private keys are
sebsequently also protected this password will be used to attempt
unlocking...P.S. Let me know if you eve need a seperate private key
password parameter...'

isrequired
end

newparam(:trustcacerts) do
desc ''
desc "When inputing certificate authorities into a keystore, they aren't
by default trusted so if you are adding a CA you need to set this to true."

newvalues(:true, :false)

defaultto :false
end

# Where we setup autorequires.
autorequire(:file) do
auto_requires = []
[:private_key, :certificate].each do |param|
if @parameters.include?(param)
auto_requires << @parameters[param].value
end
end
if @parameters.include?(:target)
auto_requires << ::File.dirname(@parameters[:target].value)
end
auto_requires
end

# Our title_patterns method for mapping titles to namevars for supporting
# composite namevars.
def self.title_patterns
identity = lambda {|x| x}
[[
/^(.*):(.*)$/,
[
[ :name, identity ],
[ :target, identity ]
]
]]
end
end
end
Loading