Class: LittleGhost::Network::EnvoyGateway

Inherits:
Gateway
  • Object
show all
Defined in:
lib/little_ghost/network/envoy_gateway.rb

Overview

Manages Envoy as a native process or pinned Docker sidecar for one Sandbox. CONNECT policy sees destinations, not encrypted request details. Optional HTTP inspection changes the child trust configuration and may not work for clients with certificate pinning or custom trust stores. The Sandbox must block direct sockets for either mode to be an enforcement boundary.

Constant Summary collapse

ENVOY_IMAGE =

:nodoc:

"envoyproxy/envoy:v1.39.0@sha256:d59f7f5fa10cff6d5892b6c5e7df5c9297ddfb2c3683e33fbfb82da24de4fa66"
RUNTIMES =

:nodoc:

%i[auto native docker].freeze
TRANSPORTS =

:nodoc:

%i[unix docker].freeze

Instance Attribute Summary collapse

Attributes inherited from Gateway

#policy

Instance Method Summary collapse

Methods inherited from Gateway

#validate!

Constructor Details

#initialize(policy:, runtime: :auto, transport: :unix, envoy: "envoy", docker: "docker", image: ENVOY_IMAGE, pull: :if_missing, dns: []) ⇒ EnvoyGateway

Builds a run-scoped Envoy gateway. Envoy remains an optional external dependency and the Docker image is pinned by digest by default.

Raises:



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/little_ghost/network/envoy_gateway.rb', line 27

def initialize(policy:, runtime: :auto, transport: :unix, envoy: "envoy", docker: "docker",
  image: ENVOY_IMAGE, pull: :if_missing, dns: [])
  super(policy:)
  @runtime = runtime.to_sym
  @transport = transport.to_sym
  raise PolicyError, "Envoy runtime must be :auto, :native, or :docker" unless RUNTIMES.include?(@runtime)
  raise PolicyError, "Envoy transport must be :unix or :docker" unless TRANSPORTS.include?(@transport)

  @envoy = String(envoy)
  @docker = String(docker)
  @image = String(image)
  @pull = pull.to_sym
  raise PolicyError, "Envoy image must be a non-option image reference" if @image.empty? || @image.start_with?("-")
  raise PolicyError, "Envoy pull must be :if_missing, :never, or :always" unless %i[if_missing never always].include?(@pull)

  @dns = Array(dns).map do |address|
    address = String(address)
    IPAddr.new(address)
    address.freeze
  rescue IPAddr::InvalidAddressError
    raise PolicyError, "Envoy DNS resolvers must be IP addresses"
  end.freeze
  @gateway_id = SecureRandom.uuid
  @opened = false
end

Instance Attribute Details

#client_networkObject (readonly)

Internal Docker network exposed only to sandbox clients, when used.



56
57
58
# File 'lib/little_ghost/network/envoy_gateway.rb', line 56

def client_network
  @client_network
end

#proxy_socketObject (readonly)

Host path of the explicit proxy's Unix socket, when used.



54
55
56
# File 'lib/little_ghost/network/envoy_gateway.rb', line 54

def proxy_socket
  @proxy_socket
end

#runtimeObject (readonly)

Configured runtime selector: :auto, :native, or :docker.



58
59
60
# File 'lib/little_ghost/network/envoy_gateway.rb', line 58

def runtime
  @runtime
end

Instance Method Details

#closeObject

Removes the process, containers, networks, sockets, and trust material.



92
93
94
95
96
97
98
99
100
# File 'lib/little_ghost/network/envoy_gateway.rb', line 92

def close
  stop_native
  stop_docker
  @authorizer_server&.close
  FileUtils.remove_entry_secure(@root) if @root && File.exist?(@root)
  @root = nil
  @opened = false
  nil
end

#environmentObject

Returns proxy variables and, for inspection, child-scoped trust paths.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/little_ghost/network/envoy_gateway.rb', line 103

def environment
  endpoint = if @transport == :docker
    "http://#{@container_name}:3128"
  else
    "http://127.0.0.1:3128"
  end
  values = {
    "HTTP_PROXY" => endpoint,
    "HTTPS_PROXY" => endpoint,
    "http_proxy" => endpoint,
    "https_proxy" => endpoint,
    "NO_PROXY" => "localhost,127.0.0.1",
    "no_proxy" => "localhost,127.0.0.1"
  }
  values.merge!(trust_environment) if @trust_paths
  values.freeze
end

#mountsObject

Returns the gateway files that must be mounted into the sandbox.



122
123
124
125
126
# File 'lib/little_ghost/network/envoy_gateway.rb', line 122

def mounts
  return [] unless @transport == :unix || @trust_paths

  [{source: @client_root, target: "/run/little-ghost-egress", access: :read_only}].freeze
end

#open(run: nil) ⇒ Object

Creates configuration, trust material, and the Envoy process.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/little_ghost/network/envoy_gateway.rb', line 61

def open(run: nil)
  return self if @opened

  @root = Dir.mktmpdir("little-ghost-egress-", "/tmp")
  File.chmod(0o700, @root)
  @client_root = File.join(@root, "client")
  Dir.mkdir(@client_root, 0o700)
  @proxy_socket = File.join(@client_root, "proxy.sock") if @transport == :unix
  @interceptor_socket = File.join(@root, "interceptor.sock")
  @authorizer_socket = File.join(@root, "authorizer.sock")
  @access_log = File.join(@root, "access.log")
  @envoy_log = File.join(@root, "envoy.log")
  File.write(@access_log, "")
  File.chmod(0o600, @access_log)
  @resolved_runtime = resolve_runtime
  validate_runtime!
  prepare_http_inspection(run)
  @config_path = File.join(@root, "envoy.json")
  File.open(@config_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
    file.write("#{JSON.pretty_generate(configuration)}\n")
  end
  (@resolved_runtime == :native) ? start_native : start_docker
  wait_until_ready!
  @opened = true
  self
rescue
  close
  raise
end

#proxy_mount_pathObject

Returns the proxy socket's stable path inside a mounted sandbox.



129
# File 'lib/little_ghost/network/envoy_gateway.rb', line 129

def proxy_mount_path = "/run/little-ghost-egress/proxy.sock"