class Krane::EjsonSecretProvisioner

Constants

EJSON_KEYS_SECRET
EJSON_SECRETS_FILE
EJSON_SECRET_KEY

Public Class Methods

new(task_config:, ejson_keys_secret:, ejson_file:, statsd_tags:, selector: nil) click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 21
def initialize(task_config:, ejson_keys_secret:, ejson_file:, statsd_tags:, selector: nil)
  @ejson_keys_secret = ejson_keys_secret
  @ejson_file = ejson_file
  @statsd_tags = statsd_tags
  @selector = selector
  @task_config = task_config
  @kubectl = Kubectl.new(
    task_config: @task_config,
    log_failure_by_default: false,
    output_is_sensitive_default: true # output may contain ejson secrets
  )
end

Public Instance Methods

resources() click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 34
def resources
  @resources ||= build_secrets
end

Private Instance Methods

build_secrets() click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 40
def build_secrets
  unless @ejson_keys_secret
    raise EjsonSecretError, "Secret #{EJSON_KEYS_SECRET} not provided, cannot decrypt secrets"
  end
  return [] unless File.exist?(@ejson_file)
  with_decrypted_ejson do |decrypted|
    secrets = decrypted[EJSON_SECRET_KEY]
    unless secrets.present?
      logger.warn("#{EJSON_SECRETS_FILE} does not have key #{EJSON_SECRET_KEY}."\
        "No secrets will be created.")
      return []
    end

    secrets.map do |secret_name, secret_spec|
      validate_secret_spec(secret_name, secret_spec)
      resource = generate_secret_resource(secret_name, secret_spec["_type"], secret_spec["data"])
      resource.validate_definition(kubectl: @kubectl)
      if resource.validation_failed?
        raise EjsonSecretError, "Resulting resource Secret/#{secret_name} failed validation"
      end
      resource
    end
  end
end
decrypt_ejson(key_dir) click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 135
def decrypt_ejson(key_dir)
  out, err, st = Open3.capture3({ 'EJSON_KEYDIR' => key_dir.to_s }, 'ejson', 'decrypt', @ejson_file.to_s)
  unless st.success?
    # older ejson versions dump some errors to STDOUT
    msg = err.presence || out
    raise EjsonSecretError, msg
  end
  JSON.parse(out)
rescue JSON::ParserError
  raise EjsonSecretError, "Failed to parse decrypted ejson"
end
encrypted_ejson() click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 65
def encrypted_ejson
  @encrypted_ejson ||= load_ejson_from_file
end
fetch_private_key_from_secret() click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 147
def fetch_private_key_from_secret
  encoded_private_key = @ejson_keys_secret["data"][public_key]
  unless encoded_private_key
    raise EjsonSecretError, "Private key for #{public_key} not found in #{EJSON_KEYS_SECRET} secret"
  end

  Base64.decode64(encoded_private_key)
rescue Kubectl::ResourceNotFoundError
  raise EjsonSecretError, "Secret/#{EJSON_KEYS_SECRET} is required to decrypt EJSON and could not be found"
end
generate_secret_resource(secret_name, secret_type, data) click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 87
def generate_secret_resource(secret_name, secret_type, data)
  unless data.is_a?(Hash) && data.values.all? { |v| v.is_a?(String) } # Secret data is map[string]string
    raise EjsonSecretError, "Data for secret #{secret_name} was invalid. Only key-value pairs are permitted."
  end
  encoded_data = data.each_with_object({}) do |(key, value), encoded|
    # Leading underscores in ejson keys are used to skip encryption of the associated value
    # To support this ejson feature, we need to exclude these leading underscores from the secret's keys
    secret_key = key.sub(/\A_/, '')
    encoded[secret_key] = Base64.strict_encode64(value)
  end

  labels = { "name" => secret_name }
  labels.reverse_merge!(@selector.to_h) if @selector

  secret = {
    'kind' => 'Secret',
    'apiVersion' => 'v1',
    'type' => secret_type,
    'metadata' => {
      "name" => secret_name,
      "labels" => labels,
      "namespace" => namespace,
    },
    "data" => encoded_data,
  }

  Krane::Secret.build(
    namespace: namespace, context: context, logger: logger, definition: secret, statsd_tags: @statsd_tags,
  )
end
load_ejson_from_file() click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 118
def load_ejson_from_file
  return {} unless File.exist?(@ejson_file)
  JSON.parse(File.read(@ejson_file))
rescue JSON::ParserError => e
  raise EjsonSecretError, "Failed to parse encrypted ejson:\n  #{e}"
end
private_key() click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 73
def private_key
  @private_key ||= fetch_private_key_from_secret
end
public_key() click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 69
def public_key
  encrypted_ejson["_public_key"]
end
validate_secret_spec(secret_name, spec) click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 77
def validate_secret_spec(secret_name, spec)
  errors = []
  errors << "secret type unspecified" if spec["_type"].blank?
  errors << "no data provided" if spec["data"].blank?

  unless errors.empty?
    raise EjsonSecretError, "Ejson incomplete for secret #{secret_name}: #{errors.join(', ')}"
  end
end
with_decrypted_ejson() { |decrypted| ... } click to toggle source
# File lib/krane/ejson_secret_provisioner.rb, line 125
def with_decrypted_ejson
  return unless File.exist?(@ejson_file)

  Dir.mktmpdir("ejson_keydir") do |key_dir|
    File.write(File.join(key_dir, public_key), private_key)
    decrypted = decrypt_ejson(key_dir)
    yield decrypted
  end
end