diff options
Diffstat (limited to 'code/environments/production/modules/certregen/spec')
20 files changed, 1041 insertions, 0 deletions
diff --git a/code/environments/production/modules/certregen/spec/acceptance/ca_spec.rb b/code/environments/production/modules/certregen/spec/acceptance/ca_spec.rb new file mode 100644 index 0000000..c9df863 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/ca_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper_acceptance' + +describe "puppet certregen ca" do + if hosts_with_role(hosts, 'master').length>0 then + context 'regen ca on master' do + + context 'C99811 - without --ca_serial' do + it 'should provide ca serial id via stderr' do + on(master, puppet("certregen ca"), :acceptable_exit_codes => 1) do |result| + expect(result.stderr).to match(/rerun this command with --ca_serial ([0-9a-fA-F]+)/) + end + end + end + + context "C99815 - 'puppet certregen ca --ca_serial'" do + before(:all) do + serial = get_ca_serial_id_on(master) + today = get_time_on(master) + @future = today + 5*YEAR + @regen_result = on(master, "puppet certregen ca --ca_serial #{serial}") + end + it 'should output the updated CA expiration date' do + expect(@regen_result.stdout).to match( /CA expiration is now #{@future.utc.strftime('%Y-%m-%d')}/ ) + end + it 'should update CA cert enddate' do + enddate = get_ca_enddate_time_on(master) + expect(enddate - @future).to be < 10.0 + end + end + + context 'C99816 - invalid ca_serial id' do + it 'should yield an error' do + on(master, puppet("certregen ca --ca_serial FD"), :acceptable_exit_codes => 1) do |result| + expect(result.stderr).to match(/The serial number of the current CA certificate .* does not match the serial number given on the command line \(FD\)/) + expect(result.stderr).to match(/rerun this command with --ca_serial ([0-9a-fA-F]+)/) + end + end + end + + context "C99817 - 'puppet certregen ca --ca_serial --ca_ttl 1d'" do + before(:all) do + today = get_time_on(master) + @tomorrow = today + 1*DAY + + serial = get_ca_serial_id_on(master) + @regen_result = on(master, "puppet certregen ca --ca_serial #{serial} --ca_ttl 1d") + end + + it 'should output the updated CA expiration date' do + expect(@regen_result.stdout).to match( /CA expiration is now #{@tomorrow.utc.strftime('%Y-%m-%d')}/ ) + end + it 'should update CA cert enddate' do + enddate = get_ca_enddate_time_on(master) + expect(enddate - @tomorrow).to be < 10.0 + end + end + + end + end +end diff --git a/code/environments/production/modules/certregen/spec/acceptance/healthcheck_spec.rb b/code/environments/production/modules/certregen/spec/acceptance/healthcheck_spec.rb new file mode 100644 index 0000000..387810d --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/healthcheck_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper_acceptance' +require 'yaml' +require 'json' + +describe "puppet certregen healthcheck" do + if hosts_with_role(hosts, 'master').length>0 then + + context 'C99803 - cert with more than 10 percent of life' do + before(:all) do + serial = get_ca_serial_id_on(master) + on(master, "puppet certregen ca --ca_serial #{serial}") + end + it 'should not produce a health warning' do + on(master, "puppet certregen healthcheck") do |result| + expect(result.stderr).to be_empty + expect(result.stdout).to match(/No certificates are approaching expiration/) + end + end + end + + context 'C99804 - cert with less than 10 percent of life' do + before(:all) do + serial = get_ca_serial_id_on(master) + # patch puppet to defeat copywrite date check when generating historical CA + patch_puppet_date_check_on(master) + @today = get_time_on(master) + # set back the clock in order to create a CA that will be approaching its EOL + past = @today - (5*YEAR - 20*DAY) + on(master, "date #{past.strftime('%m%d%H%M%Y')}") + # create old CA + on(master, "puppet certregen ca --ca_serial #{serial}") + # update to current time + on(master, "date #{@today.strftime('%m%d%H%M%Y')}") + # revert patch to defeat copywrite date check + patch_puppet_date_check_on(master, 'reverse') + end + + it 'system should have current date' do + today = get_time_on(master) + expect(today.utc.strftime('%Y-%m-%d')).to eq @today.utc.strftime('%Y-%m-%d') + end + + it 'should warn about pending expiration' do + enddate = get_ca_enddate_time_on(master) + on(master, "puppet certregen healthcheck") do |result| + expect(result.stdout).to match(/Status:\s+expiring/) + expect(result.stdout).to match(/Expiration date:\s+#{enddate.utc.strftime('%Y-%m-%d')}/) + end + end + + end + + context 'C99805 - expired cert' do + before(:all) do + serial = get_ca_serial_id_on(master) + on(master, "puppet certregen ca --ca_serial #{serial} --ca_ttl 1s") + sleep 2 + end + it 'should produce a health warning' do + on(master, "puppet certregen healthcheck") do |result| + expect(result.stdout.gsub("\n", " ")).to match(/ca.*Status: expired/) + end + end + end + + context '--all flag' do + + context 'C99806 --all' do + before(:all) do + on(master, puppet("cert list --all")) do |result| + @certs = result.stdout.scan(/\) ([A-F0-9:]+) /) + end + @result = on(master, "puppet certregen healthcheck --all") + end + it 'should contain expiration data for ca cert' do + expect(@result.stdout).to match(/"ca".*\n\s*Status:\s*[Ee]xpir/) + end + it 'should contain expiration data for all node certs' do + @certs.each do |cert| + expect(@result.stdout).to include cert[0] + end + end + end + + context '--render-as flag' do + + context 'C99808 - --render-as yaml' do + before(:all) do + on(master, puppet("cert list --all")) do |result| + @certs = result.stdout.scan(/\) ([A-F0-9:]+) /) + end + @result = on(master, "puppet certregen healthcheck --all --render-as yaml") + @yaml = YAML.load(@result.stdout) + end + it 'should return valid yaml' do + expect(YAML.parse(@result.stdout)).to be_instance_of(Psych::Nodes::Document) + end + it 'should contain expiration data for ca cert' do + ca = @yaml.find { |record| record[:name] == 'ca' } + expect(ca).not_to be nil + expect(ca[:expiry][:status]).to eq(:expired) + end + it 'should contain expiration data for all node certs' do + @certs.each do |cert| + expect(@yaml.find { |record| record[:digest] =~ /#{cert[0]}/ }).not_to be nil + end + end + end + + context 'C99809 - --render-as json prints valid json containing expiration data' do + before(:all) do + on(master, puppet("cert list --all")) do |result| + @certs = result.stdout.scan(/\) ([A-F0-9:]+) /) + end + @json = JSON.parse(on(master, "puppet certregen healthcheck --all --render-as json").stdout) + end + it 'should return valid json' do + expect(@json).not_to be nil + end + it 'should contain expiration data for ca cert' do + ca = @json.find { |record| record['name'] == 'ca' } + expect(ca).not_to be nil + end + it 'should contain expiration data for all node certs' do + @certs.each do |cert| + expect(@json.find { |record| record['digest'] =~ /#{cert[0]}/ }).not_to be nil + end + end + end + + end + end + + end +end diff --git a/code/environments/production/modules/certregen/spec/acceptance/help_spec.rb b/code/environments/production/modules/certregen/spec/acceptance/help_spec.rb new file mode 100644 index 0000000..7d1e83d --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/help_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper_acceptance' + +describe "pupper help certregen" do + # NOTE: MODULES-4733 certregen is not currently compatible with ruby < 1.9 + ruby_ver = 0 + on(default, 'ruby --version') do |result| + m = /\d+\.\d+\.\d+/.match(result.stdout) + ruby_ver = m[0] if m + end + unless version_is_less(ruby_ver, '1.9') then + describe "C98923 - Verify that 'puppet certregen --help' prints help text" do + # NOTE: `--help` only works on puppet version 4+ + if version_is_less( '3.9.9', on(default, puppet('--version')).stdout) + describe command("puppet certregen --help") do + its(:stdout) { should match( /.*USAGE: puppet certregen <action>.*/ ) } + its(:stdout) { should match( /.*See 'puppet man certregen' or 'man puppet-certregen' for full help.*/ ) } + end + end + end + describe "C99812 - Verify that 'puppet help certregen' prints help text" do + describe command("puppet help certregen") do + its(:stdout) { should match( /.*USAGE: puppet certregen <action>.*/ ) } + its(:stdout) { should match( /.*See 'puppet man certregen' or 'man puppet-certregen' for full help.*/ ) } + end + end + describe "C99813 - Verify that 'puppet help certregen healthcheck' prints help text for healthcheck subcommand" do + describe command("puppet help certregen healthcheck") do + its(:stdout) { should match( /.*USAGE: puppet certregen healthcheck .*/ ) } + its(:stdout) { should match( /.*See 'puppet man certregen' or 'man puppet-certregen' for full help.*/ ) } + end + end + describe "C99814 - Verify that 'puppet help certregen ca' prints help text for ca subcommand" do + describe command("puppet help certregen ca") do + its(:stdout) { should match( /.*USAGE: puppet certregen ca .*/ ) } + its(:stdout) { should match( /.*See 'puppet man certregen' or 'man puppet-certregen' for full help.*/ ) } + end + end + end +end diff --git a/code/environments/production/modules/certregen/spec/acceptance/helpers.rb b/code/environments/production/modules/certregen/spec/acceptance/helpers.rb new file mode 100644 index 0000000..dba7d81 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/helpers.rb @@ -0,0 +1,83 @@ +require 'openssl' + +# Time constants in seconds +HOUR = 60 * 60 +DAY = 24 * HOUR +YEAR = 365 * DAY + +# Retrieve CA Certificate from the given host +# +# @param [Host] host single Beaker::Host +# +# @return [OpenSSL::X509::Certificate] Certificate object +def get_ca_cert_on(host) + if host[:roles].include? 'master' then + dir = on(host, puppet('config', 'print', 'cadir')).stdout.chomp + ca_path = "#{dir}/ca_crt.pem" + else + dir = on(host, puppet('config', 'print', 'certdir')).stdout.chomp + ca_path = "#{dir}/ca.pem" + end + on(host, "cat #{ca_path}") do |result| + cert = OpenSSL::X509::Certificate.new(result.stdout) + return cert + end +end + +# Execute `date` command on host with optional arguments +# and get back a Ruby Time object +# +# @param [Host] host single Beaker::Host to run the command on +# @param [Array<String>] args Array of arguments to be appended to the +# `date` command +# @return [Time] Ruby Time object +def get_time_on(host, args = []) + arg_string = args.join(' ') + date = on(host, "date #{arg_string}").stdout.chomp + return Time.parse(date) +end + +# Retrieve the CA enddate on a given host as a Ruby time object +# +# @param [Host] host single Beaker::Host to get CA enddate from +# +# @return [Time] Ruby Time object, or nil if error +def get_ca_enddate_time_on(host) + cert = get_ca_cert_on(host) + return cert.not_after if cert + return nil +end + +# Retrieve the current ca_serial value for `puppet certgen ca` on a given host +# +# @param [Host] host single Beaker::Host to get ca_serial from +# +# @return [String] ca_serial in hexadecimal, or nil if error +def get_ca_serial_id_on(host) + cert = get_ca_cert_on(host) + return cert.serial.to_s(16) if cert + return nil +end + +# Patch puppet to get around the date check validation. +# +# This method is used to patch puppet in order to prevent it from failing to +# create a CA if the system clock is turned back in time by years. The same +# method is used to reverse the patch with the `reverse` parameter. +# +# @param [Host] host single Beaker::Host to run the command on +# @param [String] reverse causes the patch to be reversed +def patch_puppet_date_check_on(host, reverse=nil) + reverse = '--reverse' if reverse + apply_manifest_on(host, 'package { "patch": ensure => present}') + interface_documentation_file = "/opt/puppetlabs/puppet/lib/ruby/vendor_ruby/puppet/interface/documentation.rb" + patch =<<EOF +305c305 +< raise ArgumentError, "copyright with a year \#{fault} is very strange; did you accidentally add or subtract two years?" +--- +> #raise ArgumentError, "copyright with a year \#{fault} is very strange; did you accidentally add or subtract two years?" +EOF + patch_file = host.tmpfile('iface_doc_patch') + create_remote_file(host, patch_file, patch) + on(host, "patch #{reverse} #{interface_documentation_file} < #{patch_file}", :acceptable_exit_codes => [0,1]) +end diff --git a/code/environments/production/modules/certregen/spec/acceptance/nodesets/centos-7-x64.yml b/code/environments/production/modules/certregen/spec/acceptance/nodesets/centos-7-x64.yml new file mode 100644 index 0000000..5eebdef --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/nodesets/centos-7-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-7-x64: + roles: + - agent + - default + platform: el-7-x86_64 + hypervisor: vagrant + box: puppetlabs/centos-7.2-64-nocm +CONFIG: + type: foss diff --git a/code/environments/production/modules/certregen/spec/acceptance/nodesets/debian-8-x64.yml b/code/environments/production/modules/certregen/spec/acceptance/nodesets/debian-8-x64.yml new file mode 100644 index 0000000..fef6e63 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/nodesets/debian-8-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-8-x64: + roles: + - agent + - default + platform: debian-8-amd64 + hypervisor: vagrant + box: puppetlabs/debian-8.2-64-nocm +CONFIG: + type: foss diff --git a/code/environments/production/modules/certregen/spec/acceptance/nodesets/default.yml b/code/environments/production/modules/certregen/spec/acceptance/nodesets/default.yml new file mode 100644 index 0000000..dba339c --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/nodesets/default.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-1404-x64: + roles: + - agent + - default + platform: ubuntu-14.04-amd64 + hypervisor: vagrant + box: puppetlabs/ubuntu-14.04-64-nocm +CONFIG: + type: foss diff --git a/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/centos-7.yml b/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/centos-7.yml new file mode 100644 index 0000000..a3333aa --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/centos-7.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-7-x64: + platform: el-7-x86_64 + hypervisor: docker + image: centos:7 + docker_preserve_image: true + docker_cmd: '["/usr/sbin/init"]' + # install various tools required to get the image up to usable levels + docker_image_commands: + - 'yum install -y crontabs tar wget openssl sysvinit-tools iproute which initscripts' +CONFIG: + trace_limit: 200 diff --git a/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/debian-8.yml b/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/debian-8.yml new file mode 100644 index 0000000..df5c319 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/debian-8.yml @@ -0,0 +1,11 @@ +HOSTS: + debian-8-x64: + platform: debian-8-amd64 + hypervisor: docker + image: debian:8 + docker_preserve_image: true + docker_cmd: '["/sbin/init"]' + docker_image_commands: + - 'apt-get update && apt-get install -y net-tools wget locales strace lsof && echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen' +CONFIG: + trace_limit: 200 diff --git a/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/ubuntu-14.04.yml b/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/ubuntu-14.04.yml new file mode 100644 index 0000000..b1efa58 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/nodesets/docker/ubuntu-14.04.yml @@ -0,0 +1,12 @@ +HOSTS: + ubuntu-1404-x64: + platform: ubuntu-14.04-amd64 + hypervisor: docker + image: ubuntu:14.04 + docker_preserve_image: true + docker_cmd: '["/sbin/init"]' + docker_image_commands: + # ensure that upstart is booting correctly in the container + - 'rm /usr/sbin/policy-rc.d && rm /sbin/initctl && dpkg-divert --rename --remove /sbin/initctl && apt-get update && apt-get install -y net-tools wget && locale-gen en_US.UTF-8' +CONFIG: + trace_limit: 200 diff --git a/code/environments/production/modules/certregen/spec/acceptance/workflow_regen_after_expire_spec.rb b/code/environments/production/modules/certregen/spec/acceptance/workflow_regen_after_expire_spec.rb new file mode 100644 index 0000000..3ae0a9e --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/workflow_regen_after_expire_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper_acceptance' +require 'json' + +# https://forge.puppet.com/puppetlabs/certregen#revive-a-ca-thats-already-expired +describe "C99821 - workflow - regen CA after it expires" do + if find_install_type == 'pe' then + # This workflow only works with a master to manage the CA + # This workflow only works with a puppetdb instance to query hostnames from + context 'create CA to be expired and update agents' do + before(:all) do + ttl = 60 + serial = get_ca_serial_id_on(master) + on(master, puppet("certregen ca --ca_serial #{serial} --ca_ttl #{ttl}s")) + start = Time.now + agents.each do |agent| + on(agent, puppet('agent -t'), :acceptable_exit_codes => [0,2]) + end + finish = Time.now + elapsed_time = (finish - start).to_i + sleep (ttl - elapsed_time) if elapsed_time < ttl + sleep 1 + end + + it 'should warn that ca is expired' do + on(master, puppet("certregen healthcheck")) do |result| + expect(result.stdout).to match(/Status:\s+expired/) + end + end + + context 'regenerate CA' do + before(:all) do + serial = get_ca_serial_id_on(master) + on(master, puppet("certregen ca --ca_serial #{serial}")) + end + + it 'should update CA cert enddate' do + enddate = get_ca_enddate_time_on(master) + future = get_time_on(master, ['-d', "'5 years'"]) + expect(future - enddate).to be <= (48*HOUR) + end + + context 'automatically distribute new ca to linux hosts' do + before(:all) do + # distribute ssh key for root to agents + on(master, "ssh-keygen -t rsa -f $HOME/.ssh/id_rsa -P ''") + on(master, "cat $HOME/.ssh/id_rsa.pub") do |result| + key_array = result.stdout.split(' ') + fail_test('could not get ssh key from master') unless key_array.size > 1 + @public_key = key_array[1] + end + agents.each do |agent| + unless agent['platform'] =~ /windows/ + args = ['ensure=present', + "user='root'", + "type='rsa'", + "key='#{@public_key}'", + ] + on(agent, puppet_resource('ssh_authorized_key', master.hostname, args)) + on(master, "ssh -o StrictHostKeyChecking=no #{agent.hostname} ls") + end + end + on(master, "/opt/puppetlabs/puppet/bin/gem install chloride") + result = on(master, puppet("certregen redistribute")) + @report = JSON.parse(result.stdout) + end + + after(:all) do + on(master, "rm -f $HOME/.ssh/id_rsa $HOME/.ssh/id_rsa.pub", :acceptable_exit_codes => [0,1]) + agents.each do |agent| + on(agent, puppet_resource('ssh_authorized_key', master.hostname, ['ensure=absent', "user='root'"]), :acceptable_exit_codes => [0,1]) + end + end + + it 'should emit a report in valid json' do + expect(@report).not_to be nil + end + it 'should emit a report with a succeeded key' do + expect(@report['succeeded']).not_to be nil + end + it 'should emit a report with a failed key' do + expect(@report['failed']).not_to be nil + end + it 'should report success on all linux agents' do + agents.each do |agent| + if agent['platform'] =~ /debian|ubuntu|cumulus|huaweios|el-|centos|fedora|redhat|oracle|scientific|eos|archlinux|sles/ + expect(@report['succeeded']).to include agent.hostname + end + end + end + it 'should update CA cert on all linux agents' do + master_enddate = get_ca_enddate_time_on(master) + agents.each do |agent| + if agent['platform'] =~ /debian|ubuntu|cumulus|huaweios|el-|centos|fedora|redhat|oracle|scientific|eos|archlinux|sles/ + on(agent, puppet('agent -t'), :acceptable_exit_codes => [0,2]) + enddate = get_ca_enddate_time_on(agent) + expect(enddate).to eq master_enddate + end + end + end + end + + end + end + end +end diff --git a/code/environments/production/modules/certregen/spec/acceptance/workflow_regen_before_expire_spec.rb b/code/environments/production/modules/certregen/spec/acceptance/workflow_regen_before_expire_spec.rb new file mode 100644 index 0000000..bad7a84 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/acceptance/workflow_regen_before_expire_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper_acceptance' + +# https://forge.puppet.com/puppetlabs/certregen#refresh-a-ca-thats-expiring-soon +describe "C99818 - workflow - regen CA before it expires" do + if hosts_with_role(hosts, 'master').length>0 then + # This workflow only works with a master to manage the CA + context 'setting CA to expire soon' do + before(:all) do + serial = get_ca_serial_id_on(master) + + # patch puppet to defeat copywrite date check when generating historical CA + patch_puppet_date_check_on(master) + + # determine current time on master + @today = get_time_on(master) + + # set back the clock in order to create a CA that will be approaching its EOL + past = @today - (5*YEAR - 20*DAY) + on(master, "date #{past.strftime('%m%d%H%M%Y')}") + # create old CA + on(master, puppet(" certregen ca --ca_serial #{serial}")) + # update to current time + on(master, "date #{@today.strftime('%m%d%H%M%Y')}") + end + + it 'should have current date' do + today = get_time_on(master) + expect(today.utc.strftime('%Y-%m-%d')).to eq @today.utc.strftime('%Y-%m-%d') + end + + it 'should warn about pending expiration' do + enddate = get_ca_enddate_time_on(master) + on(master, puppet("certregen healthcheck")) do |result| + expect(result.stdout).to match(/Status:\s+expiring/) + expect(result.stdout).to match(/Expiration date:\s+#{enddate.utc.strftime('%Y-%m-%d')}/) + end + end + + context 'restoring previously patched puppet' do + before(:all) do + # revert patch to defeat copywrite date check + patch_puppet_date_check_on(master, 'reverse') + end + + context 'regenerating CA prior to expiration' do + before(:all) do + serial = get_ca_serial_id_on(master) + on(master, puppet("certregen ca --ca_serial #{serial}")) + end + # validate time stamp + it 'should update CA cert enddate' do + enddate = get_ca_enddate_time_on(master) + future = get_time_on(master, ['-d', "'5 years'"]) + expect(future - enddate).to be <= (48*HOUR) + end + + context 'distribute new ca to linux hosts that have been classified with `certregen::client`' do + before(:all) do + create_remote_file(master, '/etc/puppetlabs/code/environments/production/manifests/ca.pp', 'include certregen::client') + on(master, 'chmod 755 /etc/puppetlabs/code/environments/production/manifests/ca.pp') + on(master, puppet('agent -t'), :acceptable_exit_codes => [0,2]) + end + it 'should update CA cert on all linux agents' do + master_enddate = get_ca_enddate_time_on(master) + agents.each do |agent| + on(agent, puppet('agent -t'), :acceptable_exit_codes => [0,2]) + enddate = get_ca_enddate_time_on(agent) + expect(enddate).to eq master_enddate + end + end + end + + end + end + end + end +end diff --git a/code/environments/production/modules/certregen/spec/classes/client_spec.rb b/code/environments/production/modules/certregen/spec/classes/client_spec.rb new file mode 100644 index 0000000..843c3b1 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/classes/client_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +RSpec.shared_examples "managing the CRL on the client" do |setting| + describe "when manage_crl is false" do + let(:params) {{'manage_crl' => false}} + + it "doesn't manage the hostcrl on the client" do + should_not contain_file(client_hostcrl) + end + end + + describe "when manage_crl is true" do + let(:params) {{'manage_crl' => true}} + + it "manages the hostcrl on the client from the server '#{setting}' setting" do + should contain_file(client_hostcrl).with( + 'ensure' => 'present', + 'content' => Puppet.settings.setting(setting).open(&:read), + 'mode' => '0644', + ) + end + end +end + +RSpec.describe 'certregen::client' do + include_context "Initialize CA" + + let(:client_localcacert) { tmpfilename('ca.pem') } + let(:client_hostcrl) { tmpfilename('crl.pem') } + + let(:facts) do + { + 'localcacert' => client_localcacert, + 'hostcrl' => client_hostcrl, + 'pe_build' => '2016.4.0', + } + end + + before do + Puppet.settings.setting(:localcacert).open('w') { |f| f.write("local CA cert") } + Puppet.settings.setting(:hostcrl).open('w') { |f| f.write("local CRL") } + end + + describe 'when the compile master has CA ssl files' do + before do + Puppet.settings.setting(:cacert).open('w') { |f| f.write("CA cert") } + Puppet.settings.setting(:cacrl).open('w') { |f| f.write("CA CRL") } + end + + describe "managing the localcacert on the client" do + it do + should contain_file(client_localcacert).with( + 'ensure' => 'present', + 'content' => Puppet.settings.setting(:cacert).open(&:read), + 'mode' => '0644', + ) + end + end + + it_behaves_like "managing the CRL on the client", :cacrl + end + + describe "when the compile master only has agent SSL files" do + before do + FileUtils.rm(Puppet[:cacert]) + FileUtils.rm(Puppet[:cacrl]) + end + + describe "managing the localcacert on the client" do + it 'manages the client CA cert from the `localcacert` setting' do + should contain_file(client_localcacert).with( + 'ensure' => 'present', + 'content' => Puppet.settings.setting(:localcacert).open(&:read), + 'mode' => '0644', + ) + end + end + + it_behaves_like "managing the CRL on the client", :hostcrl + end +end diff --git a/code/environments/production/modules/certregen/spec/integration/puppet/face/certregen_spec.rb b/code/environments/production/modules/certregen/spec/integration/puppet/face/certregen_spec.rb new file mode 100644 index 0000000..342aa5a --- /dev/null +++ b/code/environments/production/modules/certregen/spec/integration/puppet/face/certregen_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require 'puppet/face/certregen' + +describe Puppet::Face[:certregen, :current] do + before(:each) do + allow(Puppet::SSL::CertificateAuthority).to receive(:instance) { Puppet::SSL::CertificateAuthority.new } + end + + include_context "Initialize CA" + + describe "ca action" do + it "invokes the cacert and crl actions" do + expect(described_class).to receive(:cacert).with(ca_serial: "01") + expect(described_class).to receive(:crl) + described_class.ca(ca_serial: "01") + end + end + + describe "cacert action" do + it "raises an error when the ca_serial option is not provided" do + expect { + described_class.ca + }.to raise_error(RuntimeError, /The serial number of the CA certificate to rotate must be provided/) + end + + it "raises an error when the ca_serial option is not provided" do + expect { + described_class.ca(ca_serial: "02") + }.to raise_error(RuntimeError, /The serial number of the current CA certificate \(01\) does not match the serial number/) + end + + it "backs up the old CA cert and regenerates a new CA cert" do + old_cacert_serial = Puppet::SSL::CertificateAuthority.new.host.certificate.content.serial + described_class.ca(ca_serial: "01") + new_cacert_serial = Puppet::SSL::CertificateAuthority.new.host.certificate.content.serial + expect(old_cacert_serial).to_not eq(new_cacert_serial) + end + + it "returns the new CA certificate" do + returned_cacert = described_class.ca(ca_serial: "01").first + new_cacert = Puppet::SSL::CertificateAuthority.new.host.certificate.content + expect(returned_cacert.content.serial).to eq new_cacert.serial + expect(returned_cacert.content.not_after).to eq new_cacert.not_after + end + end + + describe 'healthcheck action' do + let(:not_before) { Time.now - (60 * 60 * 24 * 365 * 4) } + let(:not_after) { Time.now + (60 * 60 * 24 * 30) } + it 'warns about expiring CA certificates' do + ca = Puppet::SSL::CertificateAuthority.new + cert = backdate_certificate(ca, ca.host.certificate, not_before, not_after) + Puppet::SSL::Certificate.indirection.save(cert) + + allow(PuppetX::Certregen::CA).to receive(:setup).and_return Puppet::SSL::CertificateAuthority.new + healthchecked = described_class.healthcheck + expect(healthchecked.size).to eq(1) + expect(healthchecked.first.digest.to_s).to eq(cert.digest.to_s) + end + + it 'warns about expiring client certificates' do + cert = make_certificate("expiring", not_before, not_after) + Puppet::SSL::Certificate.indirection.save(cert) + + healthchecked = described_class.healthcheck + expect(healthchecked.size).to eq(1) + expect(healthchecked.first.digest.to_s).to eq(cert.digest.to_s) + end + + it 'orders certificates from shortest expiry to longest expiry' do + Puppet::SSL::Certificate.indirection.save(make_certificate("first", not_before, not_after)) + Puppet::SSL::Certificate.indirection.save(make_certificate("last", not_before + 1, not_after + 1)) + + expect(described_class.healthcheck.map(&:name)).to eq %w[first last] + end + end +end diff --git a/code/environments/production/modules/certregen/spec/integration/puppet_x/certregen/ca_spec.rb b/code/environments/production/modules/certregen/spec/integration/puppet_x/certregen/ca_spec.rb new file mode 100644 index 0000000..bb77a7d --- /dev/null +++ b/code/environments/production/modules/certregen/spec/integration/puppet_x/certregen/ca_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' +require 'puppet_x/certregen/ca' + +RSpec.describe PuppetX::Certregen::CA do + + include_context "Initialize CA" + + describe "#setup" do + it "errors out when the node is not a CA" do + Puppet[:ca] = false + expect { + described_class.setup + }.to raise_error(RuntimeError, "Unable to set up CA: this node is not a CA server.") + end + + it "errors out when the node does not have a signed CA certificate" do + FileUtils.rm(Puppet[:cacert]) + expect { + described_class.setup + }.to raise_error(RuntimeError, "Unable to set up CA: the CA certificate is not present.") + end + end + + describe '#sign' do + let(:ca) { double('ca') } + + it 'uses the positional argument form when the Puppet version predates 4.6.0' do + stub_const('Puppet::PUPPETVERSION', '4.5.0') + expect(ca).to receive(:sign).with('hello', false, true) + described_class.sign(ca, 'hello', allow_dns_alt_names: false, self_signing_csr: true) + end + + it 'uses the hash argument form when the Puppet version is 4.6.0 or greater' do + stub_const('Puppet::PUPPETVERSION', '4.8.0') + expect(ca).to receive(:sign).with('hello', allow_dns_alt_names: false, self_signing_csr: false) + described_class.sign(ca, 'hello', allow_dns_alt_names: false, self_signing_csr: false) + end + end + + describe '#backup_cacert' do + it 'backs up the CA cert based on the current timestamp' do + now = Time.now + expect(Time).to receive(:now).at_least(:once).and_return now + described_class.backup + backup = File.join(Puppet[:cadir], "ca_crt.#{Time.now.to_i}.pem") + expect(File.read(backup)).to eq(File.read(Puppet[:cacert])) + end + end + + describe '#regenerate_cacert' do + it 'generates a certificate with a different serial number' do + old_serial = Puppet::SSL::CertificateAuthority.new.host.certificate.content.serial + described_class.regenerate(Puppet::SSL::CertificateAuthority.new) + new_serial = Puppet::SSL::Certificate.indirection.find("ca").content.serial + expect(old_serial).to_not eq new_serial + end + + before do + Puppet[:ca_name] = 'bar' + described_class.regenerate(Puppet::SSL::CertificateAuthority.new) + end + + it 'copies the old subject CN to the new certificate' do + new_cacert = Puppet::SSL::Certificate.indirection.find("ca") + expect(new_cacert.content.subject.to_a[0][1]).to eq 'Puppet CA: foo' + end + + it "matches the issuer field with the old CA and new CA" do + new_cacert = Puppet::SSL::Certificate.indirection.find("ca") + expect(new_cacert.content.issuer.to_a[0][1]).to eq 'Puppet CA: foo' + end + + it "matches the Authority Key Identifier field with the old CA and new CA" do + new_cacert = Puppet::SSL::Certificate.indirection.find("ca") + aki = new_cacert.content.extensions.find { |ext| ext.oid == 'authorityKeyIdentifier' } + expect(aki.value).to match(/Puppet CA: foo/) + end + + it 'copies the cacert to the localcacert' do + described_class.regenerate(Puppet::SSL::CertificateAuthority.new) + cacert = Puppet::SSL::Certificate.from_instance( + OpenSSL::X509::Certificate.new(File.read(Puppet[:cacert]))) + localcacert = Puppet::SSL::Certificate.from_instance( + OpenSSL::X509::Certificate.new(File.read(Puppet[:localcacert]))) + expect(cacert.content.serial).to eq localcacert.content.serial + end + end +end diff --git a/code/environments/production/modules/certregen/spec/integration/puppet_x/certregen/certificate_spec.rb b/code/environments/production/modules/certregen/spec/integration/puppet_x/certregen/certificate_spec.rb new file mode 100644 index 0000000..e60a11b --- /dev/null +++ b/code/environments/production/modules/certregen/spec/integration/puppet_x/certregen/certificate_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' +require 'puppet_x/certregen/certificate' + +RSpec.describe PuppetX::Certregen::Certificate do + include_context "Initialize CA" + + let(:ok_certificate) do + Puppet::SSL::CertificateAuthority.new.generate("ok") + end + + let(:expired_certificate) do + one_year = 60 * 60 * 24 * 365 + not_before = Time.now - one_year * 6 + not_after = Time.now - one_year + make_certificate("expired", not_before, not_after) + end + + let(:expiring_certificate) do + not_before = Time.now - (60 * 60 * 24 * 365 * 4) + not_after = Time.now + (60 * 60 * 24 * 30) + make_certificate("expiring", not_before, not_after) + end + + let(:short_lived_certificate) do + not_before = Time.now - 86400 + not_after = Time.now + (60 * 5) + make_certificate("expiring", not_before, not_after) + end + + describe "#expiring?" do + it "is false for nodes outside of the expiration window" do + expect(described_class.expiring?(ok_certificate)).to eq(false) + end + + it "is true for newly generated short lived certificates" do + expect(described_class.expiring?(short_lived_certificate)).to eq(false) + end + + it "is true for expired nodes" do + expect(described_class.expiring?(expired_certificate)).to eq(true) + end + + it "is true for nodes within the expiration window" do + expect(described_class.expiring?(expiring_certificate)).to eq(true) + end + end + + describe '#expiry' do + describe "with an expired cert" do + subject { described_class.expiry(expired_certificate) } + it "has a status of expired" do + expect(subject[:status]).to eq :expired + end + + it "includes the not after date" do + expect(subject[:expiration_date]).to eq expired_certificate.content.not_after + end + end + + describe "with an expiring cert" do + subject { described_class.expiry(expiring_certificate) } + + it "has a status of expiring" do + expect(subject[:status]).to eq :expiring + end + + it "includes the not after date" do + expect(subject[:expiration_date]).to eq expiring_certificate.content.not_after + end + + it "includes the time till expiration" do + expect(subject[:expires_in]).to match(/29 days, 23 hours, 59 minutes/) + end + end + + describe "with an ok cert" do + subject { described_class.expiry(ok_certificate) } + + it "has a status of ok" do + expect(subject[:status]).to eq :ok + end + + it "includes the not after date" do + expect(subject[:expiration_date]).to eq ok_certificate.content.not_after + end + + it "includes the time till expiration" do + expect(subject[:expires_in]).to match(/4 years, 364 days, 23 hours, 59 minutes/) + end + end + end +end diff --git a/code/environments/production/modules/certregen/spec/integration/puppet_x/crl_spec.rb b/code/environments/production/modules/certregen/spec/integration/puppet_x/crl_spec.rb new file mode 100644 index 0000000..3d50cfc --- /dev/null +++ b/code/environments/production/modules/certregen/spec/integration/puppet_x/crl_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' +require 'puppet_x/certregen/crl' + +RSpec.describe PuppetX::Certregen::CRL do + include_context "Initialize CA" + + describe '.refresh' do + def normalize_time(t) + t.utc.round + end + + let(:stub_time) { normalize_time(Time.now + 60 * 60 * 24 * 365) } + let(:oldcrl) { @oldcrl } + + before do + @oldcrl = Puppet::SSL::CertificateRevocationList.indirection.find("ca") + allow(Time).to receive(:now).and_return stub_time + described_class.refresh(Puppet::SSL::CertificateAuthority.new) + end + + subject { Puppet::SSL::CertificateRevocationList.indirection.find('ca') } + + it 'updates the lastUpdate field' do + last_update = normalize_time(subject.content.last_update.utc) + expect(last_update).to eq normalize_time(stub_time - 1) + end + + it 'updates the nextUpdate field' do + next_update = normalize_time(subject.content.next_update.utc) + expect(next_update).to eq normalize_time(stub_time + described_class::FIVE_YEARS) + end + + def crl_number(crl) + crl.content.extensions.find { |ext| ext.oid == 'crlNumber' }.value + end + + it "increments the CRL number" do + newcrl = Puppet::SSL::CertificateRevocationList.from_instance( + OpenSSL::X509::CRL.new(File.read(Puppet[:cacrl])), 'ca') + + old_crl_number = crl_number(oldcrl).to_i + new_crl_number = crl_number(newcrl).to_i + expect(new_crl_number).to eq old_crl_number + 1 + end + + it 'copies the cacrl to the hostcrl' do + cacrl = Puppet::SSL::CertificateRevocationList.from_instance( + OpenSSL::X509::CRL.new(File.read(Puppet[:cacrl])), 'ca') + hostcrl = Puppet::SSL::CertificateRevocationList.from_instance( + OpenSSL::X509::CRL.new(File.read(Puppet[:hostcrl])), 'ca') + expect(crl_number(cacrl)).to eq crl_number(hostcrl) + end + end +end diff --git a/code/environments/production/modules/certregen/spec/spec_helper.rb b/code/environments/production/modules/certregen/spec/spec_helper.rb new file mode 100644 index 0000000..9ae37b1 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/spec_helper.rb @@ -0,0 +1,16 @@ +#This file is generated by ModuleSync, do not edit. +require 'puppetlabs_spec_helper/module_spec_helper' + +if Puppet.version.to_f >= 4.5 + RSpec.configure do |c| + c.before :each do + Puppet.settings[:strict] = :error + end + end +end + +# put local configuration and setup into spec_helper_local +begin + require 'spec_helper_local' +rescue LoadError +end diff --git a/code/environments/production/modules/certregen/spec/spec_helper_acceptance.rb b/code/environments/production/modules/certregen/spec/spec_helper_acceptance.rb new file mode 100644 index 0000000..18c4fe4 --- /dev/null +++ b/code/environments/production/modules/certregen/spec/spec_helper_acceptance.rb @@ -0,0 +1,17 @@ +require 'beaker-rspec' +require 'beaker/puppet_install_helper' +require 'beaker/module_install_helper' +require 'acceptance/helpers' + +run_puppet_install_helper +install_ca_certs unless ENV['PUPPET_INSTALL_TYPE'] =~ /pe/i +hosts.each do |host| + install_module_on(host) +end +install_module_dependencies_on(hosts) + +RSpec.configure do |c| + # Readable test descriptions + c.formatter = :documentation +end + diff --git a/code/environments/production/modules/certregen/spec/spec_helper_local.rb b/code/environments/production/modules/certregen/spec/spec_helper_local.rb new file mode 100644 index 0000000..3dfb8aa --- /dev/null +++ b/code/environments/production/modules/certregen/spec/spec_helper_local.rb @@ -0,0 +1,52 @@ +RSpec.configure do |c| + c.include PuppetlabsSpec::Files + c.mock_with :rspec + + c.before(:each) do + # Suppress cert fingerprint logging + allow_any_instance_of(Puppet::SSL::CertificateAuthority).to receive(:puts) + + # remove the stub that causes puppet to believe it is + # always being run as root. + # See https://github.com/puppetlabs/puppetlabs_spec_helper/blob/master/lib/puppetlabs_spec_helper/module_spec_helper.rb#L29 + Puppet.features.unstub(:root?) + + Puppet[:vardir] = tmpdir('var') + Puppet[:confdir] = tmpdir('conf') + end + + def backdate_certificate(ca, cert, not_before, not_after) + cert.content.not_before = not_before + cert.content.not_after = not_after + signer = Puppet::SSL::CertificateSigner.new + signer.sign(cert.content, ca.host.key.content) + cert + end + + def make_certificate(name, not_before, not_after) + ca = Puppet::SSL::CertificateAuthority.new + cert = ca.generate(name) + backdate_certificate(ca, cert, not_before, not_after) + end +end + +RSpec.shared_context "Initialize CA" do + # PKI generation is done by initializing a CertificateAuthority object, which has the effect of + # applying the settings catalog, generating a RSA keypair, and generating a CA certificate. + # Since we're regenerating the CA state between each test we need to create a new + # CertificateAuthority object instead of using CertificateAuthority.instance, since that will + # memoize a single instance and will not generate the ca folder structure and PKI files. + def generate_pki + Puppet::SSL::CertificateAuthority.new + end + + before(:each) do + Puppet::SSL::Host.ca_location = :only + Puppet.settings.preferred_run_mode = "master" + + Puppet[:ca] = true + Puppet[:ca_name] = 'Puppet CA: foo' + + generate_pki + end +end |