mirror of
https://github.com/inspec/inspec
synced 2024-11-10 07:04:15 +00:00
CHEF-3962: Adds ability to verify ssh keys (#6656)
* ssh_key resource file Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * Initial commit for unit test for ssh_key resource Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * Fix linting and added resource in resources file Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * extend the ssh key resource to use file resource properties Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * Updates the ssh_key resource to get length and type of key. Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * Updates unit test for ssh_key resource. Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * ADDS Docs for ssh_key resource Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * Fix Review: Empty file handling Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * Fixed review comments and few code refactoring for ssh_key resource Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> * Doc edits Signed-off-by: Ian Maddaus <ian.maddaus@progress.com> * Updates test Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> --------- Signed-off-by: Vasu1105 <vasundhara.jagdale@progress.com> Signed-off-by: Ian Maddaus <ian.maddaus@progress.com> Co-authored-by: Ian Maddaus <ian.maddaus@progress.com>
This commit is contained in:
parent
8af5834774
commit
b946f5454d
5 changed files with 277 additions and 0 deletions
114
docs-chef-io/content/inspec/resources/ssh_key.md
Normal file
114
docs-chef-io/content/inspec/resources/ssh_key.md
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
+++
|
||||||
|
title = "ssh_key resource"
|
||||||
|
draft = false
|
||||||
|
gh_repo = "inspec"
|
||||||
|
platform = "os"
|
||||||
|
|
||||||
|
[menu]
|
||||||
|
[menu.inspec]
|
||||||
|
title = "ssh_key"
|
||||||
|
identifier = "inspec/resources/os/ssh_key.md ssh_key resource"
|
||||||
|
parent = "inspec/resources/os"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Use the `ssh_key` Chef InSpec audit resource to test ssh keys. Supported key types RSA, DSA(Limited support. Not verifies private key) , ECDSA, Ed25519
|
||||||
|
|
||||||
|
## Availability
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
{{% inspec/inspec_installation %}}
|
||||||
|
|
||||||
|
### Version
|
||||||
|
|
||||||
|
This resource first became available in v1.18.0 of Chef InSpec.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
The `ssh_key` resource tests the properties of an SSH key file. Provide the path to a key file or a key filename. If you pass in a filename, this resource checks for keys on default path `~/.ssh/`.
|
||||||
|
|
||||||
|
```rb
|
||||||
|
describe ssh_key('~/.ssh/id_rsa') do
|
||||||
|
it { should be_private }
|
||||||
|
it { should be_public }
|
||||||
|
its('type') { should cmp /rsa/ }
|
||||||
|
its('key_length') { should eq 2048 }
|
||||||
|
its('mode') { should cmp '0400' }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use an optional passphrase with `ssh_key`:
|
||||||
|
|
||||||
|
```rb
|
||||||
|
describe ssh_key('~/.ssh/id_rsa', '<PASSPHRASE>') do
|
||||||
|
it { should be_private }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<PASSPHRASE>` with the private key passphrase.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### key_length
|
||||||
|
|
||||||
|
The `key_length` property tests the number of bits in the key pair. This only works with RSA keys.
|
||||||
|
|
||||||
|
```rb
|
||||||
|
describe ssh_key('~/.ssh/id_rsa') do
|
||||||
|
its('key_length') { should eq 4096 }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### type
|
||||||
|
|
||||||
|
The `type` property verifies the key type.
|
||||||
|
|
||||||
|
```rb
|
||||||
|
describe ssh_key('~/.ssh/id_ecdsa') do
|
||||||
|
its('type') { should cmp /ecdsa/ }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed values:
|
||||||
|
|
||||||
|
- `rsa`
|
||||||
|
- `ecdsa`
|
||||||
|
- `ed25519`
|
||||||
|
- `dsa`
|
||||||
|
|
||||||
|
### file properties
|
||||||
|
|
||||||
|
The ssh_key resource also tests the same properties that the [file resource](/inspec/resources/file#properties) tests.
|
||||||
|
For example, you can use the `mode` property to test if the mode assigned to the SSH key matches the specified value.
|
||||||
|
|
||||||
|
```rb
|
||||||
|
describe ssh_key('~/.ssh/id_rsa') do
|
||||||
|
its('mode') { should cmp '0400' }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Matchers
|
||||||
|
|
||||||
|
For a full list of available matchers, see the [matchers page](/inspec/matchers/).
|
||||||
|
|
||||||
|
### be_public
|
||||||
|
|
||||||
|
Use `be_public` to verify that a key is public key:
|
||||||
|
|
||||||
|
```rb
|
||||||
|
describe ssh_key('~/.ssh/id_ed25519.pub') do
|
||||||
|
it { should be_public }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### be_private
|
||||||
|
|
||||||
|
Use `be_private` to verify that a key is a private key:
|
||||||
|
|
||||||
|
```rb
|
||||||
|
describe ssh_key('~/.ssh/id_ecdsa', '<PASSPHRASE>') do
|
||||||
|
it { should be_private }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<PASSPHRASE>` with the private key passphrase.
|
|
@ -110,6 +110,7 @@ require "inspec/resources/selinux"
|
||||||
require "inspec/resources/service"
|
require "inspec/resources/service"
|
||||||
require "inspec/resources/shadow"
|
require "inspec/resources/shadow"
|
||||||
require "inspec/resources/ssh_config"
|
require "inspec/resources/ssh_config"
|
||||||
|
require "inspec/resources/ssh_key"
|
||||||
require "inspec/resources/ssl"
|
require "inspec/resources/ssl"
|
||||||
require "inspec/resources/sys_info"
|
require "inspec/resources/sys_info"
|
||||||
require "inspec/resources/toml"
|
require "inspec/resources/toml"
|
||||||
|
|
124
lib/inspec/resources/ssh_key.rb
Normal file
124
lib/inspec/resources/ssh_key.rb
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
require "inspec/utils/file_reader"
|
||||||
|
require "net/ssh" unless defined?(Net::SSH)
|
||||||
|
|
||||||
|
# Change module if required
|
||||||
|
module Inspec::Resources
|
||||||
|
class SshKey < FileResource
|
||||||
|
# Every resource requires an internal name.
|
||||||
|
name "ssh_key"
|
||||||
|
|
||||||
|
# Restrict to only run on the below platforms (if none were given,
|
||||||
|
# all OS's and cloud API's supported)
|
||||||
|
supports platform: "unix"
|
||||||
|
supports platform: "windows"
|
||||||
|
|
||||||
|
desc "public/private SSH key pair test"
|
||||||
|
|
||||||
|
example <<~EXAMPLE
|
||||||
|
describe ssh_key('path: ~/.ssh/id_rsa') do
|
||||||
|
its('key_length') { should eq 4096 }
|
||||||
|
its('type') { should cmp /rsa/ }
|
||||||
|
it { should be_private }
|
||||||
|
end
|
||||||
|
EXAMPLE
|
||||||
|
|
||||||
|
include FileReader
|
||||||
|
|
||||||
|
def initialize(keypath, passphrase = nil)
|
||||||
|
skip_resource "The `ssh_key` resource is not yet available on your OS." unless inspec.os.unix? || inspec.os.windows?
|
||||||
|
@key_path = set_ssh_key_path(keypath)
|
||||||
|
@passphrase = passphrase
|
||||||
|
@key = read_ssh_key
|
||||||
|
super(@key_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def public?
|
||||||
|
return if @key.nil?
|
||||||
|
|
||||||
|
@key[:public]
|
||||||
|
end
|
||||||
|
|
||||||
|
def private?
|
||||||
|
return if @key.nil?
|
||||||
|
|
||||||
|
@key[:private]
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_length
|
||||||
|
return if @key.nil?
|
||||||
|
|
||||||
|
@key[:key_length]
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
return if @key.nil?
|
||||||
|
|
||||||
|
@key[:type]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Define a resource ID. This is used in reporting engines to uniquely identify the individual resource.
|
||||||
|
# This might be a file path, or a process ID, or a cloud instance ID. Only meaningful to the implementation.
|
||||||
|
# Must be a string. Defaults to the empty string if not implemented.
|
||||||
|
def resource_id
|
||||||
|
@key_path || "SSH key"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"ssh_key #{@key_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_ssh_key_path(keypath)
|
||||||
|
if File.exist?(keypath)
|
||||||
|
@key_path = keypath
|
||||||
|
elsif File.exist?(File.join("#{Dir.home}/.ssh/", keypath))
|
||||||
|
@key_path = File.join("#{Dir.home}/.ssh/", keypath)
|
||||||
|
else
|
||||||
|
raise Inspec::Exceptions::ResourceSkipped, "Can't find file: #{keypath}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_ssh_key
|
||||||
|
key_data = {}
|
||||||
|
key = nil
|
||||||
|
filecontent = read_file_content((@key_path), @passphrase)
|
||||||
|
raise Inspec::Exceptions::ResourceSkipped, "File is empty: #{@key_path}" if filecontent.split("\n").empty?
|
||||||
|
|
||||||
|
if filecontent.split("\n")[0].include?("PRIVATE")
|
||||||
|
# Net::SSH::KeyFactory does not have support to load private key for DSA
|
||||||
|
key = Net::SSH::KeyFactory.load_private_key(@key_path, @passphrase, false)
|
||||||
|
unless key.nil?
|
||||||
|
key_data[:private] = true
|
||||||
|
key_data[:public] = false
|
||||||
|
# The data send for ssh type is not in same format so it's good to match on the string
|
||||||
|
key_data[:type] = key.ssh_type
|
||||||
|
key_data[:key_length] = key_lengh(key)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
key = Net::SSH::KeyFactory.load_public_key(@key_path)
|
||||||
|
unless key.nil?
|
||||||
|
key_data[:private] = false
|
||||||
|
key_data[:public] = true
|
||||||
|
# The data send for ssh type is not in same format so it's good to match on the string
|
||||||
|
key_data[:type] = key.ssh_type
|
||||||
|
key_data[:key_length] = key_lengh(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
key_data
|
||||||
|
rescue OpenSSL::PKey::PKeyError => e
|
||||||
|
raise Inspec::Exceptions::ResourceFailed, "#{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_lengh(key)
|
||||||
|
if key.class.to_s == "OpenSSL::PKey::RSA"
|
||||||
|
key.public_key.n.num_bits
|
||||||
|
else
|
||||||
|
# Unable to get the key lenght data for other types of keys
|
||||||
|
# TODO: Need to check if there is any method that will get this info.
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -151,6 +151,7 @@ class MockLoader
|
||||||
"test_certificate.rsa.crt.pem" => mockfile.call("test_certificate.rsa.crt.pem"),
|
"test_certificate.rsa.crt.pem" => mockfile.call("test_certificate.rsa.crt.pem"),
|
||||||
"test_certificate.rsa.key.pem" => mockfile.call("test_certificate.rsa.key.pem"),
|
"test_certificate.rsa.key.pem" => mockfile.call("test_certificate.rsa.key.pem"),
|
||||||
"test_ca_public.key.pem" => mockfile.call("test_ca_public.key.pem"),
|
"test_ca_public.key.pem" => mockfile.call("test_ca_public.key.pem"),
|
||||||
|
"test/fixtures/files/test_rsa_key" => mockfile.call("test_rsa_key"),
|
||||||
# Test DH parameters, 2048 bit long safe prime, generator 2 for dh_params in PEM format
|
# Test DH parameters, 2048 bit long safe prime, generator 2 for dh_params in PEM format
|
||||||
"dh_params.dh_pem" => mockfile.call("dh_params.dh_pem"),
|
"dh_params.dh_pem" => mockfile.call("dh_params.dh_pem"),
|
||||||
"default.toml" => mockfile.call("default.toml"),
|
"default.toml" => mockfile.call("default.toml"),
|
||||||
|
|
37
test/unit/resources/ssh_key_test.rb
Normal file
37
test/unit/resources/ssh_key_test.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
require "inspec/globals"
|
||||||
|
require "#{Inspec.src_root}/test/helper"
|
||||||
|
require_relative "../../../lib/inspec/resources/ssh_key"
|
||||||
|
require "mixlib/shellout"
|
||||||
|
|
||||||
|
class TestSshKeyResource < Minitest::Test
|
||||||
|
def setup
|
||||||
|
# Generate an SSH RSA key for testing
|
||||||
|
@private_key_path = generate_ssh_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
# Clean up: remove the generated ssh keys
|
||||||
|
FileUtils.rm_rf(@private_key_path)
|
||||||
|
FileUtils.rm_rf("#{@private_key_path}.pub")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ssh_key_resoure
|
||||||
|
@ssh_key = MockLoader.new("ubuntu".to_sym).load_resource("ssh_key", @private_key_path)
|
||||||
|
assert_match("rsa", @ssh_key.type)
|
||||||
|
assert_equal(4096, @ssh_key.key_length)
|
||||||
|
assert_equal(true, @ssh_key.private?)
|
||||||
|
assert_equal(false, @ssh_key.public?)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_ssh_key
|
||||||
|
file_path = "test/fixtures/files/test_rsa_key"
|
||||||
|
cmd = Mixlib::ShellOut.new("yes | ssh-keygen -t rsa -b 4096 -N '' -f #{file_path}")
|
||||||
|
cmd.run_command
|
||||||
|
cmd.error!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
file_path
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue