Module: KubeAutoAnalyzer

Defined in:
lib/kube_auto_analyzer.rb,
lib/kube_auto_analyzer/version.rb,
lib/kube_auto_analyzer/reporting.rb,
lib/kube_auto_analyzer/utility/network.rb,
lib/kube_auto_analyzer/vuln_checks/kubelet.rb,
lib/kube_auto_analyzer/api_checks/master_node.rb,
lib/kube_auto_analyzer/vuln_checks/api_server.rb,
lib/kube_auto_analyzer/agent_checks/file_checks.rb,
lib/kube_auto_analyzer/vuln_checks/service_token.rb,
lib/kube_auto_analyzer/agent_checks/process_checks.rb

Constant Summary collapse

VERSION =
"0.0.3"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Attribute Details

#executeObject

Returns the value of attribute execute.



2
3
4
# File 'lib/kube_auto_analyzer.rb', line 2

def execute
  @execute
end

Class Method Details

.check_filesObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/kube_auto_analyzer/agent_checks/file_checks.rb', line 3

def self.check_files
  require 'json'
  @log.debug ("entering File check")
  target = @options.target_server
  @results[target]['worker_files'] = Hash.new

  #Run on any nodes that aren't NoSchedule
  #Doesn't necessarily mean worker nodes, but a reasonable facsimile for now.
  nodes = Array.new
  @client.get_nodes.each do |node|
    unless node.spec.taints.to_s =~ /NoSchedule/
      nodes << node
    end
  end
  nodes.each do |nod|
    node_hostname = nod..labels['kubernetes.io/hostname']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod. = {}
    pod..name = container_name
    pod..namespace = "default"
    pod.spec = {}
    pod.spec.restartPolicy = "Never"
    pod.spec.containers = {}
    pod.spec.containers = [{name: "kubeautoanalyzerfiletest", image: "raesene/kaa-agent:latest"}]
    pod.spec.volumes = [{name: 'etck8s', hostPath: {path: '/etc'}}]
    pod.spec.containers[0].volumeMounts = [{mountPath: '/etc', name: 'etck8s'}]
    pod.spec.containers[0].args = ["/file-checker.rb","/etc/kubernetes"]
    pod.spec.nodeselector = {}
    begin
      pod.spec.nodeselector['kubernetes.io/hostname'] = node_hostname
      @client.create_pod(pod)
      begin
        sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
      rescue
        retry
      end
      files = JSON.parse(@client.get_pod_log(container_name,"default"))
    
      @results[target]['worker_files'][node_hostname] = files
    ensure
      @client.delete_pod(container_name,"default")
    end

  end
  @log.debug("Finished Worker File Check")
end

.check_kubelet_processObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
52
53
54
55
56
57
58
59
60
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/kube_auto_analyzer/agent_checks/process_checks.rb', line 3

def self.check_kubelet_process
  @log.debug("Entering Process Checks")
  target = @options.target_server
  @results[target]['kubelet_checks'] = Hash.new
  @results[target]['node_evidence'] = Hash.new


  nodes = Array.new
  @client.get_nodes.each do |node|
    unless node.spec.taints.to_s =~ /NoSchedule/
      nodes << node
    end
  end

  nodes.each do |nod|
    node_hostname = nod..labels['kubernetes.io/hostname']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod. = {}
    pod..name = container_name
    pod..namespace = "default"
    pod.spec = {}
    pod.spec.restartPolicy = "Never"
    pod.spec.containers = {}
    pod.spec.containers = [{name: "kaakubelettest", image: "raesene/kaa-agent:latest"}]
    pod.spec.containers[0].args = ["/process-checker.rb"]
    pod.spec.hostPID = true
    pod.spec.nodeselector = {}
    pod.spec.nodeselector['kubernetes.io/hostname'] = node_hostname
    begin
      @client.create_pod(pod)
      begin
        sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
      rescue
        retry
      end
      processes = JSON.parse(@client.get_pod_log(container_name,"default"))
      #If we didn't get more than one process, we're probably not reading the host ones
      #So either it's a bug or we don't have rights
      if processes.length < 2
        @log.debug("Process Check failed didn't get the node process list")
        @results[target]['kubelet_checks'][node_hostname]['Kubelet Not Found'] = "Error - couldn't see host process list"
        @client.delete_pod(container_name,"default")
        return
      end
      #puts processes
      kubelet_proc = ''
      processes.each do |proc|
        if proc =~ /kubelet/
          kubelet_proc = proc
        end
      end
      @results[target]['kubelet_checks'][node_hostname] = Hash.new
      unless kubelet_proc.length > 1
        @results[target]['kubelet_checks'][node_hostname]['Kubelet Not Found'] = "Error"
        @log.debug(processes)
        @client.delete_pod(container_name,"default")
        return
      end

      @results[target]['node_evidence'][node_hostname] = Hash.new
      @results[target]['node_evidence'][node_hostname]['kubelet'] = kubelet_proc


      
      #Checks
      unless kubelet_proc =~ /--allow-privileged=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.1 - Ensure that the --allow-privileged argument is set to false'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.1 - Ensure that the --allow-privileged argument is set to false'] = "Pass"
      end

      unless kubelet_proc =~ /--anonymous-auth=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.2 - Ensure that the --anonymous-auth argument is set to false'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.2 - Ensure that the --anonymous-auth argument is set to false'] = "Pass"
      end

      if kubelet_proc =~ /--authorization-mode\S*AlwaysAllow/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.3 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.3 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Pass"
      end

      unless kubelet_proc =~ /--client-ca-file/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.4 - Ensure that the --client-ca-file argument is set as appropriate'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.4 - Ensure that the --client-ca-file argument is set as appropriate'] = "Pass"
      end

      unless kubelet_proc =~ /--read-only-port=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.5 - Ensure that the --read-only-port argument is set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.5 - Ensure that the --read-only-port argument is set to 0'] = "Pass"
      end

      if kubelet_proc =~ /--streaming-connection-idle-timeout=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.6 - Ensure that the --streaming-connection-idle-timeout argument is not set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.6 - Ensure that the --streaming-connection-idle-timeout argument is not set to 0'] = "Pass"
      end

      unless kubelet_proc =~ /--protect-kernel-defaults=true/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.7 - Ensure that the --protect-kernel-defaults argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.7 - Ensure that the --protect-kernel-defaults argument is set to true'] = "Pass"
      end

      if kubelet_proc =~ /--make-iptables-util-chains=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.8 - Ensure that the --make-iptables-util-chains argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.8 - Ensure that the --make-iptables-util-chains argument is set to true'] = "Pass"
      end

      unless kubelet_proc =~ /--keep-terminated-pod-volumes=false/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.9 - that the --keep-terminated-pod-volumes argument is set to false'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.9 - Ensure that the --keep-terminated-pod-volumes argument is set to false'] = "Pass"
      end

      if kubelet_proc =~ /--hostname-override/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.10 - Ensure that the --hostname-override argument is not set'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.10 - Ensure that the --hostname-override argument is not set'] = "Pass"
      end      

      unless kubelet_proc =~ /--event-qps=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.11 - Ensure that the --event-qps argument is set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.11 - Ensure that the --event-qps argument is set to 0'] = "Pass"
      end

      unless (kubelet_proc =~ /--tls-cert-file/) && (kubelet_proc =~ /--tls-private-key-file/)
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.12 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.12 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Pass"
      end

      unless kubelet_proc =~ /--cadvisor-port=0/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.13 - Ensure that the --cadvisor-port argument is set to 0'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.13 - Ensure that the --cadvisor-port argument is set to 0'] = "Pass"
      end
    #Need an ensure block here to make sure that the pod is deleted after its run  
    ensure
      @client.delete_pod(container_name,"default")
    end

  end

end

.execute(commmand_line_opts) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
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
52
53
54
55
56
57
58
59
60
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
90
91
92
93
94
95
96
97
98
# File 'lib/kube_auto_analyzer.rb', line 14

def self.execute(commmand_line_opts)
  @options = commmand_line_opts
  require 'logger'
  begin
    require 'kubeclient'
  rescue LoadError
    puts "You need to install kubeclient for this, try 'gem install kubeclient'"
    exit
  end

  @base_dir = @options.report_directory
  if !File.exists?(@base_dir)
    Dir.mkdirs(@base_dir)
  end

  @log = Logger.new(@base_dir + '/kube-analyzer-log.txt')
  @log.level = Logger::DEBUG
  @log.debug("Log created at " + Time.now.to_s)
  @log.debug("Target API Server is " + @options.target_server)

  @report_file_name = @base_dir + '/' + @options.report_file
  #Remove the Text report for now as we're not using this option
  #@report_file = File.new(@report_file_name + '.txt','w+')
  @html_report_file = File.new(@report_file_name + '.html','w+')
  @log.debug("New Report File created #{@report_file_name}")
      
  @results = Hash.new
  #TODO: Expose this as an option rather than hard-code to off
  unless @options.config_file
    ssl_options = { verify_ssl: OpenSSL::SSL::VERIFY_NONE}
    #TODO: Need to setup the other authentication options
    if @options.token.length > 1
      auth_options = { bearer_token: @options.token}
    elsif @options.token_file.length > 1
      auth_options = { bearer_token_file: @options.token_file}
    else
      #Not sure this will actually work for no auth. needed, try and ooold cluster to check
      auth_options = {}
    end
    @results[@options.target_server] = Hash.new
    @client = Kubeclient::Client.new @options.target_server, 'v1', auth_options: auth_options, ssl_options: ssl_options
  else
    begin
      config = Kubeclient::Config.read(@options.config_file)
    rescue Errno::ENOENT
      puts "Config File could not be read, check the path?"
      exit
    end
    @client = Kubeclient::Client.new(
      config.context.api_endpoint,
      config.context.api_version,
      {
        ssl_options: config.context.ssl_options,
        auth_options: config.context.auth_options
      }
    )
    #We didn't specify the target on the command line so lets get it from the config file
    @options.target_server = config.context.api_endpoint
    @log.debug("target is " + @options.target_server)
    @results[config.context.api_endpoint] = Hash.new
  end
  #Test response
  begin
    @client.get_pods.to_s
  rescue
    puts "Check of API connection failed."
    puts "try using kubectl with the same connection details"
    puts "to see what's going wrong."
    exit
  end
  test_api_server
  test_scheduler
  test_controller_manager
  test_etcd
  test_unauth_kubelet_external
  test_insecure_api_external
  if @options.agent_checks
    test_unauth_kubelet_internal
    test_insecure_api_internal
    test_service_token_internal
    check_files
    check_kubelet_process
  end
  html_report
end

.html_reportObject



53
54
55
56
57
58
59
60
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/kube_auto_analyzer/reporting.rb', line 53

def self.html_report
  logo_path = File.join(__dir__, "data-logo.b64")
   = File.open(logo_path).read
  @log.debug("Starting HTML Report")
  @html_report_file << '
    <!DOCTYPE html>
    <head>
     <title> Kubernetes Auto Analyzer Report</title>
     <meta charset="utf-8"> 
     <style>
      body {
        font: normal 14px;
        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
        color: #C41230;
        background: #FFFFFF;
      }
      #kubernetes-analyzer {
        font-weight: bold;
        font-size: 48px;
        color: #C41230;
      }
      .master-node, .worker-node {
        background: #F5F5F5;
        border: 1px solid black;
        padding-left: 6px;
      }
      #api-server-results {
        font-weight: italic;
        font-size: 36px;
        color: #C41230;
      }
      table, th, td {
        border-collapse: collapse;
        border: 1px solid black;
      }
      th {
       font: bold 11px;
       color: #C41230;
       background: #999999;
       letter-spacing: 2px;
       text-transform: uppercase;
       text-align: left;
       padding: 6px 6px 6px 12px;
      }
      td {
      background: #FFFFFF;
      padding: 6px 6px 6px 12px;
      color: #333333;
      }
  </style>
</head>
<body>

  '
  @html_report_file.puts '<img width="100" height="100" align="right"' + " src=#{logo} />"
  @html_report_file.puts "<h1>Kubernetes Auto Analyzer</h1>"
  @html_report_file.puts "<br><b>Server Reviewed : </b> #{@options.target_server}"
  @html_report_file.puts '<br><br><div class="master-node"><h2>Master Node Results</h2><br>'
  @html_report_file.puts "<h2>API Server</h2>"
  @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
  @results[@options.target_server]['api_server'].each do |test, result|      
    if result == "Fail"
      result = '<span style="color:red;">Fail</span>'
    elsif result == "Pass"
      result = '<span style="color:green;">Pass</span>'
    end
    @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
  end
  @html_report_file.puts "</table>"
  @html_report_file.puts "<br><br>"
  @html_report_file.puts "<br><br><h2>Scheduler</h2>"
  @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
  @results[@options.target_server]['scheduler'].each do |test, result|      
    if result == "Fail"
      result = '<span style="color:red;">Fail</span>'
    elsif result == "Pass"
      result = '<span style="color:green;">Pass</span>'
    end
    @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
  end
  @html_report_file.puts "</table>"

  @html_report_file.puts "<br><br>"
  @html_report_file.puts "<br><br><h2>Controller Manager</h2>"
  @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
  @results[@options.target_server]['controller_manager'].each do |test, result|      
    if result == "Fail"
      result = '<span style="color:red;">Fail</span>'
    elsif result == "Pass"
      result = '<span style="color:green;">Pass</span>'
    end
    @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
  end
  @html_report_file.puts "</table>"

  @html_report_file.puts "<br><br>"
  @html_report_file.puts "<br><br><h2>etcd</h2>"
  @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
  @results[@options.target_server]['etcd'].each do |test, result|      
    if result == "Fail"
      result = '<span style="color:red;">Fail</span>'
    elsif result == "Pass"
      result = '<span style="color:green;">Pass</span>'
    end
    @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
  end
  @html_report_file.puts "</table>"

  @html_report_file.puts "<br><br><h2>Evidence</h2><br>"
  @html_report_file.puts "<table><thead><tr><th>Area</th><th>Output</th></tr></thead>"
  @results[@options.target_server]['evidence'].each do |area, output|
    @html_report_file.puts "<tr><td>#{area}</td><td>#{output}</td></tr>"
  end
  #Close the master Node Div
  @html_report_file.puts "</table></div>"
  if @options.agent_checks
    @html_report_file.puts '<br><br><div class="worker-node"><h2>Worker Node Results</h2>'
    @results[@options.target_server]['kubelet_checks'].each do |node, results|
      @html_report_file.puts "<br><b>#{node} Kubelet Checks</b>"
      @html_report_file.puts "<table><thead><tr><th>Check</th><th>result</th></tr></thead>"
      results.each do |test, result|
        if result == "Fail"
          result = '<span style="color:red;">Fail</span>'
        elsif result == "Pass"
          result = '<span style="color:green;">Pass</span>'
        end
        @html_report_file.puts "<tr><td>#{test}</td><td>#{result}</td></tr>"
      end
      @html_report_file.puts "</table>"
    end

    @html_report_file.puts "<br><br><h2>Evidence</h2><br>"
    @html_report_file.puts "<table><thead><tr><th>Host</th><th>Area</th><th>Output</th></tr></thead>"
    @results[@options.target_server]['node_evidence'].each do |node, evidence|
      evidence.each do |area, data|
        @html_report_file.puts "<tr><td>#{node}</td><td>#{area}</td><td>#{data}</td></tr>"   
      end
    end
    @html_report_file.puts "</table>"

  end
  #Close the Worker Node Div
  @html_report_file.puts '</div>'
  if @options.agent_checks
    @html_report_file.puts '<br><h2>File Permissions</h2>'
    @results[@options.target_server]['worker_files'].each do |node, results|
      @html_report_file.puts "<br><b>#{node}</b><br>"
      @html_report_file.puts "<table><thead><tr><th>file</th><th>user</th><th>group</th><th>permissions</th></thead>"
      results.each do |file|
        @html_report_file.puts "<tr><td>#{file[0]}</td><td>#{file[1]}</td><td>#{file[2]}</td><td>#{file[3]}</td></tr>"
      end
      @html_report_file.puts "</table>"
    end
  end

  @html_report_file.puts '<br><h2>Vulnerability Checks</h2>'
  @html_report_file.puts '<br><h3>External Unauthenticated Access to the Kubelet</h3>'
  @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
  @results[@options.target_server]['vulns']['unauth_kubelet'].each do |node, result|
    unless (result =~ /Forbidden/ || result =~ /Not Open/)
      output = "Vulnerable"
    else
      output = result
    end
    @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
  end
  @html_report_file.puts "</table>"
  if @options.agent_checks
    @html_report_file.puts '<br><h3>Internal Unauthenticated Access to the Kubelet</h3>'
    @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
    @results[@options.target_server]['vulns']['internal_kubelet'].each do |node, result|
      unless (result =~ /Forbidden/ || result =~ /Not Open/)
        output = "Vulnerable"
      else
        output = result
      end
      @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
    end
    @html_report_file.puts "</table>"
  end

  @html_report_file.puts '<br><h3>External Insecure API Port Exposed</h3>'
  @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
  @results[@options.target_server]['vulns']['insecure_api_external'].each do |node, result|
    unless (result =~ /Forbidden/ || result =~ /Not Open/)
      output = "Vulnerable"
    else
      output = result
    end
    @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
  end
  @html_report_file.puts "</table>"
  if @options.agent_checks
    @html_report_file.puts '<br><h3>Internal Insecure API Port Exposed</h3>'
    @html_report_file.puts "<table><thead><tr><th>Node IP Address</th><th>Result</th></thead>"
    @results[@options.target_server]['vulns']['insecure_api_internal'].each do |node, result|
      unless (result =~ /Forbidden/ || result =~ /Not Open/)
        output = "Vulnerable"
      else
        output = result
      end
      @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
    end
    @html_report_file.puts "</table>"
  end

  if @options.agent_checks
    @html_report_file.puts '<br><h3>Default Service Token In Use</h3>'
    @html_report_file.puts "<table><thead><tr><th>API endpoint</th><th>Result</th></thead>"
    @results[@options.target_server]['vulns']['service_token'].each do |node, result|
      unless (result =~ /Forbidden/ || result =~ /Not Open/)
        output = "Vulnerable"
      else
        output = result
      end
      @html_report_file.puts "<tr><td>#{node}</td><td>#{output}</td></tr>"
    end
    @html_report_file.puts "</table>"
  end



  @html_report_file.puts "<br><br><h2>Vulnerability Evidence</h2><br>"
  @html_report_file.puts "<table><thead><tr><th>Vulnerability</th><th>Host</th><th>Output</th></tr></thead>"
  @results[@options.target_server]['vulns']['unauth_kubelet'].each do |node, result|
    @html_report_file.puts "<tr><td>External Unauthenticated Kubelet Access</td><td>#{node}</td><td>#{result}</td></tr>"   
  end
  if @options.agent_checks
    @results[@options.target_server]['vulns']['internal_kubelet'].each do |node, result|
      @html_report_file.puts "<tr><td>Internal Unauthenticated Kubelet Access</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end
  @results[@options.target_server]['vulns']['insecure_api_external'].each do |node, result|
    @html_report_file.puts "<tr><td>External Insecure API Server Access</td><td>#{node}</td><td>#{result}</td></tr>"   
  end
  if @options.agent_checks
    @results[@options.target_server]['vulns']['insecure_api_internal'].each do |node, result|
      @html_report_file.puts "<tr><td>Internal Insecure API Server Access</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end
  if @options.agent_checks
    @results[@options.target_server]['vulns']['service_token'].each do |node, result|
      @html_report_file.puts "<tr><td>Default Service Token In Use</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end
  @html_report_file.puts "</table>"


  #Closing the report off
  @html_report_file.puts '</body></html>'
end

.is_port_open?(ip, port) ⇒ Boolean

Returns:

  • (Boolean)


4
5
6
7
8
9
10
11
12
13
# File 'lib/kube_auto_analyzer/utility/network.rb', line 4

def self.is_port_open?(ip, port)
  begin
    Socket.tcp(ip, port, connect_timeout: 2)
  rescue Errno::ECONNREFUSED
    return false
  rescue Errno::ETIMEDOUT
    return false
  end
  true
end

.reportObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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/kube_auto_analyzer/reporting.rb', line 2

def self.report
  @log.debug("Starting Report")
  @report_file.puts "Kubernetes Analyzer"
  @report_file.puts "===================\n\n"
  @report_file.puts "**Server Reviewed** : #{@options.target_server}"
  @report_file.puts "\n\nAPI Server Results"
  @report_file.puts "----------------------\n\n"
  @results[@options.target_server]['api_server'].each do |test, result|
    @report_file.puts '* ' + test + ' - **' + result + '**'
  end
  @report_file.puts "\n\nScheduler Results"
  @report_file.puts "----------------------\n\n"
  @results[@options.target_server]['scheduler'].each do |test, result|
    @report_file.puts '* ' + test + ' - **' + result + '**'
  end

  @report_file.puts "\n\nController Manager Results"
  @report_file.puts "----------------------\n\n"
  @results[@options.target_server]['controller_manager'].each do |test, result|
    @report_file.puts '* ' + test + ' - **' + result + '**'
  end

  @report_file.puts "\n\netcd Results"
  @report_file.puts "----------------------\n\n"
  @results[@options.target_server]['etcd'].each do |test, result|
    @report_file.puts '* ' + test + ' - **' + result + '**'
  end
  if @options.agent_file_checks
    @report_file.puts "\n\nWorker Nodes File Permissions"
    @report_file.puts "----------------------\n\n"
    @log.debug("Class is #{@results[@options.target_server]['worker_files'].class}")
    @results[@options.target_server]['worker_files'].each do |node, results|
      @report_file.puts "\n\n#{node}\n"
      results.each do |file|
       @report_file.puts file.join(', ')
      end
    end
  end

  @report_file.puts "\n\nEvidence"
  @report_file.puts "---------------\n\n"
  @report_file.puts '    ' + @results[@options.target_server]['evidence']['API Server'].to_s
  @report_file.puts "---------------\n\n"
  @report_file.puts '    ' + @results[@options.target_server]['evidence']['Scheduler'].to_s
  @report_file.puts "---------------\n\n"
  @report_file.puts '    ' + @results[@options.target_server]['evidence']['Controller Manager'].to_s
  @report_file.puts "---------------\n\n"
  @report_file.puts '    ' + @results[@options.target_server]['evidence']['etcd'].to_s
  @report_file.close
end

.test_api_serverObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
52
53
54
55
56
57
58
59
60
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 3

def self.test_api_server
  @log.debug("Entering the test API Server Method")
  target = @options.target_server
  @log.debug("target is #{target}")
  @results[target]['api_server'] = Hash.new
  @results[target]['evidence'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /kube-apiserver/
      @api_server = pod
    end
  end

  unless @api_server
    @results[target]['api_server']['API Server Pod Not Found'] = "Error"
    return
  end
  
  api_server_command_line = @api_server['spec']['containers'][0]['command']

  #Check for Allow Privileged
  unless api_server_command_line.index{|line| line =~ /--allow-privileged=false/}
    @results[target]['api_server']['CIS 1.1.1 - Ensure that the --allow-privileged argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.1 - Ensure that the --allow-privileged argument is set to false'] = "Pass"
  end

  #Check for Anonymous Auth
  unless api_server_command_line.index{|line| line =~ /--anonymous-auth=false/}
    @results[target]['api_server']['CIS 1.1.2 - Ensure that the --anonymous-auth argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.2 - Ensure that the --anonymous-auth argument is set to false'] = "Pass"
  end

  #Check for Basic Auth
  if api_server_command_line.index{|line| line =~ /--basic-auth-file/}
    @results[target]['api_server']['CIS 1.1.3 - Ensure that the --basic-auth-file argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.3 - Ensure that the --basic-auth-file argument is not set'] = "Pass"
  end

  #Check for Insecure Allow Any Token
  if api_server_command_line.index{|line| line =~ /--insecure-allow-any-token/}
    @results[target]['api_server']['CIS 1.1.4 - Ensure that the --insecure-allow-any-token argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.4 - Ensure that the --insecure-allow-any-token argument is not set'] = "Pass"
  end

  #Check to confirm that Kubelet HTTPS isn't set to false
  if api_server_command_line.index{|line| line =~ /--kubelet-https=false/}
    @results[target]['api_server']['CIS 1.1.5 - Ensure that the --kubelet-https argument is set to true'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.5 - Ensure that the --kubelet-https argument is set to true'] = "Pass"
  end

  #Check for Insecure Bind Address
  if api_server_command_line.index{|line| line =~ /--insecure-bind-address/}
    @results[target]['api_server']['CIS 1.1.6 - Ensure that the --insecure-bind-address argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.6 - Ensure that the --insecure-bind-address argument is not set'] = "Pass"
  end

  #Check for Insecure Bind port
  unless api_server_command_line.index{|line| line =~ /--insecure-port=0/}
    @results[target]['api_server']['CIS 1.1.7 - Ensure that the --insecure-port argument is set to 0'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.7 - Ensure that the --insecure-port argument is set to 0'] = "Pass"
  end

  #Check Secure Port isn't set to 0
  if api_server_command_line.index{|line| line =~ /--secure-port=0/}
    @results[target]['api_server']['CIS 1.1.8 - Ensure that the --secure-port argument is not set to 0'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.8 - Ensure that the --secure-port argument is not set to 0'] = "Pass"
  end

  #
  unless api_server_command_line.index{|line| line =~ /--profiling=false/}
    @results[target]['api_server']['CIS 1.1.9 - Ensure that the --profiling argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.9 - Ensure that the --profiling argument is set to false'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--repair-malformed-updates/}
    @results[target]['api_server']['CIS 1.1.10 - Ensure that the --repair-malformed-updates argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.10 - Ensure that the --repair-malformed-updates argument is set to false'] = "Pass"
  end

  if api_server_command_line.index{|line| line =~ /--admission-control\S*AlwaysAdmit/}
    @results[target]['api_server']['CIS 1.1.11 - Ensure that the admission control policy is not set to AlwaysAdmit'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.11 - Ensure that the admission control policy is not set to AlwaysAdmit'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*AlwaysPullImages/}
    @results[target]['api_server']['CIS 1.1.12 - Ensure that the admission control policy is set to AlwaysPullImages'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.12 - Ensure that the admission control policy is set to AlwaysPullImages'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*DenyEscalatingExec/}
    @results[target]['api_server']['CIS 1.1.13 - Ensure that the admission control policy is set to DenyEscalatingExec'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.13 - Ensure that the admission control policy is set to DenyEscalatingExec'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*SecurityContextDeny/}
    @results[target]['api_server']['CIS 1.1.14 - Ensure that the admission control policy is set to SecurityContextDeny'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.14 - Ensure that the admission control policy is set to SecurityContextDeny'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*NamespaceLifecycle/}
    @results[target]['api_server']['CIS 1.1.15 - Ensure that the admission control policy is set to NamespaceLifecycle'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.15 - Ensure that the admission control policy is set to NamespaceLifecycle'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--audit-log-path/}
    @results[target]['api_server']['CIS 1.1.16 - Ensure that the --audit-log-path argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.16 - Ensure that the --audit-log-path argument is set as appropriate'] = "Pass"
  end

  #TODO: This check needs to do something with the number of days but for now lets just check whether it's present.
  unless api_server_command_line.index{|line| line =~ /--audit-log-maxage/}
    @results[target]['api_server']['CIS 1.1.17 - Ensure that the --audit-log-maxage argument is set to 30 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.17 - Ensure that the --audit-log-maxage argument is set to 30 or as appropriate'] = "Pass"
  end

  #TODO: This check needs to do something with the number of backups but for now lets just check whether it's present.
  unless api_server_command_line.index{|line| line =~ /--audit-log-maxbackup/}
    @results[target]['api_server']['CIS 1.1.18 - Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.18 - Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate'] = "Pass"
  end

  #TODO: This check needs to do something with the size of backups but for now lets just check whether it's present.
  unless api_server_command_line.index{|line| line =~ /--audit-log-maxsize/}
    @results[target]['api_server']['CIS 1.1.19 - Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.19 - Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate'] = "Pass"
  end

  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*AlwaysAllow/}
    @results[target]['api_server']['CIS 1.1.20 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.20 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Pass"
  end

  if api_server_command_line.index{|line| line =~ /--token-auth-file/}
    @results[target]['api_server']['CIS 1.1.21 - Ensure that the --token-auth-file argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.21 - Ensure that the --token-auth-file argument is not set'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--kubelet-certificate-authority/}
    @results[target]['api_server']['CIS 1.1.22 - Ensure that the --kubelet-certificate-authority argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.22 - Ensure that the --kubelet-certificate-authority argument is set as appropriate'] = "Pass"
  end

  unless (api_server_command_line.index{|line| line =~ /--kubelet-client-certificate/} && api_server_command_line.index{|line| line =~ /--kubelet-client-key/})
    @results[target]['api_server']['CIS 1.1.23 - Ensure that the --kubelet-client-certificate and --kubelet-client-key arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.23 - Ensure that the --kubelet-client-certificate and --kubelet-client-key arguments are set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--service-account-lookup=true/}
    @results[target]['api_server']['CIS 1.1.24 - Ensure that the --service-account-lookup argument is set to true'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.24 - Ensure that the --service-account-lookup argument is set to true'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*PodSecurityPolicy/}
    @results[target]['api_server']['CIS 1.1.25 - Ensure that the admission control policy is set to PodSecurityPolicy'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.25 - Ensure that the admission control policy is set to PodSecurityPolicy'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--service-account-key-file/}
    @results[target]['api_server']['CIS 1.1.26 - Ensure that the --service-account-key-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.26 - Ensure that the --service-account-key-file argument is set as appropriate'] = "Pass"
  end

  unless (api_server_command_line.index{|line| line =~ /--etcd-certfile/} && api_server_command_line.index{|line| line =~ /--etcd-keyfile/})
    @results[target]['api_server']['CIS 1.1.27 - Ensure that the --etcd-certfile and --etcd-keyfile arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.27 - Ensure that the --etcd-certfile and --etcd-keyfile arguments are set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--admission-control\S*ServiceAccount/}
    @results[target]['api_server']['CIS 1.1.28 - Ensure that the admission control policy is set to ServiceAccount'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.28 - Ensure that the admission control policy is set to ServiceAccount'] = "Pass"
  end

  unless (api_server_command_line.index{|line| line =~ /--tls-cert-file/} && api_server_command_line.index{|line| line =~ /--tls-private-key-file/})
    @results[target]['api_server']['CIS 1.1.29 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.29 - Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--client-ca-file/}
    @results[target]['api_server']['CIS 1.1.30 - Ensure that the --client-ca-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.30 - Ensure that the --client-ca-file argument is set as appropriate'] = "Pass"
  end

  unless api_server_command_line.index{|line| line =~ /--etcd-cafile/}
    @results[target]['api_server']['CIS 1.1.31 - Ensure that the --etcd-cafile argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.31 - Ensure that the --etcd-cafile argument is set as appropriate'] = "Pass"
  end

  @results[target]['evidence']['API Server'] = api_server_command_line
end

.test_controller_managerObject



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 251

def self.test_controller_manager
  target = @options.target_server
  @results[target]['controller_manager'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /kube-controller-manager/
      @controller_manager = pod
    end
  end

  unless @controller_manager
    @results[target]['controller_manager']['Controller Manager Pod Not Found'] = "Error"
    return
  end    


  controller_manager_command_line = @controller_manager['spec']['containers'][0]['command']

  unless controller_manager_command_line.index{|line| line =~ /--terminated-pod-gc-threshold/}
    @results[target]['controller_manager']['CIS 1.3.1 - Ensure that the --terminated-pod-gc-threshold argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.1 - Ensure that the --terminated-pod-gc-threshold argument is set as appropriate'] = "Pass"
  end 

  unless controller_manager_command_line.index{|line| line =~ /--profiling=false/}
    @results[target]['controller_manager']['CIS 1.3.2 - Ensure that the --profiling argument is set to false'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.2 - Ensure that the --profiling argument is set to false'] = "Pass"
  end  

  if controller_manager_command_line.index{|line| line =~ /--insecure-experimental-approve-all-kubelet-csrs-for-group/}
    @results[target]['controller_manager']['CIS 1.3.3 - Ensure that the --insecure-experimental-approve-all-kubelet-csrs-for-group argument is not set'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.3 - Ensure that the --insecure-experimental-approve-all-kubelet-csrs-for-group argument is not set'] = "Pass"
  end  

  unless controller_manager_command_line.index{|line| line =~ /--use-service-account-credentials=true/}
    @results[target]['controller_manager']['CIS 1.3.4 - Ensure that the --use-service-account-credentials argument is set to true'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.4 - Ensure that the --use-service-account-credentials argument is set to true'] = "Pass"
  end 

  unless controller_manager_command_line.index{|line| line =~ /--service-account-private-key-file/}
    @results[target]['controller_manager']['CIS 1.3.5 - Ensure that the --service-account-private-key-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.5 - Ensure that the --service-account-private-key-file argument is set as appropriate'] = "Pass"
  end 

  unless controller_manager_command_line.index{|line| line =~ /--root-ca-file/}
    @results[target]['controller_manager']['CIS 1.3.6 - Ensure that the --root-ca-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.6 - Ensure that the --root-ca-file argument is set as appropriate'] = "Pass"
  end 

  @results[target]['evidence']['Controller Manager'] = controller_manager_command_line

end

.test_etcdObject



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 310

def self.test_etcd
  target = @options.target_server
  @results[target]['etcd'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /etcd/
      @etcd = pod
    end
  end
  
  unless @etcd
    @results[target]['etcd']['etcd Pod Not Found'] = "Error"
    return
  end

  etcd_command_line = @etcd['spec']['containers'][0]['command']

  unless (etcd_command_line.index{|line| line =~ /--cert-file/} && etcd_command_line.index{|line| line =~ /--key-file/})
    @results[target]['etcd']['CIS 1.5.1 - Ensure that the --cert-file and --key-file arguments are set as appropriate'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.1 - Ensure that the --cert-file and --key-file arguments are set as appropriate'] = "Pass"
  end 

  unless etcd_command_line.index{|line| line =~ /--client-cert-auth=true/}
    @results[target]['etcd']['CIS 1.5.2 - Ensure that the --client-cert-auth argument is set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.2 - Ensure that the --client-cert-auth argument is set to true'] = "Pass"
  end

  if etcd_command_line.index{|line| line =~ /--auto-tls argument=true/}
    @results[target]['etcd']['CIS 1.5.3 - Ensure that the --auto-tls argument is not set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.3 - Ensure that the --auto-tls argument is not set to true'] = "Pass"
  end

  unless (etcd_command_line.index{|line| line =~ /--peer-cert-file/} && etcd_command_line.index{|line| line =~ /--peer-key-file/})
    @results[target]['etcd']['CIS 1.5.4 - Ensure that the --peer-cert-file and --peer-key-file arguments are set as appropriate'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.4 - Ensure that the --peer-cert-file and --peer-key-file arguments are set as appropriate'] = "Pass"
  end 

  unless etcd_command_line.index{|line| line =~ /--peer-client-cert-auth=true/}
    @results[target]['etcd']['CIS 1.5.5 - Ensure that the --peer-client-cert-auth argument is set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.5 - Ensure that the --peer-client-cert-auth argument is set to true'] = "Pass"
  end

  if etcd_command_line.index{|line| line =~ /--peer-auto-tls argument=true/}
    @results[target]['etcd']['CIS 1.5.6 - Ensure that the --peer-auto-tls argument is not set to true'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.6 - Ensure that the --peer-auto-tls argument is not set to true'] = "Pass"
  end



  @results[target]['evidence']['etcd'] = etcd_command_line
end

.test_insecure_api_externalObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/kube_auto_analyzer/vuln_checks/api_server.rb', line 3

def self.test_insecure_api_external
  @log.debug("Doing the external Insecure API check")
  target = @options.target_server
  unless @results[target]['vulns']
    @results[target]['vulns'] = Hash.new
  end
  @results[target]['vulns']['insecure_api_external'] = Hash.new
  #Check for whether the Insecure API port is visible outside the cluster
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  nodes.each do |nod|
    if is_port_open?(nod, 8080)
      begin
        pods_resp = RestClient::Request.execute(:url => "http://#{nod}:8080/api",:method => :get)
      rescue RestClient::Forbidden
        pods_resp = "Not Vulnerable - Request Forbidden"
      end
      @results[target]['vulns']['insecure_api_external'][nod] = pods_resp
    else
      @results[target]['vulns']['insecure_api_external'][nod] = "Not Vulnerable - Port Not Open"
    end
  end
end

.test_insecure_api_internalObject

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it’s looking for” perspective, as a weakness in API Server, it makes more sense here.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/kube_auto_analyzer/vuln_checks/api_server.rb', line 31

def self.test_insecure_api_internal
  require 'json'

  @log.debug("Doing the internal Insecure API Server check")
  target = @options.target_server
  @results[target]['vulns']['insecure_api_internal'] = Hash.new
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  container_name = "kaainsecureapitest"
  pod = Kubeclient::Resource.new
  pod. = {}
  pod..name = container_name
  pod..namespace = "default"
  pod.spec = {}
  pod.spec.restartPolicy = "Never"
  pod.spec.containers = {}
  pod.spec.containers = [{name: "kubeautoanalyzerapitest", image: "raesene/kaa-agent:latest"}]
  pod.spec.containers[0].args = ["/api-server-checker.rb",nodes.join(',')]
  begin
    @log.debug("About to start API Server check pod")
    @client.create_pod(pod)
    @log.debug("Executed the create pod")
    begin
      sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
    rescue
      retry
    end
    @log.debug ("started Kube API Check pod")
    results = JSON.parse(@client.get_pod_log(container_name,"default"))
    results.each do |node, results|
      @results[target]['vulns']['insecure_api_internal'][node] = results
    end
  ensure
    @client.delete_pod(container_name,"default")
  end
end

.test_schedulerObject



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/kube_auto_analyzer/api_checks/master_node.rb', line 225

def self.test_scheduler
  target = @options.target_server
  @results[target]['scheduler'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod| 
    #Ok this is a bit naive as a means of hitting the API server but hey it's a start
    if pod['metadata']['name'] =~ /kube-scheduler/
      @scheduler = pod
    end
  end
  
  unless @scheduler
    @results[target]['scheduler']['Scheduler Pod Not Found'] = "Error"
    return
  end

  scheduler_command_line = @scheduler['spec']['containers'][0]['command']

  unless scheduler_command_line.index{|line| line =~ /--profiling=false/}
    @results[target]['scheduler']['CIS 1.2.1 - Ensure that the --profiling argument is set to false'] = "Fail"
  else
    @results[target]['scheduler']['CIS 1.2.1 - Ensure that the --profiling argument is set to false'] = "Pass"
  end  
  @results[target]['evidence']['Scheduler'] = scheduler_command_line
end

.test_service_token_internalObject

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it’s looking for” perspective, as a weakness in Kubelet, it makes more sense here.



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/kube_auto_analyzer/vuln_checks/service_token.rb', line 5

def self.test_service_token_internal
  require 'json'

  @log.debug("Doing the internal Service Token check")
  target = @options.target_server
  @results[target]['vulns']['service_token'] = Hash.new
  api_server_url = @client.api_endpoint.to_s
  container_name = "kaakubeletunauthtest"
  pod = Kubeclient::Resource.new
  pod. = {}
  pod..name = container_name
  pod..namespace = "default"
  pod.spec = {}
  pod.spec.restartPolicy = "Never"
  pod.spec.containers = {}
  pod.spec.containers = [{name: "kubeautoanalyzerservicetokentest", image: "raesene/kaa-agent:latest"}]
  pod.spec.containers[0].args = ["/service-token-checker.rb",api_server_url]
  begin
    @log.debug("About to start Service Token Check pod")
    @client.create_pod(pod)
    @log.debug("Executed the create pod")
    begin
      sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
    rescue
      retry
    end
    @log.debug ("started Service Token Check pod")
    results = JSON.parse(@client.get_pod_log(container_name,"default"))
    results.each do |node, results|
      @results[target]['vulns']['service_token'][api_server_url] = results
    end
  ensure
    @client.delete_pod(container_name,"default")
  end
end

.test_unauth_kubelet_externalObject



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/kube_auto_analyzer/vuln_checks/kubelet.rb', line 3

def self.test_unauth_kubelet_external
  @log.debug("Doing the external kubelet check")
  target = @options.target_server
  unless @results[target]['vulns']
    @results[target]['vulns'] = Hash.new
  end
  @results[target]['vulns']['unauth_kubelet'] = Hash.new
  #Check for whether the Kubelet port is visible outside the cluster
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  nodes.each do |nod|
    if is_port_open?(nod, 10250)
      begin
        pods_resp = RestClient::Request.execute(:url => "https://#{nod}:10250/runningpods",:method => :get, :verify_ssl => false)
      rescue RestClient::Forbidden
        pods_resp = "Not Vulnerable - Request Forbidden"
      end
      @results[target]['vulns']['unauth_kubelet'][nod] = pods_resp
    else
      @results[target]['vulns']['unauth_kubelet'][nod] = "Not Vulnerable - Port Not Open"
    end
  end
end

.test_unauth_kubelet_internalObject

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it’s looking for” perspective, as a weakness in Kubelet, it makes more sense here.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/kube_auto_analyzer/vuln_checks/kubelet.rb', line 31

def self.test_unauth_kubelet_internal
  require 'json'

  @log.debug("Doing the internal kubelet check")
  target = @options.target_server
  @results[target]['vulns']['internal_kubelet'] = Hash.new
  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node['status']['addresses'][0]['address']
  end
  container_name = "kaakubeletunauthtest"
  pod = Kubeclient::Resource.new
  pod. = {}
  pod..name = container_name
  pod..namespace = "default"
  pod.spec = {}
  pod.spec.restartPolicy = "Never"
  pod.spec.containers = {}
  pod.spec.containers = [{name: "kubeautoanalyzerkubelettest", image: "raesene/kaa-agent:latest"}]
  pod.spec.containers[0].args = ["/kubelet-checker.rb",nodes.join(',')]
  begin
    @log.debug("About to start Kubelet check pod")
    @client.create_pod(pod)
    @log.debug("Executed the create pod")
    begin
      sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
    rescue
      retry
    end
    @log.debug ("started Kubelet Check pod")
    results = JSON.parse(@client.get_pod_log(container_name,"default"))
    results.each do |node, results|
      @results[target]['vulns']['internal_kubelet'][node] = results
    end
  ensure
    @client.delete_pod(container_name,"default")
  end
end