shadow resource
Use the shadow
Chef InSpec audit resource to test the contents of /etc/shadow
, which contains password details that are readable only by the root
user. shadow
is a plural resource. Like all plural resources, it functions by performing searches across multiple entries in the shadow file.
The format for /etc/shadow
includes:
- A username
- The hashed password for that user
- The last date a password was changed, as the number of days since Jan 1, 1970
- The minimum number of days a password must exist before it may be changed
- The maximum number of days after which a password must be changed
- The number of days a user is warned about an expiring password
- The number of days a user must be inactive before the user account is disabled
- The date on which a user account was disabled, as the number of days since Jan 1, 1970
These entries are defined as a colon-delimited row in the file, one row per user:
username:Gb7crrO5CDF.:10063:0:99999:7:::
The shadow
resource understands this format, allows you to search on the fields, and exposes the selected users’ properties.
Availability
Install
This resource is distributed with Chef InSpec and is automatically available for use.Version
This resource first became available in v1.0.0 of InSpec.
Resource Parameters
The shadow
resource takes one optional parameter: the path to the shadow file. If omitted, /etc/shadow
is assumed.
# Expect a file to exist at the default location and have 32 users
describe shadow do
its('count') { should eq 32 }
end
# Use a custom location
describe shadow('/etc/my-custom-place/shadow') do
its('count') { should eq 32 }
end
Examples
A shadow
resource block uses where
to filter entries from the shadow file. If where
is omitted, all entries are selected.
# Select all users. Among them, there should not be a user with the name 'forbidden_user'.
describe shadow do
its('users') { should_not include 'forbidden_user' }
end
# Ensure there is only one user named 'root' (Select all with name 'root', then count them).
describe shadow.where(user: 'root') do
its('count') { should eq 1 }
end
Use where
to match any of the supported filter criteria. where
has a method form for simple equality and a block form for more complex queries.
# Method form, simple
# Select just the root user (direct equality)
describe shadow.where(user: 'root') do
its ('count') { should eq 1 }
end
# Method form, with a regex
# Select all users whose names begin with smb
describe shadow.where(user: /^smb/) do
its ('count') { should eq 2 }
end
# Block form
# Select users whose passwords have expired
describe shadow.where { expiry_date > 0 } do
# This test directly asserts that there should be 0 such users
its('count') { should eq 0 }
# But if the count test fails, this test outputs the users that are causing the failure.
its('users') { should be_empty }
end
Use where
with expect syntax to show all users (that aren’t disabled or locked) without SHA512 hashed passwords.
# Users with password fields that are not *, !, or don't begin with $6$
bad_users = inspec.shadow.where { password !~ /^[*!]$|^\$6\$.*/ }.users
describe 'Password hashes in /etc/shadow' do
it 'should only contain SHA512 hashes' do
message = "Users without SHA512 hashes: #{bad_users.join(', ')}"
expect(bad_users).to be_empty, message
end
end
Properties
As a plural resource, all of shadow
’s properties return lists (that is, Ruby Arrays). include
and be_empty
are two useful matchers when working with lists. You can also perform manipulation of the lists, such as calling uniq
, sort
, count
, first
, last
, min
, and max
.
users
A list of strings, representing the usernames matched by the filter.
describe shadow
its('users') { should include 'root' }
end
passwords
A list of strings, representing the encrypted password strings for entries matched by the where
filter. Each string may not be an encrypted password, but rather a *
or similar which indicates that direct logins are not allowed. Different operating systems use different flags here (such as *LK*
to indicate the account is locked).
# Use uniq to remove duplicates, then determine
# if the only password left on the list is '*'
describe shadow.where(user: /adm$/) do
its('passwords.uniq.first') { should cmp '*' }
its('passwords.uniq.count') { should eq 1 }
end
last_changes
A list of integers, indicating the number of days since Jan 1, 1970 since the password for each matching entry was changed.
# Ensure all entries have changed their password in the last 90 days. (Probably want a filter on that)
describe shadow do
its('last_changes.min') { should be < Date.today - 90 - Date.new(1970,1,1) }
end
min_days
A list of integers reflecting the minimum number of days a password must exist, before it may be changed, for the users that matched the filter.
# min_days seems crazy today; make sure it is zero for everyone
describe shadow do
its('min_days.uniq') { should eq [0] }
end
max_days
A list of integers reflecting the maximum number of days after which the password must be changed for each user matching the filter.
# Make sure there is no policy allowing longer than 90 days
describe shadow do
its('max_days.max') { should be < 90 }
end
warn_days
A list of integers reflecting the number of days a user is warned about an expiring password for each user matching the filter.
# Ensure everyone gets the same 7-day policy
describe shadow do
its('warn_days.uniq.count') { should eq 1 }
its('warn_days.uniq.first') { should eq 7 }
end
inactive_days
A list of integers reflecting the number of days a user must be inactive before the user account is disabled for each user matching the filter.
# Ensure everyone except admins has an stale policy of no more than 14 days
describe shadow.where { user !~ /adm$/ } do
its('inactive_days.max') { should be <= 14 }
end
expiry_dates
A list of integers reflecting the number of days since Jan 1, 1970 that a user account has been disabled, for each user matching the filter. Value is nil
if the account has not expired.
# No one should have an expired account.
describe shadow do
its('expiry_dates.compact') { should be_empty }
end
count
The count
property tests the number of records that the filter matched.
# Should probably only have one root user
describe shadow.user('root') do
its('count') { should eq 1 }
end
Filter Criteria
You may use any of these filter criteria with the where
function. They are named after the columns in the shadow file. Each has a related list property.
user
The string username of a user. Always present. Not required to be unique.
# Expect all users whose name ends in adm to have a disabled password via the '*' flag
describe shadow.where(user: /adm$/) do
its('password.uniq') { should eq ['*'] }
end
password
The encrypted password strings, or an account status string. Each string may not be an encrypted password, but rather a *
or similar which indicates that direct logins are not allowed. Different operating systems use other flags here (such as *LK*
to indicate the account is locked).
# Find 'locked' accounts and ensure 'nobody' is on the list
describe shadow.where(password: '*LK*') do
its('users') { should include 'nobody' }
end
last_change
An integer reflecting the number of days since Jan 1, 1970 since the user’s password was changed.
# Find users who have not changed their password within 90 days
describe shadow.where { last_change > Date.today - 90 - Date.new(1970,1,1) } do
its('users') { should be_empty }
end
min_days
An integer reflecting the minimum number of days a user is required to wait before changing their password again.
# Find users who have a nonzero wait time
describe shadow.where { min_days > 0 } do
its('users') { should be_empty }
end
max_days
An integer reflecting the maximum number of days a user may go without changing their password.
# All users should have a 30-day policy
describe shadow.where { max_days != 30 } do
its('users') { should be_empty }
end
warn_days
An integer reflecting the number of days before a password expiration that a user receives an alert.
# All users should have a 7-day warning policy
describe shadow.where { warn_days != 7 } do
its('users') { should be_empty }
end
inactive_days
An integer reflecting the number of days that must pass before a user who has not logged in will be disabled.
# Ensure everyone has a stale policy of no more than 14 days.
describe shadow.where { inactive_days.nil? || inactive_days > 14 } do
its('users') { should be_empty }
end
expiry_date
An integer reflecting the number of days since Jan 1, 1970 on which the user was disabled. The expiry_date
criterion is nil
for enabled users.
# Ensure no one is disabled due to a old password
describe shadow.where { !expiry_date.nil? } do
its('users') { should be_empty }
end
# Ensure no one is disabled for more than 14 days
describe shadow.where { !expiry_date.nil? && expiry_date - Date.new(1970,1,1) > 14} do
its('users') { should be_empty }
end
Matchers
This resource has no resource-specific matchers.
For a full list of available matchers, please visit our Universal Matchers page.