class Hiera::Backend::Vault_backend

Public Class Methods

new() click to toggle source
# File lib/hiera/backend/vault_backend.rb, line 6
def initialize()
  require 'json'
  require 'vault'
  Hiera.debug("Hiera VAULT backend starting")

  @config = Config[:vault]
  @config[:mounts] ||= {}
  @config[:mounts][:generic] ||= ['secret']
  @config[:default_field_parse] ||= 'string' # valid values: 'string', 'json'

  # :override_behavior:
  # Valid values: 'normal', 'flag'
  # Default: 'normal'
  # If set to 'flag' a read from vault will only be done if the override parameter
  # is a hash, and it contains the 'flag', it will behave like this:
  # - when the value of the 'flag' key is 'vault', it will look in vault
  # - when the value is 'vault_only', it will return the default or raise an exception
  #   if the lookup key is not found in vault
  # If the 'flag' key does not exist, or if the override parameter is not a hash,
  # nil will be returned to signal that the next backend should be searched.
  # If the hash contains the 'override' key, its value will be used as the actual
  # override.
  # To support the 'flag' behavior, the `hiera_vault`, `hiera_vault_array`, and
  # `hiera_vault_hash` functions need to be used, since they will make sure the
  # override parameter is checked and changed where needed
  # Additionally, when 'vault_only' is used, it will only work properly using the
  # special hiera_vault* functions
  #
  # The 'flag_default' setting can be used to set the default for the 'flag' element
  # to 'vault_only'. This is handled by the hiera_vault* parser functions.
  #
  @config[:override_behavior] ||= 'normal'
  if not ['normal','flag'].include?(@config[:override_behavior])
    raise Exception, "[hiera-vault] invalid value for :override_behavior: '#{@config[:override_behavior]}', should be one of 'normal','flag'"
  end

  @config[:flag_default] ||= 'vault_first'
  if not ['vault_first','vault_only'].include?(@config[:flag_default])
    raise Exception, "hiera_vault: invalid value '#{@config[:flag_default]}' for :flag_default in hiera config, one of 'vault_first', 'vault_only' expected"
  end

  @config[:default_field_parse] ||= 'string' # valid values: 'string', 'json'
  if not ['string','json'].include?(@config[:default_field_parse])
    raise Exception, "[hiera-vault] invalid value for :default_field_parse: '#{@config[:default_field_behavior]}', should be one of 'string','json'"
  end

  # :default_field_behavior:
  #   'ignore' => ignore additional fields, if the field is not present return nil
  #   'only'   => only return value of default_field when it is present and the only field, otherwise return hash as normal
  @config[:default_field_behavior] ||= 'ignore'
  if not ['ignore','only'].include?(@config[:default_field_behavior])
    raise Exception, "[hiera-vault] invalid value for :default_field_behavior: '#{@config[:default_field_behavior]}', should be one of 'ignore','only'"
  end

  vault_connect
end

Public Instance Methods

generate(password_size) click to toggle source
# File lib/hiera/backend/vault_backend.rb, line 238
def generate(password_size)
  pass = ""
  (1..password_size).each do
    pass += (("a".."z").to_a+("A".."Z").to_a+("0".."9").to_a)[rand(62).to_int]
  end

  pass
end
lookup(key, scope, order_override, resolution_type) click to toggle source
# File lib/hiera/backend/vault_backend.rb, line 63
def lookup(key, scope, order_override, resolution_type)
  begin
    vault_connect

    read_vault = false
    genpw = false
    otp = nil

    if @config[:override_behavior] == 'flag'
      if order_override.kind_of? Hash
        if order_override.has_key?('flag')
          if ['vault_default','vault_first','vault_only'].include?(order_override['flag'])
            read_vault = true
            if order_override['flag'] == 'vault_default'
              # since variables are passed by reference, the caller will know afterwards, which flag was actually used
              order_override['flag'] = @config[:flag_default]
            end
            if order_override.has_key?('generate')
              pwlen = order_override['generate'].to_i
              if pwlen > 8 # TODO: make configurable
                genpw = true
              end
            end
            if order_override.has_key?('vault_otp')
              otp = order_override['vault_otp']
            end
            if order_override.has_key?('resolution_type')
              resolution_type = order_override['resolution_type']
            end
            # this one must be last, because order_override gets a new value!:
            if order_override.has_key?('override')
              order_override = order_override['override']
            else
              order_override = nil
            end
          else
            raise Exception, "[hiera-vault] Invalid value '#{order_override['flag']}' for 'flag' element in override parameter, expected one of ['vault_default', 'vault_first', 'vault_only'], while override_behavior is 'flag'"
          end
          if @vault.nil?
            raise Exception, "[hiera-vault] Cannot skip, because vault is unavailable and vault must be read, while override_behavior is 'flag'"
          end
        else
          Hiera.debug("[hiera-vault] Not reading from vault, because 'flag' element does not exist in override parameter, while override_behavior is 'flag'")
        end
      else
        Hiera.debug("[hiera-vault] Not reading from vault, because override parameter is not a hash, while override_behavior is 'flag'")
      end
    else
      # normal behavior
      return nil if @vault.nil?
      read_vault = true
    end

    answer = nil

    if read_vault
      Hiera.debug("[hiera-vault] Looking up #{key} in vault backend")

      found = false

      # Only generic mounts supported so far
      @config[:mounts][:generic].each do |mount|
        path = Backend.parse_string(mount, scope, { 'key' => key })
        Backend.datasources(scope, order_override) do |source|
          Hiera.debug("Looking in path #{path}/#{source}/")
          new_answer = lookup_generic("#{path}/#{source}/#{key}", scope)
          #Hiera.debug("[hiera-vault] Answer: #{new_answer}:#{new_answer.class}")
          next if new_answer.nil?
          case resolution_type
          when :array
            raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
            answer ||= []
            answer << new_answer
          when :hash
            raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
            answer ||= {}
            answer = Backend.merge_answer(new_answer,answer)
          else
            answer = new_answer
            found = true
            break
          end
        end

        break if found
      end
    end

    if answer.nil? and @config[:default_field] and genpw
      new_answer = generate(pwlen)

      @config[:mounts][:generic].each do |mount|
        path = Backend.parse_string(mount, scope, { 'key' => key })
        Backend.datasources(scope, order_override) do |source|
          # Storing the generated secret in the override path or the highest path in the hierarchy
          # make sure to use a proper override or an appropriate hierarchy if the secret is to be used
          # on different nodes, otherwise the same key might be written with a different value at different
          # paths
          Hiera.debug("Storing generated secret in vault at path #{path}/#{source}/#{key}")
          answer = new_answer if store("#{path}/#{source}/#{key}", { @config[:default_field].to_sym => new_answer })
          break
        end
        break
      end
    end
    if answer.nil? and not otp.nil?
      answer = otp
    end
    return answer
  rescue Exception => e
    raise Exception, "#{e.message} in #{e.backtrace[0]}"
  end
end
lookup_generic(key, scope) click to toggle source
# File lib/hiera/backend/vault_backend.rb, line 208
def lookup_generic(key, scope)
    begin
      secret = @vault.logical.read(key)
    rescue Exception => e
      raise Exception, "[hiera-vault] Could not read secret #{key}, #{e.class}: #{e.errors.join("\n").rstrip}"
    end

    return nil if secret.nil?

    Hiera.debug("[hiera-vault] Read secret: #{key}")
    if @config[:default_field] and (@config[:default_field_behavior] == 'ignore' or (secret.data.has_key?(@config[:default_field].to_sym) and secret.data.length == 1))
      return nil if not secret.data.has_key?(@config[:default_field].to_sym)
      # Return just our default_field
      data = secret.data[@config[:default_field].to_sym]
      if @config[:default_field_parse] == 'json'
        begin
          data = JSON.parse(data)
        rescue JSON::ParserError
          Hiera.debug("[hiera-vault] Could not parse string as JSON")
        end
      end
    else
      # Turn secret's hash keys into strings
      data = secret.data.inject({}) { |h, (k, v)| h[k.to_s] = v; h }
    end
    #Hiera.debug("[hiera-vault] Data: #{data}:#{data.class}")

    return Backend.parse_answer(data, scope)
end
store(key, secret_hash) click to toggle source
# File lib/hiera/backend/vault_backend.rb, line 247
def store(key, secret_hash)
    begin
      write_result = @vault.logical.write(key, secret_hash)
    rescue Vault::HTTPConnectionError
      Hiera.debug("[hiera-vault] Could not connect to write secret: #{key}")
    rescue Vault::HTTPError => e
      Hiera.warn("[hiera-vault] Could not write secret #{key}: #{e.errors.join("\n").rstrip}")
    end

    if write_result == true
      Hiera.debug("[hiera-vault] Successfully written secret: #{key}")
      return true
    else
      Hiera.warn("[hiera-vault] Could not write secret #{key}: #{write_result}")
      return false
    end
end
vault_connect() click to toggle source
# File lib/hiera/backend/vault_backend.rb, line 177
def vault_connect
  if @vault.nil?
    begin
      @vault = Vault::Client.new
      @vault.configure do |config|
        config.address = @config[:addr] if @config[:addr]
        config.token = @config[:token] if @config[:token]
        config.ssl_pem_file = @config[:ssl_pem_file] if @config[:ssl_pem_file]
        config.ssl_verify = @config[:ssl_verify] if @config[:ssl_verify]
        config.ssl_ca_cert = @config[:ssl_ca_cert] if config.respond_to? :ssl_ca_cert
        config.ssl_ca_path = @config[:ssl_ca_path] if config.respond_to? :ssl_ca_path
        config.ssl_ciphers = @config[:ssl_ciphers] if config.respond_to? :ssl_ciphers
      end

      fail if @vault.sys.seal_status.sealed?
      Hiera.debug("[hiera-vault] Client configured to connect to #{@vault.address}")
    rescue Exception => e
      @vault = nil
      Hiera.warn("[hiera-vault] Skipping backend. Configuration error: #{e}")
    end
  end
  if @vault
    begin
      fail if @vault.sys.seal_status.sealed?
    rescue Exception => e
      @vault = nil
      Hiera.warn("[hiera-vault] Vault is unavailable or configuration error: #{e}")
    end
  end
end