module KubeAutoAnalyzer
Constants
- VERSION
Attributes
Public Class Methods
# 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
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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
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
# 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
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
# 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
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