Skip to content

[Feature request]: Handling of Sensitive data when Deferred is hard to do correctly #189

@dabelenda

Description

@dabelenda

Use Case

I am in an environment where the secrets management is handled strictly. The Agents use their own credentials to access a Hashicorp Vault directly (using Deferred calls to https://github.com/voxpupuli/puppet-vault_lookup) and the secrets must not be present in reports or logs of the puppet agent.

In this setup, I get an issue when presented with a case as follows:

A class expects a Sensitive[String[1]] as parameter for a certificate private key and passes this parameter to a file ressource.
The profile gets the secret from vault using a Deferred function call ... since puppet-vault_lookup returns a Sensitive[Hash], I made a ruby wrapper for PuppetX::VaultLookup::Lookup.lookup to return Hash[String[1], Sensitive[String[1]]] and then use a Deferred('get', ... to obtain the content of the hash.

During typechecking this works correctly. However, it seems that the content of the file resource ignores the Sensitive type of the parameter and only looks at the Deferred it actually receives. This means that the diff does show in the logs of the agent and in the report.

The puppet documentation tells to wrap the Deferred with Sensitive but in this case I have a type checking error:

Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Evaluation Error: Error while evaluating a Resource Statement, Class[SomeClass]: parameter 'private_key' expects a Sensitive[String] value, got Sensitive[Object[{name => 'Deferred', attributes => {'name' => Pattern[/\A[$]?[a-z][a-z0-9_]*(?:::[a-z][a-z0-9_]*)*\z/], 'arguments' => {type => Optional[Array], value => undef}}}]]

For completeness, here is the wrapper function I created:

# frozen_string_literal: true
require 'puppet'
require 'puppet_x/vault_lookup/lookup'

# @summary Get secret from Hashicorp Vault, cast the result to a Hash[String[1], Sensitive[String[1]]], where the keys are the fields requested.
Puppet::Functions.create_function(:'base::get_vault_secret', Puppet::Functions::InternalFunction) do
  # @param my_data The String to evaluate
  # @return Hash[String[1],Sensitive[String[1]]]
  dispatch :get_vault_secret do
    cache_param
    param 'String[1]', :vault_path
    param 'Array[String]', :fields
    optional_param 'String', :role
    optional_param 'Optional[String]', :auth_mount_path
    optional_param 'String', :vault_addr
    return_type 'Hash[String[1],Sensitive[String[1]]]'
  end

  def get_vault_secret(cache,
                       vault_path,
                       fields,
                       role = 'agent',
                       auth_mount_path = '/v1/auth/puppet',
                       vault_addr = 'https://vault.example.com')

    temp = PuppetX::VaultLookup::Lookup.lookup(cache: cache,
                                       path: vault_path,
                                       vault_addr: vault_addr,
                                       cert_path_segment: auth_mount_path,
                                       cert_role: role,
                                       namespace: nil,
                                       field: nil,
                                       auth_method: 'cert',
                                       role_id: role,
                                       secret_id: nil,
                                       approle_path_segment: nil,
                                       agent_sink_file: nil).unwrap

    fields.each_with_object({}) do |elem, memo|
      raise ArgumentError, _("Vault Secret at path '%{path}' does not contain field '%{field}'") % {field: elem, path: vault_path} unless temp.key?(elem)
      memo[elem] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(temp[elem])
    end
  end
end

And a quick coding example of what I describe:

class someclass(
  Sensitive[String[1]] $private_key,
)  {
  file { '/etc/ssl/private.key':
    content => $private_key,
  }
}

class profile {
  $vault_result = Deferred(
                            'base::get_vault_secret',
                            [
                              'secret/vault/path/for/private/key',
                              ['private_key']
                            ],
                          )

  $private_key = Deferred('get', [$vault_result, 'private_key'])
  class { 'someclass':
    private_key => $private_key,
  }
}

Describe the solution you would like

I would prefer if the Sensitive type was correctly handled by all native resource types even if wrapped by a Deferred, this would simplify the understanding of how Sensitive actually work and meet the expectation of what the type checking actually agrees with.

Describe alternatives you've considered

My guess is that I should wrap the variable usage in the file resource content parameter... but this is conceptually weird since the typechecking agrees this is already a Sensitive. This would also mean every module requesting a Sensitive[String] to wrap the parameter into a Sensitive.

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions