puppetlabs/puppet · error · ArgumentError

SSL context must contain a client certificate.

Error message

SSL context must contain a client certificate.

What it means

Raised by Puppet::HTTP::Service::Ca#post_certificate_renewal when the ssl_context passed to the call has no client certificate (ssl_context.client_cert is nil). Certificate renewal is the CA operation where an already-certified agent asks for a new certificate, so the request must authenticate with the existing client cert. Note the check runs after the POST is issued, so a request has already gone on the wire by the time the ArgumentError surfaces.

Source

Thrown at lib/puppet/http/service/ca.rb:127

  # Submit a POST request to send a certificate renewal request to the server
  #
  # @param [Puppet::SSL::SSLContext] ssl_context
  #
  # @return [Array<Puppet::HTTP::Response, String>] The request response
  #
  # @api public
  def post_certificate_renewal(ssl_context)
    headers = add_puppet_headers(HEADERS)
    headers['Content-Type'] = 'text/plain'

    response = @client.post(
      with_base_url('/certificate_renewal'),
      '', # Puppet::HTTP::Client.post requires a body, the API endpoint does not
      headers: headers,
      options: { ssl_context: ssl_context }
    )

    raise ArgumentError, _('SSL context must contain a client certificate.') unless ssl_context.client_cert

    process_response(response)

    [response, response.body.to_s]
  end
end

View on GitHub (pinned to e227c27540)

Solutions

  1. Load the context for the full agent chain from the SSL directory (Puppet::SSL::Provider#load_context) so client_cert is populated, and only call renewal on nodes that already hold a signed cert
  2. If the node has no certificate yet, switch to CSR submission (put_certificate_request) instead of renewal
  3. When building a context manually, pass client_cert: and private_key: to create_context
  4. Verify cert.pem and private_key.pem exist under the agent's ssl directory before invoking the CA service

Example fix

# before (ruby)
ssl_context = provider.create_context(cacerts: [ca_cert], crls: [crl])
client.post_certificate_renewal(ssl_context) # => ArgumentError

# after
ssl_context = provider.create_context(
  cacerts: [ca_cert], crls: [crl],
  client_cert: agent_cert, private_key: agent_key
)
client.post_certificate_renewal(ssl_context)
Defensive patterns

Strategy: validation

Validate before calling

# ruby
raise ArgumentError, 'ssl_context has no client certificate' if ssl_context.client_cert.nil?
client.post_certificate_renewal(ssl_context)

Type guard

def has_client_cert?(ctx)
  ctx.respond_to?(:client_cert) && !ctx.client_cert.nil?
end

Try / catch

begin
  client.post_certificate_renewal(ssl_context)
rescue ArgumentError => e
  raise unless e.message.include?('client certificate')
  client.put_certificate_request(certname, csr)  # fall back to enrollment
end

Prevention

When it happens

Trigger: Calling client.post_certificate_renewal(ssl_context) with a context built without a client cert — e.g. Puppet::SSL::Provider#create_context called with only :cacerts/:crls and no :client_cert/:private_key, or a CA-chain-only context instead of one for the full agent chain.

Common situations: Running certificate renewal on a node that never obtained a signed certificate (renewal used where submit_certificate_request is the right enrollment path); custom tooling that hand-builds an SSLContext and forgets the client cert pair; the agent's cert files were cleaned (puppet certificate clean / ssl dir wiped) but renewal logic still runs.

Understand the failure class

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/d5822111f0ec2471. Report an issue: GitHub.