Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5b353b3
Allow file content that looks like a checksum.
Aug 21, 2025
742edb3
Correct Rubocop offense about nested ifs
Aug 21, 2025
4d671f4
Update configuration reference for use_checksum_in_file_content
Aug 21, 2025
a9ca544
Add spec test for new setting use_checksum_in_file_content
Aug 25, 2025
de06ed3
Merge branch 'main' into filecontentchecksumbug
jeremie-pierson Sep 8, 2025
e28dbd3
Merge branch 'main' into filecontentchecksumbug
jeremie-pierson Sep 17, 2025
bd8d56e
Merge branch 'main' into filecontentchecksumbug
jeremie-pierson Sep 30, 2025
cdca923
Merge branch 'main' into filecontentchecksumbug
jeremie-pierson Nov 17, 2025
d983288
Remove deprecated checksum-in-file-content functionality
jeremie-pierson Nov 17, 2025
012415d
Remove now obsolete tests on checksums-in-file-content
jeremie-pierson Nov 17, 2025
6a12f6b
Merge branch 'main' into filecontentchecksumbug
jeremie-pierson Nov 26, 2025
968efc8
Merge branch 'main' into filecontentchecksumbug
jeremie-pierson Mar 3, 2026
8639e0f
Merge branch 'OpenVoxProject:main' into filecontentchecksumbug
jeremie-pierson Mar 6, 2026
92621e6
style: systemd unit branding
d1nuc0m Jan 26, 2026
a0c14e8
Merge pull request #300 from d1nuc0m/systemd-unit-branding
corporate-gadfly Mar 10, 2026
551a4b5
Restore the legend of help help help help help
nmburgan Mar 10, 2026
379b5fd
Merge pull request #364 from OpenVoxProject/restore_help
nmburgan Mar 11, 2026
2654372
Add a renew subcommand to puppet ssl
jay7x Mar 8, 2026
2665666
Merge pull request #363 from OpenVoxProject/puppet_ssl_renew
bastelfreak Mar 19, 2026
8a2d027
Use usermod(8) on OpenBSD to unbreak password management
klemensn Jan 2, 2026
b3d1b7f
Merge pull request #294 from klemensn/user-password-openbsd
corporate-gadfly Mar 21, 2026
03e85d5
Allow file content that looks like a checksum.
Aug 21, 2025
c6c40d1
Correct Rubocop offense about nested ifs
Aug 21, 2025
a73dfdc
Update configuration reference for use_checksum_in_file_content
Aug 21, 2025
399816a
Add spec test for new setting use_checksum_in_file_content
Aug 25, 2025
16bbe9a
Remove deprecated checksum-in-file-content functionality
jeremie-pierson Nov 17, 2025
09ede3c
Remove now obsolete tests on checksums-in-file-content
jeremie-pierson Nov 17, 2025
cb04bf3
Merge branch 'filecontentchecksumbug' of github.com:jeremie-pierson/o…
jeremie-pierson Mar 25, 2026
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
2 changes: 1 addition & 1 deletion ext/systemd/puppet.service
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# then running systemctl show puppet | grep LimitNOFILE
#
[Unit]
Description=Puppet agent
Description=Puppet agent daemon provided by OpenVox
Documentation=man:puppet-agent(8)
Wants=basic.target
After=basic.target network.target network-online.target
Expand Down
65 changes: 65 additions & 0 deletions lib/puppet/application/ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ def help
* --target CERTNAME
Clean the specified device certificate instead of this host's certificate.

* --if-expiring-in DURATION
When renewing a certificate only renew if the certificate is valid for
less than this amount of time. Duration can be specified as a time
interval, such as 30s, 5m, 1h.

ACTIONS
-------

Expand Down Expand Up @@ -71,6 +76,11 @@ def help
for subsequent requests. If there is already an existing certificate, it
will be overwritten.

* renew_cert
Renew an existing and non-expired client certificate. When
`--if-expiring-in` option is specified, then renew the certificate only
if it's going to expire in the amount of time given.

* verify:
Verify the private key and certificate are present and match, verify the
certificate is issued by a trusted CA, and check revocation status.
Expand Down Expand Up @@ -98,6 +108,28 @@ def help
option('--localca')
option('--verbose', '-v')
option('--debug', '-d')
option('--if-expiring-in DURATION') do |arg|
options[:expiring_in_sec] = parse_duration(arg)
end

def parse_duration(value)
unit_map = {
"y" => 365 * 24 * 60 * 60,
"d" => 24 * 60 * 60,
"h" => 60 * 60,
"m" => 60,
"s" => 1
}
format = /^(\d+)(y|d|h|m|s)?$/

v = (value.is_a?(Integer) ? "#{value}s" : value)

if v =~ format
Regexp.last_match(1).to_i * unit_map[::Regexp.last_match(2) || 's']
else
raise ArgumentError, "Invalid duration format: #{value}"
end
end

def initialize(command_line = Puppet::Util::CommandLine.new)
super(command_line)
Expand Down Expand Up @@ -148,6 +180,8 @@ def main
unless cert
raise Puppet::Error, _("The certificate for '%{name}' has not yet been signed") % { name: certname }
end
when 'renew_cert'
renew_cert(certname, options[:expiring_in_sec])
when 'generate_request'
generate_request(certname)
when 'verify'
Expand Down Expand Up @@ -248,6 +282,37 @@ def download_cert(ssl_context)
raise Puppet::Error.new(_("Failed to download certificate: %{message}") % { message: e.message }, e)
end

def renew_cert(certname, expiring_in_sec_maybe)
ssl_context = @ssl_provider.load_context(certname: certname)

if expiring_in_sec_maybe && (ssl_context[:client_cert].not_after - Time.now) > expiring_in_sec_maybe
Puppet.info _("Certificate '%{name}' is still valid until %{date}") % { name: certname, date: ssl_context[:client_cert].not_after }
return ssl_context[:client_cert]
end

Puppet.debug _("Renewing certificate '%{name}'") % { name: certname }
route = create_route(ssl_context)
_, x509 = route.post_certificate_renewal(ssl_context)
cert = OpenSSL::X509::Certificate.new(x509)
Puppet.notice _("Downloaded certificate '%{name}' with fingerprint %{fingerprint}") % { name: certname, fingerprint: fingerprint(cert) }

# verify client cert before saving
@ssl_provider.create_context(
cacerts: ssl_context.cacerts, crls: ssl_context.crls, private_key: ssl_context.private_key, client_cert: cert
)
@cert_provider.save_client_cert(certname, cert)
@cert_provider.delete_request(certname)
cert
rescue Puppet::HTTP::ResponseError => e
if e.response.code == 404
nil
else
raise Puppet::Error.new(_("Failed to download certificate: %{message}") % { message: e.message }, e)
end
rescue => e
raise Puppet::Error.new(_("Failed to download certificate: %{message}") % { message: e.message }, e)
end

def verify(certname)
password = @cert_provider.load_private_key_password
ssl_context = @ssl_provider.load_context(certname: certname, password: password)
Expand Down
18 changes: 18 additions & 0 deletions lib/puppet/face/help.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@
end

if args.length > 2
# rubocop:disable all
if args.select { |x| x == 'help' }.length > 2 then
c = "\n %'(),-./=ADEFHILORSTUXY\\_`gnv|".split('')
i = <<-'EOT'.gsub(/\s*/, '').to_i(36)
3he6737w1aghshs6nwrivl8mz5mu9nywg9tbtlt081uv6fq5kvxse1td3tj1wvccmte806nb
cy6de2ogw0fqjymbfwi6a304vd56vlq71atwmqsvz3gpu0hj42200otlycweufh0hylu79t3
gmrijm6pgn26ic575qkexyuoncbujv0vcscgzh5us2swklsp5cqnuanlrbnget7rt3956kam
j8adhdrzqqt9bor0cv2fqgkloref0ygk3dekiwfj1zxrt13moyhn217yy6w4shwyywik7w0l
xtuevmh0m7xp6eoswin70khm5nrggkui6z8vdjnrgdqeojq40fya5qexk97g4d8qgw0hvokr
pli1biaz503grqf2ycy0ppkhz1hwhl6ifbpet7xd6jjepq4oe0ofl575lxdzjeg25217zyl4
nokn6tj5pq7gcdsjre75rqylydh7iia7s3yrko4f5ud9v8hdtqhu60stcitirvfj6zphppmx
7wfm7i9641d00bhs44n6vh6qvx39pg3urifgr6ihx3e0j1ychzypunyou7iplevitkyg6gbg
wm08oy1rvogcjakkqc1f7y1awdfvlb4ego8wrtgu9vzw4vmj59utwifn2ejcs569dh1oaavi
sc581n7jjg1dugzdu094fdobtx6rsvk3sfctvqnr36xctold
EOT
353.times{i,x=i.divmod(1184);a,b=x.divmod(37);print(c[a]*b)}
end
# rubocop:enable all
# TRANSLATORS 'puppet help' is a command line and should not be translated
raise ArgumentError, _("The 'puppet help' command takes two (optional) arguments: a subcommand and an action")
end
Expand Down
15 changes: 15 additions & 0 deletions lib/puppet/provider/user/openbsd.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,19 @@ def modifycmd(param, value)
end
cmd
end

def password=(value)
user = @resource.name
begin
cmd = [command(:modify), '-p', value, user]
execute_options = {
:failonfail => true,
:combine => true,
:sensitive => has_sensitive_data?
}
execute(cmd, execute_options)
rescue => detail
raise Puppet::Error, "Could not set password on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace
end
end
end
34 changes: 13 additions & 21 deletions lib/puppet/type/file/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,19 @@
if value == :absent
value
elsif value.is_a?(String) && checksum?(value)
# XXX This is potentially dangerous because it means users can't write a file whose
# entire contents are a plain checksum unless it is a Binary content.
Puppet.puppet_deprecation_warning([
# TRANSLATORS "content" is an attribute and should not be translated
_('Using a checksum in a file\'s "content" property is deprecated.'),
# TRANSLATORS "filebucket" is a resource type and should not be translated. The quoted occurrence of "content" is an attribute and should not be translated.
_('The ability to use a checksum to retrieve content from the filebucket using the "content" property will be removed in a future release.'),
# TRANSLATORS "content" is an attribute and should not be translated.
_('The literal value of the "content" property will be written to the file.'),
# TRANSLATORS "static catalogs" should not be translated.
_('The checksum retrieval functionality is being replaced by the use of static catalogs.'),
_('See https://puppet.com/docs/puppet/latest/static_catalogs.html for more information.')
].join(" "),
:file => @resource.file,
:line => @resource.line) if !@actual_content && !resource.parameter(:source)
value
# Our argument looks like a checksum. Is it the value of the content
# attribute in Puppet code, that happens to look like a checksum, or is
# it an actual checksum computed on the actual content?
if @actual_content || resource.parameter(:source)
# Actual content is already set, value contains it's checksum
value
else
# The content only happens to look like a checksum by chance.
@actual_content = value.is_a?(Puppet::Pops::Types::PBinaryType::Binary) ? value.binary_buffer : value
resource.parameter(:checksum).sum(@actual_content)
end
Comment on lines +51 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be removed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried without it but got obscure errors. I think the problem was that the checksum mechanism is not only used for the functionality we're trying to remove, but also for other purposes in the file handling code. Probably has to do with filebuckets.

It seems like "value" can legitimately contain a checksum if this attribute ("content") was already set. I'm not sure about this, but as I understand it, since we are in a "munge" block, we're transforming the value of the attribute prior to storing it. In the standard case, when user sets the "content" attribute to something in Puppet code, we store the actual content in @actual_content and attribute value holds the checksum.

As I recall, when I first removed theses lines, I had loads of errors in filebucket-related code elsewhere because, presumably, other code assumes that file content attribute value is in fact a checksum...

If you think I'm mistaken, I'll try to delete those lines again and see what CI says this time.

else
# Our argument is definitely not a checksum: set actual_value and return calculated checksum.
@actual_content = value.is_a?(Puppet::Pops::Types::PBinaryType::Binary) ? value.binary_buffer : value
resource.parameter(:checksum).sum(@actual_content)
end
Expand Down Expand Up @@ -155,17 +151,13 @@
def each_chunk_from
if actual_content.is_a?(String)
yield actual_content
elsif content_is_really_a_checksum? && actual_content.nil?
elsif actual_content.nil?
yield read_file_from_filebucket
Comment on lines +154 to 155
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be related to above discussion (or I could very well be mistaken about all this :-) ).

elsif actual_content.nil?

Check failure on line 156 in lib/puppet/type/file/content.rb

View workflow job for this annotation

GitHub Actions / AIO Package Rake Checks

Lint/DuplicateElsifCondition: Duplicate `elsif` condition detected.

Check failure on line 156 in lib/puppet/type/file/content.rb

View workflow job for this annotation

GitHub Actions / rubocop

Lint/DuplicateElsifCondition: Duplicate `elsif` condition detected.
yield ''
end
end

def content_is_really_a_checksum?
checksum?(should)
end

def read_file_from_filebucket
dipper = resource.bucket
raise "Could not get filebucket from file" unless dipper
Expand Down
14 changes: 0 additions & 14 deletions spec/integration/type/file_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -640,20 +640,6 @@ def get_aces_for_path_by_sid(path, sid)
it_should_behave_like "files are backed up", {} do
let(:filebucket_digest) { method(:digest) }
end

it "should give a checksum deprecation warning" do
expect(Puppet).to receive(:puppet_deprecation_warning).with('Using a checksum in a file\'s "content" property is deprecated. The ability to use a checksum to retrieve content from the filebucket using the "content" property will be removed in a future release. The literal value of the "content" property will be written to the file. The checksum retrieval functionality is being replaced by the use of static catalogs. See https://puppet.com/docs/puppet/latest/static_catalogs.html for more information.', {:file => 'my/file.pp', :line => 5})
d = digest("this is some content")
catalog.add_resource described_class.new(:path => path, :content => "{#{digest_algorithm}}#{d}")
catalog.apply
end

it "should not give a checksum deprecation warning when no content is specified while checksum and checksum value are used" do
expect(Puppet).not_to receive(:puppet_deprecation_warning)
d = digest("this is some content")
catalog.add_resource described_class.new(:path => path, :checksum => digest_algorithm, :checksum_value => d)
catalog.apply
end
end

CHECKSUM_TYPES_TO_TRY.each do |checksum_type, checksum|
Expand Down
62 changes: 62 additions & 0 deletions spec/unit/application/ssl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,68 @@ def expects_command_to_fail(message)
end
end

context 'when renewing a certificate' do
let(:renewed) do
# Create a new cert with the same private key ("renew" an existing certificate)
@ca.create_cert('ssl-client', @ca.ca_cert, @ca.key, reuse_key: @host[:private_key])
end

before do
ssl.command_line.args << 'renew_cert'
# This command requires an existing certificate
File.write(Puppet[:hostcert], @host[:cert].to_pem)
end

it 'renews a new cert' do
stub_request(:post, %r{puppet-ca/v1/certificate_renewal}).to_return(status: 200, body: renewed[:cert].to_pem)

expects_command_to_pass(%r{Downloaded certificate '#{name}' with fingerprint .*})

expect(File.read(Puppet[:hostcert])).to eq(renewed[:cert].to_pem)
end

context 'with --if-expiring-in=100y specified' do
before do
ssl.command_line.args << '--if-expiring-in' << '100y'
ssl.parse_options
end

it 'renews a cert' do
stub_request(:post, %r{puppet-ca/v1/certificate_renewal}).to_return(status: 200, body: renewed[:cert].to_pem)

expects_command_to_pass(%r{Downloaded certificate '#{name}' with fingerprint .*})

expect(File.read(Puppet[:hostcert])).to eq(renewed[:cert].to_pem)
end
end

context 'with --if-expiring-in=0 specified' do
before do
ssl.command_line.args << '--if-expiring-in' << '0y'
ssl.parse_options
end

it 'does not renew a cert' do
expects_command_to_pass(%r{Certificate '#{name}' is still valid until .*})

expect(File.read(Puppet[:hostcert])).to eq(@host[:cert].to_pem)
end
end

it "reports an error if the downloaded cert's public key doesn't match our private key" do
# generate a new host key, whose public key doesn't match the cert
private_key = OpenSSL::PKey::RSA.new(512)
File.write(Puppet[:hostprivkey], private_key.to_pem)
File.write(Puppet[:hostpubkey], private_key.public_key.to_pem)

stub_request(:post, %r{puppet-ca/v1/certificate_renewal}).to_return(status: 200, body: renewed[:cert].to_pem)

expects_command_to_fail(
%r{^Failed to download certificate: The certificate for 'CN=ssl-client' does not match its private key}
)
end
end

context 'when verifying' do
before do
ssl.command_line.args << 'verify'
Expand Down
Loading