module KubeAutoAnalyzer

Constants

VERSION

Attributes

execute[RW]

Public Class Methods

audit_rbac() click to toggle source
# File lib/kube_auto_analyzer/api_checks/rbac_auditor.rb, line 2
def self.audit_rbac
  @log.debug("Entering the RBAC Auditor")
  target = @options.target_server
  @log.debug("Auditing RBAC on #{target}")
  @results[target][:rbac] = Hash.new
  cluster_roles = @rbac_client.get_cluster_roles
  @log.debug("got #{cluster_roles.length.to_s} cluster roles")
  cluster_role_bindings = @rbac_client.get_cluster_role_bindings
  @log.debug("got #{cluster_role_bindings.length.to_s} cluster role bindings")
  @results[target][:rbac][:cluster_roles] = Hash.new
  cluster_roles.each do |role|
    role_output = Hash.new
    role_output[:rules] = role.rules

    @log.debug("metadata in #{role.metadata[:name]} , #{role.metadata}")
    begin
      if role.metadata[:labels]['kubernetes.io/bootstrapping'] == "rbac-defaults"
        role_output[:default] = true
      else
        role_output[:default] = false
      end
    rescue NoMethodError
      #If there's no method, it can't be a default...
      role_output[:default] = false
    end
    role_output[:subjects] = Array.new
    cluster_role_bindings.each do |binding|
      #So we're testing if the binding has any subjects and if so whether they apply to this role or not
      if binding.subjects 
        @log.debug("#{binding.roleRef[:name]} binding has #{binding.subjects.length.to_s} bindings")
      else
        @log.debug("#{binding.roleRef[:name]} has no subjects")
      end
      @log.debug(binding.roleRef[:kind] + ", " + role.metadata[:name] + ", " + binding.roleRef[:name] + ", " + (binding.subjects ? binding.subjects.length.to_s : "0") )
      if binding.roleRef[:kind] == "ClusterRole"
        @log.debug("Matched the cluster role")
        if binding.roleRef[:name] == role.metadata[:name]
          @log.debug("matched the role name")
          if binding.subjects
            binding.subjects.each do |subject|
              @log.debug("added a subject to the list")
              role_output[:subjects] << subject
            end
          end
        end
      end
    end
    @results[target][:rbac][:cluster_roles][role.metadata[:name]] = role_output
  end
end
check_amicontained() click to toggle source

This is somewhat awkward placement. Deployment mechanism sits more with the agent checks But from a “what it's looking for” perspective, its more with the vuln. checks as there's not a CIS check for it.

# File lib/kube_auto_analyzer/vuln_checks/amicontained.rb, line 5
def self.check_amicontained
  require 'json'
  @log.debug("Doing Am I contained check")
  target = @options.target_server
  @results[target]['vulns']['amicontained'] = Hash.new

  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node
  end
  
  nodes.each do |nod|
    node_hostname = nod.metadata.labels['kubernetes.io/hostname']
    node_ip = nod['status']['addresses'][0]['address']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod.metadata = {}
    pod.metadata.name = container_name
    pod.metadata.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 = ["/amicontained.rb"]

    #Try the Toleration for Master
    pod.spec.tolerations = {}
    #pod.spec.tolerations = [{ key:"key", operator:"Equal", value:"value",effect:"NoSchedule"}]
    pod.spec.tolerations = [{ operator:"Exists" }]
    
    pod.spec.nodeselector = {}
    pod.spec.nodeselector['kubernetes.io/hostname'] = node_hostname
    begin
      @log.debug("About to start amicontained 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 amicontained pod")
      results = JSON.parse(@client.get_pod_log(container_name,"default"))
      @results[target]['vulns']['amicontained'][node_ip] = results
    ensure
      @client.delete_pod(container_name,"default")
    end
  end
end
check_authn() click to toggle source
# File lib/kube_auto_analyzer/api_checks/authentication_checker.rb, line 2
def self.check_authn
  @log.debug("Entering the Authentication Checker")
  target = @options.target_server
  @log.debug("Checking enabled Authentication Options on #{target}")
  @results[target][:authn] = Hash.new
  @results[target]['evidence'] = Hash.new
  pods = @client.get_pods
  pods.each do |pod|
    if pod['metadata']['name'] =~ /kube-apiserver/
      @api_server = pod
    end
  end

  api_server_command_line = @api_server['spec']['containers'][0]['command']
  if api_server_command_line.index{|line| line =~ /--basic-auth-file/}
    @results[target][:authn][:basic] = true
  else
    @results[target][:authn][:basic] = false
  end

  if api_server_command_line.index{|line| line =~ /--token-auth-file/}
    @results[target][:authn][:token] = true
  else
    @results[target][:authn][:token] = false
  end

  if api_server_command_line.index{|line| line =~ /--client-ca-file/}
    @results[target][:authn][:certificate] = true
  else
    @results[target][:authn][:certificate] = false
  end

  if api_server_command_line.index{|line| line =~ /--oidc-issuer-url/}
    @results[target][:authn][:oidc] = true
  else
    @results[target][:authn][:oidc] = false
  end

  if api_server_command_line.index{|line| line =~ /--authentication-token-webhook-config-file/}
    @results[target][:authn][:webhook] = true
  else
    @results[target][:authn][:webhook] = false
  end

  if api_server_command_line.index{|line| line =~ /--requestheader-username-headers/}
    @results[target][:authn][:proxy] = true
  else
    @results[target][:authn][:proxy] = false
  end
  #Gather evidence for the API server
  @results[target]['evidence']['API Server'] = api_server_command_line
end
check_authz() click to toggle source
# File lib/kube_auto_analyzer/api_checks/authorization_checker.rb, line 2
def self.check_authz
  @log.debug("Entering the authorization checker")
  target = @options.target_server
  @log.debug("Checking enabled authorization options on #{target}")
  @results[target][:authz] = Hash.new
  pods = @client.get_pods
  pods.each do |pod|
    if pod['metadata']['name'] =~ /kube-apiserver/
      @api_server = pod
    end
  end

  api_server_command_line = @api_server['spec']['containers'][0]['command']
  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*RBAC/}
    @results[target][:authz][:rbac] = true
    
  else
    @results[target][:authz][:rbac] = false      
  end

  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*ABAC/}
    @results[target][:authz][:abac] = true
  else
    @results[target][:authz][:abac] = false
  end

  if api_server_command_line.index{|line| line =~ /--authorization-mode\S*Webhook/}
    @results[target][:authz][:webhook] = true
  else
    @results[target][:authz][:webhook] = false
  end
end
check_files() click to toggle source
# 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]['node_files'] = Hash.new


  nodes = Array.new
  @client.get_nodes.each do |node|
    nodes << node
  end
  nodes.each do |nod|
    node_hostname = nod.metadata.labels['kubernetes.io/hostname']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod.metadata = {}
    pod.metadata.name = container_name
    pod.metadata.namespace = "default"
    pod.spec = {}
    pod.spec.restartPolicy = "Never"
    pod.spec.containers = {}
    pod.spec.containers = [{name: "kubeautoanalyzerfiletest", image: "raesene/kaa-agent:latest"}]

    #Try the Toleration for Master
    pod.spec.tolerations = {}
    #Old version doesn't work with 1.8
    #pod.spec.tolerations = [{ key:"key", operator:"Equal", value:"value",effect:"NoSchedule"}]
    pod.spec.tolerations = [{ operator:"Exists" }]
    
    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]['node_files'][node_hostname] = files
    ensure
      @client.delete_pod(container_name,"default")
    end

  end
  @log.debug("Finished Node File Check")
end
check_kubelet_process() click to toggle source
# 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.metadata.labels['kubernetes.io/hostname']
    container_name = "kaa" + node_hostname
    pod = Kubeclient::Resource.new
    pod.metadata = {}
    pod.metadata.name = container_name
    pod.metadata.namespace = "default"
    pod.spec = {}
    pod.spec.restartPolicy = "Never"
    pod.spec.containers = {}
    pod.spec.containers = [{name: "kaakubelettest", image: "raesene/kaa-agent:latest"}]
    
    #Try the Toleration for Master
    pod.spec.tolerations = {}
    #pod.spec.tolerations = [{ key:"key", operator:"Equal", value:"value",effect:"NoSchedule"}]
    pod.spec.tolerations = [{ operator:"Exists" }]
    
    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 - Ensure 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

      unless kubelet_proc =~ /--feature-gates=RotateKubeletClientCertificate=true/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.14 - Ensure that the RotateKubeletClientCertificate argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.14 - Ensure that the RotateKubeletClientCertificate argument is set to true'] = "Pass"
      end

      unless kubelet_proc =~ /--feature-gates=RotateKubeletServerCertificate=true/
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.15 - Ensure that the RotateKubeletServerCertificate argument is set to true'] = "Fail"
      else
        @results[target]['kubelet_checks'][node_hostname]['CIS 2.1.15 - Ensure that the RotateKubeletServerCertificate argument is set to true'] = "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
dump_config() click to toggle source
# File lib/kube_auto_analyzer/api_checks/config_dumper.rb, line 2
def self.dump_config
 @log.debug("Entering the config dumper module")
 target = @options.target_server
 @log.debug("dumping the config for #{target}")
 @results[target][:config] = Hash.new
 pods = @client.get_pods
 services = @client.get_services
 docker_images = Array.new
 #Specific requirement here in that it's useful to know what Docker images are in use on the cluster.
 pods.each do |pod|
   docker_images << pod.status[:containerStatuses][0][:image]
 end
 @log.debug("logged #{docker_images.length} docker images")
 @results[target][:config][:docker_images] = docker_images.uniq

 @results[target][:config][:pod_info] = Array.new

 #Lets record some information about each pod
 pods.each do |pod|
   currpod = Hash.new
   currpod[:name] = pod.metadata[:name]
   currpod[:namespace] = pod.metadata[:namespace]
   currpod[:service_account] = pod.spec[:serviceAccount]
   currpod[:host_ip] = pod[:status][:hostIP]
   currpod[:pod_ip] = pod[:status][:podIP]
   @results[target][:config][:pod_info] << currpod
 end

 @results[target][:config][:service_info] = Array.new

 services.each do |service|
   currserv = Hash.new
   currserv[:name] = service.metadata[:name]
   currserv[:cluster_ip] = service.spec[:clusterIP]
   if service.spec[:externalIP]
     currserv[:external_ip] = service.spec[:externalIP]
   else
     currserv[:external_ip] = "None"
   end
   if service.spec[:ports]
     currserv[:ports] = Array.new
     service.spec[:ports].each do |port|
       currserv[:ports] << "#{port[:port]}/#{port[:protocol]}:#{port[:targetPort]}/#{port[:protocol]}"
     end
   else
     currserv[:ports] = "None"
   end
   @results[target][:config][:service_info] << currserv
 end


end
execute(commmand_line_opts) click to toggle source
# File lib/kube_auto_analyzer.rb, line 19
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
  if @options.json_report
    @json_report_file = File.new(@report_file_name + '.json','w+')
  end

  if @options.html_report
    @html_report_file = File.new(@report_file_name + '.html','w+')
  end
  @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}
    elsif @options.insecure 
      #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
    @rbac_client = Kubeclient::Client.new @options.target_server + '/apis/rbac.authorization.k8s.io', 'v1', auth_options: auth_options, ssl_options: ssl_options
  else
    begin
      config = Kubeclient::Config.read(@options.config_file)
      if @options.context
        context = config.context(@options.context)
      else
        context = config.context
      end
    rescue Errno::ENOENT
      puts "Config File could not be read, check the path?"
      exit
    end
    if @options.nosslverify
      @client = Kubeclient::Client.new(
        context.api_endpoint,
        context.api_version,
        {
          ssl_options: {client_cert: context.ssl_options[:client_cert], client_key: context.ssl_options[:client_key],verify_ssl: OpenSSL::SSL::VERIFY_NONE},
          auth_options: context.auth_options
        }
      )
      @rbac_client = Kubeclient::Client.new(
        context.api_endpoint + '/apis/rbac.authorization.k8s.io',
        context.api_version,
        {
          ssl_options: {client_cert: context.ssl_options[:client_cert], client_key: context.ssl_options[:client_key],verify_ssl: OpenSSL::SSL::VERIFY_NONE},
          auth_options: context.auth_options
        }
      )
    else
      @client = Kubeclient::Client.new(
        context.api_endpoint,
        context.api_version,
        {
          ssl_options: context.ssl_options,
          auth_options: context.auth_options
        }
      )
      @rbac_client = Kubeclient::Client.new(
        context.api_endpoint + '/apis/rbac.authorization.k8s.io',
        context.api_version,
        {
          ssl_options: context.ssl_options,
          auth_options: context.auth_options
        }
      )
    end
    #We didn't specify the target on the command line so lets get it from the config file
    @options.target_server = context.api_endpoint
    @log.debug("target is " + @options.target_server)
    @results[context.api_endpoint] = Hash.new
  end
  #Test response
  begin
    @client.get_pods.to_s
  rescue => error
    puts error
    puts "Check of API connection failed."
    puts "try using kubectl with the same connection details"
    puts "to see what's going wrong."
    exit
  end
  if @options.cis_audit
    test_api_server
    test_scheduler
    test_controller_manager
    test_etcd
  end
  check_authn
  check_authz
  test_unauth_kubelet_external
  test_insecure_api_external
  if @options.agent_checks
    test_unauth_kubelet_internal
    test_insecure_api_internal
    test_service_token_internal
    if @options.cis_audit
      check_files
      check_kubelet_process
    end
    check_amicontained
  end
  if @options.dump_config
    dump_config
  end
  if @options.audit_rbac
    audit_rbac
  end
  if @options.html_report
    html_report
  end
  if @options.json_report
    json_report
  end
end
html_report() click to toggle source
# File lib/kube_auto_analyzer/reporting.rb, line 10
def self.html_report

  logo_path = File.join(__dir__, "data-logo.b64")
  logo = 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, .vuln-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;
      }
      .container{
        display: flex;
      } 
      .fixed{
        width: 300px;
      }
      .flex-item{
        flex-grow: 1;
      }
  </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}"
  if @options.cis_audit
    chartkick_path = File.join(__dir__, "js_files/chartkick.js")
    chartkick = File.open(chartkick_path).read
    highcharts_path = File.join(__dir__, "js_files/highcharts.js")
    highcharts = File.open(highcharts_path).read
    @html_report_file.puts "<script>#{chartkick}</script>"
    @html_report_file.puts "<script>#{highcharts}</script>"
    @html_report_file.puts '<br><br><div class="master-node"><h2>Master Node Results</h2><br>'
    #Charting setup counts for the passes and fails
    api_server_pass = 0
    api_server_fail = 0
    @results[@options.target_server]['api_server'].each do |test, result|
      if result == "Pass"
        api_server_pass  = api_server_pass + 1
      elsif result == "Fail"
        api_server_fail = api_server_fail + 1
      end
    end

    #Not a lot of point in scheduler when there's only one check...
    #scheduler_pass = 0
    #scheduler_fail = 0
    #@results[@options.target_server]['scheduler'].each do |test, result|
    #  if result == "Pass"
    #    scheduler_pass  = scheduler_pass + 1
    #  elsif result == "Fail"
    #     scheduler_fail = scheduler_fail + 1
    #  end
    #end

    controller_manager_pass = 0
    controller_manager_fail = 0
    @results[@options.target_server]['controller_manager'].each do |test, result|
      if result == "Pass"
        controller_manager_pass  = controller_manager_pass + 1
      elsif result == "Fail"
        controller_manager_fail = controller_manager_fail + 1
      end
    end

    etcd_pass = 0
    etcd_fail = 0
    @results[@options.target_server]['etcd'].each do |test, result|
      if result == "Pass"
        etcd_pass  = etcd_pass + 1
      elsif result == "Fail"
        etcd_fail = etcd_fail + 1
      end
    end

    #Start of Chart Divs
    @html_report_file.puts '<div class="container">'
    #API Server Chart
    @html_report_file.puts '<div class="fixed" id="chart-1" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    @html_report_file.puts '<script>new Chartkick.PieChart("chart-1", {"pass": ' + api_server_pass.to_s + ', "fail": ' + api_server_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"API Server Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #Scheduler Chart
    #@html_report_file.puts '<div class="flex-item" id="chart-2" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    #@html_report_file.puts '<script>new Chartkick.PieChart("chart-2", {"pass": ' + scheduler_pass.to_s + ', "fail": ' + scheduler_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"Scheduler Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #Controller Manager Chart
    @html_report_file.puts '<div class="fixed" id="chart-2" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    @html_report_file.puts '<script>new Chartkick.PieChart("chart-2", {"pass": ' + controller_manager_pass.to_s + ', "fail": ' + controller_manager_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"Controller Manager Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #etcd Chart
    @html_report_file.puts '<div class="fixed" id="chart-3" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
    @html_report_file.puts '<script>new Chartkick.PieChart("chart-3", {"pass": ' + etcd_pass.to_s + ', "fail": ' + etcd_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"etcd Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'
    #End of Chart Divs
    @html_report_file.puts '</div>'
    @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>"
    #Close the master Node Div
    @html_report_file.puts "</table></div>"
  end


  if @options.agent_checks
    @html_report_file.puts '<br><br><div class="worker-node"><h2>Worker Node Results</h2>'

    #Start of Chart Divs
    @html_report_file.puts '<div class="container">'
    @results[@options.target_server]['kubelet_checks'].each do |node, results|
      node_kubelet_pass = 0
      node_kubelet_fail = 0
      results.each do |test, result|
        if result == "Fail"
          node_kubelet_fail = node_kubelet_fail + 1
        elsif result == "Pass"
          node_kubelet_pass = node_kubelet_pass + 1
        end
      end


      #Create the Chart
      @html_report_file.puts '<div class="fixed" id="' + node + '" style="height: 300px; width: 300px; text-align: center; color: #999; line-height: 300px; font-size: 14px;font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;"></div>'
      @html_report_file.puts '<script>new Chartkick.PieChart("' + node + '", {"pass": ' + node_kubelet_pass.to_s + ', "fail": ' + node_kubelet_fail.to_s + '}, {"colors":["green","red"], "library":{"title":{"text":"' + node + ' Kubelet Results"},"chart":{"backgroundColor":"#F5F5F5"}}})</script>'

    end



    #End of Chart Divs
    @html_report_file.puts '</div>'

    @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>Node File Permissions</h2>'
    @results[@options.target_server]['node_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/ || result =~ /Unauthorized/)
      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

  if @options.agent_checks
    @html_report_file.puts '<br><h3>Container Configuration checks</h3>'
    @results[@options.target_server]['vulns']['amicontained'].each do |node, result|
      @html_report_file.puts "<br><b>#{node} Container Checks</b>"
      @html_report_file.puts "<table><thead><tr><th>Container item</th><th>Result</th></thead>"
      @html_report_file.puts "<tr><td>Runtime in Use</td><td>#{result['runtime']}</td></tr>"
      @html_report_file.puts "<tr><td>Host PID namespace used?</td><td>#{result['hostpid']}</td></tr>"
      @html_report_file.puts "<tr><td>AppArmor Profile</td><td>#{result['apparmor']}</td></tr>"
      @html_report_file.puts "<tr><td>User Namespaces in use?</td><td>#{result['uid_map']}</td></tr>"
      @html_report_file.puts "<tr><td>Inherited Capabilities</td><td>#{result['cap_inh']}</td></tr>"
      @html_report_file.puts "<tr><td>Effective Capabilities</td><td>#{result['cap_eff']}</td></tr>"
      @html_report_file.puts "<tr><td>Permitted Capabilities</td><td>#{result['cap_per']}</td></tr>"
      @html_report_file.puts "<tr><td>Bounded Capabilities</td><td>#{result['cap_bnd']}</td></tr>"
      @html_report_file.puts "</table>"
    end
  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
  if @options.agent_checks
    @results[@options.target_server]['vulns']['amicontained'].each do |node, result|
      @html_report_file.puts "<tr><td>Am I Contained Output</td><td>#{node}</td><td>#{result}</td></tr>"   
    end
  end

  @html_report_file.puts "</table>"

  #Show what cluster authentication modes are supported.
  @html_report_file.puts "<br><br><h1>Kubernetes Cluster Information</h1>"
  @html_report_file.puts "<br><br><h2>Kubernetes Authentication Options</h2>"
  @html_report_file.puts "<table><thead><tr><th>Authentication Option</th><th>Enabled?</th></tr></thead>"
  if @results[@options.target_server][:authn][:basic] == true
    @html_report_file.puts "<tr><td>Basic Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Basic Authentication</td><td>Disabled</td></tr>"
  end
  if @results[@options.target_server][:authn][:token] == true
    @html_report_file.puts "<tr><td>Token Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Token Authentication</td><td>Disabled</td></tr>"
  end
  if @results[@options.target_server][:authn][:certificate] == true
    @html_report_file.puts "<tr><td>Client Certificate Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Client Certificate Authentication</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authn][:oidc] == true
    @html_report_file.puts "<tr><td>OpenID Connect Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>OpenID Connect Authentication</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authn][:webhook] == true
    @html_report_file.puts "<tr><td>Webhook Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Webhook Authentication</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authn][:proxy] == true
    @html_report_file.puts "<tr><td>Proxy Authentication</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Proxy Authentication</td><td>Disabled</td></tr>"
  end

  @html_report_file.puts "</table>"

  #Show what cluster authorization modes are supported.
  @html_report_file.puts "<br><br>"
  @html_report_file.puts "<br><br><h2>Kubernetes Authorization Options</h2>"
  @html_report_file.puts "<table><thead><tr><th>Authorization Option</th><th>Enabled?</th></tr></thead>"

  if @results[@options.target_server][:authz][:rbac] == true
    @html_report_file.puts "<tr><td>Role Based Authorization</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Role Based Authorization</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authz][:abac] == true
    @html_report_file.puts "<tr><td>Attribute Based Authorization</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Attribute Based Authorization</td><td>Disabled</td></tr>"
  end

  if @results[@options.target_server][:authz][:webhook] == true
    @html_report_file.puts "<tr><td>Webhook Authorization</td><td>Enabled</td></tr>"
  else
    @html_report_file.puts "<tr><td>Webhook Authorization</td><td>Disabled</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
  @html_report_file.puts "</table>"  

  #Only show this section if we were asked to dump the config
  if @options.dump_config
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br><h2>Cluster Config Information</h2>"
    @html_report_file.puts "<table><thead><tr><th>Docker Images In Use</th></tr></thead>"
    @results[@options.target_server][:config][:docker_images].each do |image|
      @html_report_file.puts "<tr><td>#{image}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<table><thead><tr><th>Pod Name</th><th>Namespace</th><th>Service Account</th><th>Host IP</th><th>Pod IP</th></tr></thead>"
    @results[@options.target_server][:config][:pod_info].each do |pod|
      @html_report_file.puts "<tr><td>#{pod[:name]}</td><td>#{pod[:namespace]}</td><td>#{pod[:service_account]}</td><td>#{pod[:host_ip]}</td><td>#{pod[:pod_ip]}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<table><thead><tr><th>Service Name</th><th>Cluster IP</th><th>External IP</th><th>Port:Target Port</th></tr></thead>"
    @results[@options.target_server][:config][:service_info].each do |service|

      @html_report_file.puts "<tr><td>#{service[:name]}</td><td>#{service[:cluster_ip]}</td><td>#{service[:external_ip]}</td><td>#{service[:ports].join('<br>')}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
  end

  #Only show this section if we were asked to dump RBAC
  if @options.audit_rbac
    @html_report_file.puts "<br><br>"
    @html_report_file.puts "<br><br><h2>Cluster Role Information</h2>"
    @html_report_file.puts "<table><thead><tr><th>Name</th><th>Default?</th><th>Subjects</th><th>Rules</th></tr></thead>"
    @results[@options.target_server][:rbac][:cluster_roles].each do |name, info|
      subjects = ''
      info[:subjects].each do |subject|
        subjects << "#{subject[:kind]}:#{subject[:namespace]}:#{subject[:name]}<br>"
      end
      rules = ''
      info[:rules].each do |rule|
        unless rule.verbs
          rule.verbs = Array.new
        end
        unless rule.apiGroups
          rule.apiGroups = Array.new
        end
        unless rule.resources
          rule.resources = Array.new
        end
        rules << "Verbs : #{rule.verbs.join(', ')}<br>API Groups : #{rule.apiGroups.join(', ')}<br>Resources : #{rule.resources.join(', ')}<br><hr>"
      end
      @html_report_file.puts "<tr><td>#{name}</td><td>#{info[:default]}</td><td>#{subjects}</td><td>#{rules}</td></tr>"
    end
    @html_report_file.puts "</table>"
    @html_report_file.puts "<br><br>"
  end

  #Closing the report off
  @html_report_file.puts '</body></html>'
end
is_port_open?(ip, port) click to toggle source
# 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
  rescue Errno::ENETUNREACH
    return false
  end
  true
end
json_report() click to toggle source
# File lib/kube_auto_analyzer/reporting.rb, line 3
def self.json_report
  require 'json'
  @log.debug("Starting Report")
  @json_report_file.puts JSON.generate(@results) 

end
test_api_server() click to toggle source
# 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
  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 Anonymous Auth
  unless api_server_command_line.index{|line| line =~ /--anonymous-auth=false/}
    @results[target]['api_server']['CIS 1.1.1 - Ensure that the --anonymous-auth argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.1 - 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.2 - Ensure that the --basic-auth-file argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.2 - 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.3 - Ensure that the --insecure-allow-any-token argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.3 - 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.4 - Ensure that the --kubelet-https argument is set to true'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.4 - 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.5 - Ensure that the --insecure-bind-address argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.5 - 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.6 - Ensure that the --insecure-port argument is set to 0'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.6 - 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.7 - Ensure that the --secure-port argument is not set to 0'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.7 - 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.8 - Ensure that the --profiling argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.8 - 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.9 - Ensure that the --repair-malformed-updates argument is set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.9 - 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.10 - Ensure that the admission control policy is not set to AlwaysAdmit'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.10 - 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.11 - Ensure that the admission control policy is set to AlwaysPullImages'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.11 - 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.12 - Ensure that the admission control policy is set to DenyEscalatingExec'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.12 - 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.13 - Ensure that the admission control policy is set to SecurityContextDeny'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.13 - 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.14 - Ensure that the admission control policy is set to NamespaceLifecycle'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.14 - 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.15 - Ensure that the --audit-log-path argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.15 - 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.16 - Ensure that the --audit-log-maxage argument is set to 30 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.16 - 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.17 - Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.17 - 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.18 - Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.18 - 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.19 - Ensure that the --authorization-mode argument is not set to AlwaysAllow'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.19 - 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.20 - Ensure that the --token-auth-file argument is not set'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.20 - 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.21 - Ensure that the --kubelet-certificate-authority argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.21 - 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.22 - Ensure that the --kubelet-client-certificate and --kubelet-client-key arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.22 - 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.23 - Ensure that the --service-account-lookup argument is set to true'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.23 - 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.24 - Ensure that the admission control policy is set to PodSecurityPolicy'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.24 - 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.25 - Ensure that the --service-account-key-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.25 - 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.26 - Ensure that the --etcd-certfile and --etcd-keyfile arguments are set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.26 - 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.27 - Ensure that the admission control policy is set to ServiceAccount'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.27 - 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.28 - 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.28 - 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.29 - Ensure that the --client-ca-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.29 - 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.30 - Ensure that the --etcd-cafile argument is set as appropriate'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.30 - Ensure that the --etcd-cafile argument is set as appropriate'] = "Pass"
  end

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

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

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

  #1.1.34 can't be checked using this methodology so it's TBD

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

  if api_server_command_line.index{|line| line =~ /--feature-gates=AdvancedAuditing=false/}
    @results[target]['api_server']['CIS 1.1.36 - Ensure that the AdvancedAuditing argument is not set to false'] = "Fail"
  else
    @results[target]['api_server']['CIS 1.1.36 - Ensure that the AdvancedAuditing argument is not set to false'] = "Pass"
  end

  #1.1.37 This one is dubious for a pass/fail test as the value should be evaluated against the relity of the cluster.

  
end
test_controller_manager() click to toggle source
# File lib/kube_auto_analyzer/api_checks/master_node.rb, line 277
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.3 - Ensure that the --use-service-account-credentials argument is set to true'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.3 - 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.4 - Ensure that the --service-account-private-key-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.4 - 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.5 - Ensure that the --root-ca-file argument is set as appropriate'] = "Fail"
  else
    @results[target]['controller_manager']['CIS 1.3.5 - Ensure that the --root-ca-file argument is set as appropriate'] = "Pass"
  end 

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

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

end
test_etcd() click to toggle source
# File lib/kube_auto_analyzer/api_checks/master_node.rb, line 342
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

  #This isn't quite right as we should really check the dir. but as that's not easily done lets start with an existence check
  unless etcd_command_line.index{|line| line =~ /--wal-dir/}
    @results[target]['etcd']['CIS 1.5.7 - Ensure that the --wal-dir argument is set as appropriate'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.7 - Ensure that the --wal-dir argument is set as appropriate'] = "Pass"
  end

  unless etcd_command_line.index{|line| line =~ /--max-wals=0/}
    @results[target]['etcd']['CIS 1.5.8 - Ensure that the --max-wals argument is set to 0'] = "Fail"
  else
    @results[target]['etcd']['CIS 1.5.8 - Ensure that the --max-wals argument is set to 0'] = "Pass"
  end



  @results[target]['evidence']['etcd'] = etcd_command_line
end
test_insecure_api_external() click to toggle source
# 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"
      rescue RestClient::NotFound
        pods_resp = "Not Vulnerable - Request Not Found"
      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_internal() click to toggle source

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.

# File lib/kube_auto_analyzer/vuln_checks/api_server.rb, line 33
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.metadata = {}
  pod.metadata.name = container_name
  pod.metadata.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")
    sleep_count = 0
    begin
      sleep(5) until @client.get_pod(container_name,"default")['status']['containerStatuses'][0]['state']['terminated']['reason'] == "Completed"
      sleep_count = sleep_count + 1
      @log.debug("Waited #{(5 * sleep_count).to_s} seconds for the API Server Check Pod")
    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_scheduler() click to toggle source
# File lib/kube_auto_analyzer/api_checks/master_node.rb, line 251
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_internal() click to toggle source

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.

# 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.metadata = {}
  pod.metadata.name = container_name
  pod.metadata.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_external() click to toggle source
# 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"
      rescue RestClient::Unauthorized
        pods_resp = "Not Vulnerable - Request Unauthorized"
      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_internal() click to toggle source

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.

# File lib/kube_auto_analyzer/vuln_checks/kubelet.rb, line 33
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.metadata = {}
  pod.metadata.name = container_name
  pod.metadata.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