Class: OpenC3::Secrets

Inherits:
Object show all
Defined in:
lib/openc3/utilities/secrets.rb

Direct Known Subclasses

RedisSecrets

Constant Summary collapse

DEFAULT_SECRET_FILE_DIR =

Default base directory that FILE type secret paths must reside under. FILE type secrets are written by the operator (or mounted by Kubernetes) and then read back by the microservice, so the path is a destination we control, not an arbitrary file on the host.

'/tmp'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSecrets

Returns a new instance of Secrets.



24
25
26
# File 'lib/openc3/utilities/secrets.rb', line 24

def initialize
  @local_secrets = {}
end

Class Method Details

.getClientObject



67
68
69
70
71
72
# File 'lib/openc3/utilities/secrets.rb', line 67

def self.getClient
  raise 'OPENC3_SECRET_BACKEND environment variable is required' unless ENV['OPENC3_SECRET_BACKEND']
  secrets_class = ENV.fetch('OPENC3_SECRET_BACKEND').capitalize + 'Secrets'
  klass = OpenC3.require_class('openc3/utilities/' + secrets_class.class_name_to_filename)
  klass.new
end

.path_contained?(base, path) ⇒ Boolean

Returns true if path is inside base, or is base itself.

Returns:

  • (Boolean)

    true if path is inside base, or is base itself



63
64
65
# File 'lib/openc3/utilities/secrets.rb', line 63

def self.path_contained?(base, path)
  path == base or path.start_with?(base.end_with?(File::SEPARATOR) ? base : base + File::SEPARATOR)
end

.secret_file_dirObject

Base directory that FILE type secret paths must reside under



29
30
31
32
33
# File 'lib/openc3/utilities/secrets.rb', line 29

def self.secret_file_dir
  dir = ENV.fetch('OPENC3_SECRET_FILE_DIR', nil)
  dir = DEFAULT_SECRET_FILE_DIR if dir.nil? or dir.empty?
  dir
end

.validate_file_path(path) ⇒ String

Validates a FILE type secret path. The path must expand to a location strictly inside Secrets.secret_file_dir. This prevents a plugin configuration such as 'SECRET FILE KEY /root/.ssh/id_rsa' or 'SECRET FILE KEY /tmp/../etc/shadow' from reading or overwriting arbitrary files as the OpenC3 process user.

Parameters:

  • path (String)

    Path from a SECRET FILE definition

Returns:

  • (String)

    The validated absolute path

Raises:

  • (ArgumentError)


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/openc3/utilities/secrets.rb', line 43

def self.validate_file_path(path)
  raise ArgumentError, "Secret file path must be a String but is a #{path.class}" unless path.is_a?(String)
  raise ArgumentError, "Secret file path must not be blank" if path.strip.empty?
  raise ArgumentError, "Secret file path must not contain a null byte" if path.include?("\x00")

  # expand_path collapses '..' and '.', expands '~', and makes a relative path
  # absolute, so the comparison below can't be walked out of. This is
  # deliberately a purely lexical check: it does not touch the filesystem, so
  # a path validates identically at plugin install time (cmd-tlm-api) and at
  # write time (openc3-operator), which are separate containers. Symlinks are
  # rejected by the operator when it writes the secret.
  base = File.expand_path(secret_file_dir)
  absolute = File.expand_path(path)
  unless absolute.start_with?(base + File::SEPARATOR)
    raise ArgumentError, "Secret file path '#{path}' must be under '#{base}'"
  end
  absolute
end

Instance Method Details

#delete(key, secret_store: nil, scope:) ⇒ Object

Raises:

  • (NotImplementedError)


86
87
88
# File 'lib/openc3/utilities/secrets.rb', line 86

def delete(key, secret_store: nil, scope:)
  raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end

#get(key, secret_store: nil, scope:) ⇒ Object



78
79
80
# File 'lib/openc3/utilities/secrets.rb', line 78

def get(key, secret_store: nil, scope:)
  return @local_secrets[key]
end

#keys(secret_store: nil, scope:) ⇒ Object

Raises:

  • (NotImplementedError)


74
75
76
# File 'lib/openc3/utilities/secrets.rb', line 74

def keys(secret_store: nil, scope:)
  raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end

#set(key, value, secret_store: nil, scope:) ⇒ Object

Raises:

  • (NotImplementedError)


82
83
84
# File 'lib/openc3/utilities/secrets.rb', line 82

def set(key, value, secret_store: nil, scope:)
  raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end

#setup(secrets) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/openc3/utilities/secrets.rb', line 90

def setup(secrets)
  secrets.each do |secret|
    if secret.length < 3
      raise ArgumentError, "Secret must have at least 3 items (type, key, data), got #{secret.length}"
    end
    type, key, data, _secret_store = secret
    case type
    when 'ENV'
      @local_secrets[key] = ENV.fetch(data, nil)
    when 'FILE'
      @local_secrets[key] = File.read(Secrets.validate_file_path(data))
    else
      raise "Unknown secret type: #{type}"
    end
  end
end