mirror of
https://github.com/inspec/inspec
synced 2024-11-21 20:23:06 +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/shadow"
|
||||
require "inspec/resources/ssh_config"
|
||||
require "inspec/resources/ssh_key"
|
||||
require "inspec/resources/ssl"
|
||||
require "inspec/resources/sys_info"
|
||||
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.key.pem" => mockfile.call("test_certificate.rsa.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
|
||||
"dh_params.dh_pem" => mockfile.call("dh_params.dh_pem"),
|
||||
"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