Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ other attributes):

* Key headers, such as 'ssh-rsa' --- put these in the `type` attribute.

##### `marker`

Valid values: `cert-authority`, `revoked`

Optional marker for the `known_hosts` entry. Valid values are
`cert-authority` (the key is a CA that signs host certificates for the
listed patterns) and `revoked` (the key is explicitly denied). See the
`SSH_KNOWN_HOSTS FILE FORMAT` section of sshd(8).

##### `target`

The file in which to store the ssh key. Only used by
Expand Down
30 changes: 27 additions & 3 deletions lib/puppet/provider/sshkey/parsed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,39 @@

record_line :parsed, fields: ['name', 'type', 'key'],
post_parse: proc { |hash|
names = hash[:name].split(',', -1)
hash[:name] = names.shift
hash[:host_aliases] = names
# record_line takes a fixed field count; peel any leading @cert-authority/@revoked marker off by hand.
if hash[:name] && hash[:name].start_with?('@')
marker = hash[:name].delete_prefix('@')
key_field = hash[:key].is_a?(String) ? hash[:key] : ''
keytype, actual_key = key_field.split(%r{\s+}, 2)
if ['cert-authority', 'revoked'].include?(marker) && keytype && actual_key
hash[:marker] = marker.to_sym
hash[:name] = hash[:type]
hash[:type] = keytype
hash[:key] = actual_key
else
Puppet.warning(_('Ignoring malformed known_hosts entry starting with %{marker}') % ({ marker: hash[:name] }))
end
end

# marker isn't a record_line field, so ParsedFile#mk_resource_methods will fall back to the desired value unless we set it explicitly.
hash[:marker] ||= :absent

if hash[:name]
names = hash[:name].split(',', -1)
hash[:name] = names.shift
hash[:host_aliases] = names
end
},
pre_gen: proc { |hash|
if hash[:host_aliases]
hash[:name] = [hash[:name], hash[:host_aliases]].flatten.join(',')
hash.delete(:host_aliases)
end
if hash[:marker] && hash[:marker] != :absent
hash[:name] = "@#{hash[:marker]} #{hash[:name]}"
hash.delete(:marker)
end
}

# Make sure to use mode 644 if ssh_known_hosts is newly created
Expand Down
9 changes: 9 additions & 0 deletions lib/puppet/type/sshkey.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ def self.title_patterns
aliasvalue(:'ed25519-sk', :'sk-ssh-ed25519@openssh.com')
end

newproperty(:marker) do
desc "Optional marker for the `known_hosts` entry. Valid values are
`cert-authority` (the key is a CA that signs host certificates for the
listed patterns) and `revoked` (the key is explicitly denied). See the
`SSH_KNOWN_HOSTS FILE FORMAT` section of sshd(8)."

newvalues :'cert-authority', :revoked
end

newproperty(:key) do
desc "The key itself; generally a long string of unencoded characters. The `key`
attribute may not contain any whitespace, including embedded newlines.
Expand Down
120 changes: 120 additions & 0 deletions spec/acceptance/tests/resource/sshkey/marker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require 'spec_helper_acceptance'

RSpec.context 'sshkeys: Marker (cert-authority / revoked)' do
let(:keyname) { "pl#{rand(999_999).to_i}" }
let(:sample_key) { 'how_about_the_key_of_c' }
let(:ssh_known_hosts) { '/etc/ssh/ssh_known_hosts' }

before(:each) do
posix_agents.agents.each do |agent|
# The 'cp' might fail because the source file doesn't exist
on(
agent,
"cp -fv #{ssh_known_hosts} /tmp/ssh_known_hosts",
acceptable_exit_codes: [0, 1],
)
end
end

after(:each) do
posix_agents.each do |agent|
rc = on(
agent,
'[ -e /tmp/ssh_known_hosts ]',
accept_all_exit_codes: true,
)
if rc.exit_code == 0
on(
agent,
"mv -fv /tmp/ssh_known_hosts #{ssh_known_hosts}",
accept_all_exit_codes: true,
)
else
on(
agent,
"rm -fv #{ssh_known_hosts}",
accept_all_exit_codes: true,
)
end
end
end

posix_agents.each do |agent|
it "#{agent} writes a cert-authority marker to ssh_known_hosts" do
on agent, puppet('apply'), stdin: <<MANIFEST
sshkey { '#{keyname}':
ensure => 'present',
type => 'ssh-rsa',
key => '#{sample_key}',
marker => 'cert-authority',
}
MANIFEST

on(agent, "cat #{ssh_known_hosts}") do |res|
expect(res.stdout).to match(%r{^@cert-authority #{keyname} ssh-rsa #{sample_key}$})
end
end

it "#{agent} writes a revoked marker to ssh_known_hosts" do
on agent, puppet('apply'), stdin: <<MANIFEST
sshkey { '#{keyname}':
ensure => 'present',
type => 'ssh-rsa',
key => '#{sample_key}',
marker => 'revoked',
}
MANIFEST

on(agent, "cat #{ssh_known_hosts}") do |res|
expect(res.stdout).to match(%r{^@revoked #{keyname} ssh-rsa #{sample_key}$})
end
end

it "#{agent} is idempotent when the marker already matches" do
manifest = <<MANIFEST
sshkey { '#{keyname}':
ensure => 'present',
type => 'ssh-rsa',
key => '#{sample_key}',
marker => 'cert-authority',
}
MANIFEST

on(agent, puppet('apply'), stdin: manifest)
on(agent, puppet('apply', '--detailed-exitcodes'), stdin: manifest, acceptable_exit_codes: [0])
end

it "#{agent} adds a marker to an existing plain entry" do
on(agent, "echo '#{keyname} ssh-rsa #{sample_key}' >> #{ssh_known_hosts}")

on agent, puppet('apply'), stdin: <<MANIFEST
sshkey { '#{keyname}':
ensure => 'present',
type => 'ssh-rsa',
key => '#{sample_key}',
marker => 'cert-authority',
}
MANIFEST

on(agent, "cat #{ssh_known_hosts}") do |res|
expect(res.stdout).to match(%r{^@cert-authority #{keyname} ssh-rsa #{sample_key}$})
expect(res.stdout.scan(%r{#{keyname} ssh-rsa}).length).to eq(1)
end
end

it "#{agent} removes an entry with a marker when ensure => absent" do
on(agent, "echo '@revoked #{keyname} ssh-rsa #{sample_key}' >> #{ssh_known_hosts}")

on agent, puppet('apply'), stdin: <<MANIFEST
sshkey { '#{keyname}':
ensure => 'absent',
type => 'ssh-rsa',
}
MANIFEST

on(agent, "cat #{ssh_known_hosts}") do |res|
expect(res.stdout).not_to include(keyname.to_s)
end
end
end
end
2 changes: 2 additions & 0 deletions spec/fixtures/unit/provider/sshkey/parsed/sample
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ rh3b,192.168.0.56 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdS
centos1,192.168.0.57 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0DXqYF+3Lf68GkWBAjjKBb6UITNnzm4wiDi/AGjv5+DoVXqDcqHvZ8rZFAMgUe1dVob4pWT2ZWLHW0gicoJCdr4UQbPXlWz1F62z8fo2PRRPlG6KN1wmF7pnyml8jr0wBX8lQZJsMqi4InGozf7wFHLH/7DNGRK3MD6tSp3Z4is=
doorstop.cafes.net,205.241.238.186 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApJKeB9/bN5t55zLETHs0MVo/vEkfQ3EzY7178GKLI/yiOFmcn+NvUvUtCQK/xKpod813LBHCODxZPG1Kb0SjlaC/EkFEenb74LNu0o1qXa1GWh3wfaIm0JRNjXqPqxAWTlMs43O2HXwOwmLVhl7SSP3xtTw6h9gREbVKmfBaRdsRfzD0etfz1NCnmGh/1Sh9+j4eeS+IBtwoR5JVhZVhuofHCqs5HZ8gLDgfn8HXP7pMbLkx54cf1R/tmFmn9JGLdTPtEGcSIiu7414XSbfChSC83rGZCDPKHq7ZodiE8GpbWLBnyPXi2AYxTPM7aZMqitIHv3MWf5suV0q0WLGdnQ==
host.domain.com,host1.domain.com,192.168.0.1 dss thisismykey1
@cert-authority *.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM=
@revoked bad.example.com,alt.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleRevokedKeyForFixtureTestingOnly000000000
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ hosting2.planetargon.com,64.34.164.77 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t5

will.isawesome.com,192.168.0.5 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0=
will.isgreat.com,192.168.0.6 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBasd=

@cert-authority *.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM=
74 changes: 74 additions & 0 deletions spec/integration/provider/sshkey_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,78 @@
resource_app.main
end
end

describe 'marker functionality' do
let(:provider_class) { described_class }
let(:type) { Puppet::Type.type(:sshkey) }
let(:sample_key) { 'AAAAB3NzaC1yc2EAAAABIwAAAQEAzwHhxXvIrtfIwrudFqc8yQcIfMudrgpnuh1F3AV6d2BrLgu/yQE7W5UyJMUjfj427sQudRwKW45O0Jsnr33F4mUw+GIMlAAmp9g24/OcrTiB8ZUKIjoPy/cO4coxGi8/NECtRzpD/ZUPFh6OEpyOwJPMb7/EC2Az6Otw4StHdXUYw22zHazBcPFnv6zCgPx1hA7QlQDWTu4YcL0WmTYQCtMUb3FUqrcFtzGDD0ytosgwSd+JyN5vj5UwIABjnNOHPZ62EY1OFixnfqX/+dUwrFSs5tPgBF/KkC6R7tmbUfnBON6RrGEmu+ajOTOLy23qUZB4CQ53V7nyAWhzqSK+hw==' } # rubocop:disable Layout/LineLength

describe 'round-trip conversion' do
it 'parses and regenerates a cert-authority entry' do
line = "@cert-authority *.example.com ssh-rsa #{sample_key}"

parsed = provider_class.parse_line(line)
expect(parsed[:marker]).to eq(:'cert-authority')
expect(parsed[:name]).to eq('*.example.com')
expect(parsed[:type]).to eq('ssh-rsa')
expect(parsed[:key]).to eq(sample_key)

expect(provider_class.to_line(parsed)).to eq(line)
end

it 'parses and regenerates a cert-authority entry with host aliases' do
line = "@cert-authority *.example.com,*.test.com ssh-rsa #{sample_key}"

parsed = provider_class.parse_line(line)
expect(parsed[:marker]).to eq(:'cert-authority')
expect(parsed[:name]).to eq('*.example.com')
expect(parsed[:host_aliases]).to eq(['*.test.com'])
expect(parsed[:type]).to eq('ssh-rsa')

expect(provider_class.to_line(parsed)).to eq(line)
end

it 'parses and regenerates a revoked entry' do
line = "@revoked bad.example.com ssh-rsa #{sample_key}"

parsed = provider_class.parse_line(line)
expect(parsed[:marker]).to eq(:revoked)
expect(parsed[:name]).to eq('bad.example.com')
expect(parsed[:type]).to eq('ssh-rsa')

expect(provider_class.to_line(parsed)).to eq(line)
end

it 'emits a plain line when marker is :absent' do
record = {
record_type: :parsed,
name: '*.example.com',
type: 'ssh-rsa',
key: sample_key,
marker: :absent,
}
expect(provider_class.to_line(record)).to eq("*.example.com ssh-rsa #{sample_key}")
end
end

describe 'resource creation' do
it 'creates a cert-authority sshkey resource' do
expect {
type.new(name: '*.example.com', type: 'ssh-rsa', marker: 'cert-authority', key: sample_key)
}.not_to raise_error
end

it 'creates a revoked sshkey resource' do
expect {
type.new(name: 'bad.example.com', type: 'ssh-rsa', marker: 'revoked', key: sample_key)
}.not_to raise_error
end

it 'still resolves keytype aliases when a marker is set' do
resource = type.new(name: '*.example.com', type: :rsa, marker: :'cert-authority', key: sample_key)
expect(resource[:type]).to eq(:'ssh-rsa')
expect(resource[:marker]).to eq(:'cert-authority')
end
end
end
end
39 changes: 39 additions & 0 deletions spec/unit/provider/sshkey/parsed_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@ def key
expect(subject.parse_line('test ssh-rsa ' + key)[:host_aliases]).to eq([])
end

it 'parses cert-authority entries into a marker with a clean type' do
result = subject.parse_line('@cert-authority *.example.com ssh-rsa ' + key)
expect(result[:marker]).to eq(:'cert-authority')
expect(result[:name]).to eq('*.example.com')
expect(result[:type]).to eq('ssh-rsa')
expect(result[:key]).to eq(key)
end

it 'parses cert-authority entries with host aliases' do
result = subject.parse_line('@cert-authority *.example.com,*.test.com ssh-rsa ' + key)
expect(result[:marker]).to eq(:'cert-authority')
expect(result[:name]).to eq('*.example.com')
expect(result[:host_aliases]).to eq(['*.test.com'])
expect(result[:type]).to eq('ssh-rsa')
end

it 'parses revoked entries' do
result = subject.parse_line('@revoked *.example.com ssh-rsa ' + key)
expect(result[:marker]).to eq(:revoked)
expect(result[:name]).to eq('*.example.com')
expect(result[:type]).to eq('ssh-rsa')
expect(result[:key]).to eq(key)
end

it 'defaults marker to :absent on plain entries so Puppet detects marker transitions' do
expect(subject.parse_line('test ssh-rsa ' + key)[:marker]).to eq(:absent)
end

it 'warns on a malformed marker line and does not set a marker' do
expect(Puppet).to receive(:warning).with(%r{malformed known_hosts entry}i)
result = subject.parse_line('@cert-authority *.example.com')
expect(result[:marker]).to eq(:absent)
end

it 'warns on an unknown @-prefixed marker' do
expect(Puppet).to receive(:warning).with(%r{malformed known_hosts entry}i)
subject.parse_line('@bogus *.example.com ssh-rsa ' + key)
end

context 'with the sample file' do
['sample', 'sample_with_blank_lines'].each do |sample_file|
let(:fixture) { my_fixture(sample_file) }
Expand Down
20 changes: 19 additions & 1 deletion spec/unit/type/sshkey_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
end
end

[:host_aliases, :ensure, :key].each do |property|
[:host_aliases, :ensure, :key, :marker].each do |property|
it "has a #{property} property" do
expect(described_class.attrtype(property)).to eq :property
end
Expand Down Expand Up @@ -56,6 +56,24 @@
expect(key.parameter(:type).value).to eq :'sk-ssh-ed25519@openssh.com'
end

[:'cert-authority', :revoked].each do |mark|
it "accepts marker => #{mark}" do
key = described_class.new(name: 'foo', type: :'ssh-rsa', marker: mark, key: 'AAA')
expect(key[:marker]).to eq(mark)
end
end

it 'rejects an unknown marker value' do
expect {
described_class.new(name: 'foo', type: :'ssh-rsa', marker: :bogus, key: 'AAA')
}.to raise_error(Puppet::Error, %r{Invalid value.*bogus})
end

it 'leaves marker unset by default' do
key = described_class.new(name: 'foo', type: :'ssh-rsa', key: 'AAA')
expect(key[:marker]).to be_nil
end

it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa for type" do
expect {
described_class.new(name: 'whev', type: :'ssh-dsa')
Expand Down
Loading