diff --git a/CIS hardening.md b/CIS hardening.md index 1acfa9d..50eda46 100644 --- a/CIS hardening.md +++ b/CIS hardening.md @@ -1,6 +1,6 @@ # CIS Benchmark -I used the CIS Kubernetes Benchmark 1.9 +I used the CIS Kubernetes Benchmark 1.12 ## 1.1 - Control plane @@ -42,7 +42,7 @@ We use externalIP services Set by default -### 1.2.5 - Ensure that the --kubelet-certificate-authority argument is set as appropriate (Automated) +### 1.2.5 - Ensure that the --kubelet-certificate-authority argument is set as appropriate (Automated) Fixed using automatic certificate renewal and issuing to kubelets @@ -70,7 +70,7 @@ Set by default ### 1.2.15 -New relic uses the profiling data +Fixed ### 1.2.16 - Ensure that the --audit-log-path argument is set @@ -96,7 +96,11 @@ Set by default Set by default -### 1.2.29 - Ensure that the API Server only makes use of Strong Cryptographic Ciphers (Manual) +### 1.2.29 - Ensure that the API Server only makes use of Strong Cryptographic Ciphers (Manual) + +Fixed + +### 1.2.30 - Ensure that the --service-account-extend-token-expiration parameter is set to false Fixed @@ -108,7 +112,7 @@ Fixed ### 1.3.2 - Ensure that the --profiling argument is set to false -New Relic uses the profiling information +Fixed ### 1.3.3 - 1.3.7 @@ -118,7 +122,7 @@ Set by default ### 1.4.1 - Ensure that the --profiling argument is set to false -New Relic uses the profiling information +Fixed ### 1.4.2 @@ -164,7 +168,7 @@ Set by default Set by default - config file is stored in the container -### 4.1.4 - If proxy kubeconfig file exists ensure ownership is set to root:root (Manual) +### 4.1.4 - If proxy kubeconfig file exists ensure ownership is set to root:root (Manual) Set by default - config file is stored in the container @@ -206,7 +210,7 @@ Setting serverTLSBootstrap resolves this Set by default -### 4.2.11 - Verify that the RotateKubeletServerCertificate argument is set to true (Manual) +### 4.2.11 - Verify that the RotateKubeletServerCertificate argument is set to true (Manual) Set by default @@ -218,9 +222,13 @@ Fixed Fixed +### 4.2.14 - Ensure that the --seccomp-default parameter is set to true + +Fixed + ## 4.3 - Kube Proxy -### 4.3.1 - Ensure that the kube-proxy metrics service is bound to localhost (Automated) +### 4.3.1 - Ensure that the kube-proxy metrics service is bound to localhost (Automated) Fixed @@ -238,6 +246,6 @@ Set by default Fixed for initally created namespaces, it's a mnaul process to maintain the configuration on all default service accounts -### 5.1.6 - 5.1.11 +### 5.1.6 - 5.1.13 Set by default diff --git a/README.md b/README.md index 670bc18..97c545c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Purpose -The purpose of this playbook and roles is to install a vanilla Kubernetes cluster with OIDC enabled hardened against the CIS Benchmark and DOD Stig. +The purpose of this playbook and roles is to install a vanilla Kubernetes cluster with OIDC enabled hardened against the CIS Benchmark 1.12 and DOD Stig. It is a vanilla `kubeadm` cluster that can be managed by `kubeadm` going forward, or for easy upgrades you can use the included `upgrade` playbook. diff --git a/roles/kubernetes-control-plane/tasks/main.yml b/roles/kubernetes-control-plane/tasks/main.yml index 4329fea..bc95903 100644 --- a/roles/kubernetes-control-plane/tasks/main.yml +++ b/roles/kubernetes-control-plane/tasks/main.yml @@ -23,6 +23,19 @@ register: temp_token changed_when: true +- name: Get CA certificate hash + ansible.builtin.shell: + cmd: | + set -eo pipefail + openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \ + openssl rsa -pubin -outform der 2>/dev/null | \ + openssl dgst -sha256 -hex | sed 's/^.* //' + executable: /bin/bash + delegate_to: "{{ first_kube_control_plane }}" + run_once: true + register: cluster_ca_cert_hash_temp + changed_when: false + - name: Upload certificates so they are fresh and not expired ansible.builtin.command: cmd: kubeadm init phase --config {{ kubernetes_config_directory }}/kubeadm.yaml upload-certs --upload-certs @@ -35,6 +48,7 @@ ansible.builtin.set_fact: join_token: "{{ temp_token.stdout }}" kubeadm_upload_token: "{{ kubeadm_upload_cert.stdout_lines[-1] | trim }}" + cluster_ca_cert_hash: "sha256:{{ cluster_ca_cert_hash_temp.stdout }}" run_once: true - name: Configure additional control planes diff --git a/roles/kubernetes-control-plane/templates/kubeadm-init-1.33.yaml.j2 b/roles/kubernetes-control-plane/templates/kubeadm-init-1.33.yaml.j2 index 9550cb4..f3b5516 100644 --- a/roles/kubernetes-control-plane/templates/kubeadm-init-1.33.yaml.j2 +++ b/roles/kubernetes-control-plane/templates/kubeadm-init-1.33.yaml.j2 @@ -19,6 +19,8 @@ tlsCipherSuites: podPidsLimit: {{ kubernetes_podpidslimit }} # STIG V-242434 protectKernelDefaults: true +# CIS 4.2.14 +seccompDefault: {{ kubernetes_seccomp_default }} --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration @@ -105,6 +107,9 @@ apiServer: - name: service-account-issuer value: "{{ kubernetes_service_account_issuer }}" {% endif %} + # CIS 1.2.30 + - name: service-account-extend-token-expiration + value: "{{ kubernetes_service_account_extend_token_expiration | lower }}" {% if kubernetes_api_server_extra_args is defined and kubernetes_api_server_extra_args | length > 0 %} {% for arg in kubernetes_api_server_extra_args %} - name: "{{ arg.name }}" @@ -203,7 +208,7 @@ skipPhases: {% endfor %} {% endif %} localAPIEndpoint: - advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_host)[0] }}" + advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_facts['fqdn'])[0] }}" bindPort: {{ kubernetes_api_server_port }} nodeRegistration: kubeletExtraArgs: @@ -212,7 +217,7 @@ nodeRegistration: value: "external" {% endif %} - name: node-ip - value: "{{ kubernetes_kubelet_node_ip | default(ansible_default_ipv4.address) }}" + value: "{{ kubernetes_kubelet_node_ip | default(ansible_facts['default_ipv4'].address) }}" patches: directory: "{{ kubernetes_config_directory }}/patches" timeouts: diff --git a/roles/kubernetes-control-plane/templates/kubeadm-init-1.34.yaml.j2 b/roles/kubernetes-control-plane/templates/kubeadm-init-1.34.yaml.j2 index 9550cb4..f3b5516 100644 --- a/roles/kubernetes-control-plane/templates/kubeadm-init-1.34.yaml.j2 +++ b/roles/kubernetes-control-plane/templates/kubeadm-init-1.34.yaml.j2 @@ -19,6 +19,8 @@ tlsCipherSuites: podPidsLimit: {{ kubernetes_podpidslimit }} # STIG V-242434 protectKernelDefaults: true +# CIS 4.2.14 +seccompDefault: {{ kubernetes_seccomp_default }} --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration @@ -105,6 +107,9 @@ apiServer: - name: service-account-issuer value: "{{ kubernetes_service_account_issuer }}" {% endif %} + # CIS 1.2.30 + - name: service-account-extend-token-expiration + value: "{{ kubernetes_service_account_extend_token_expiration | lower }}" {% if kubernetes_api_server_extra_args is defined and kubernetes_api_server_extra_args | length > 0 %} {% for arg in kubernetes_api_server_extra_args %} - name: "{{ arg.name }}" @@ -203,7 +208,7 @@ skipPhases: {% endfor %} {% endif %} localAPIEndpoint: - advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_host)[0] }}" + advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_facts['fqdn'])[0] }}" bindPort: {{ kubernetes_api_server_port }} nodeRegistration: kubeletExtraArgs: @@ -212,7 +217,7 @@ nodeRegistration: value: "external" {% endif %} - name: node-ip - value: "{{ kubernetes_kubelet_node_ip | default(ansible_default_ipv4.address) }}" + value: "{{ kubernetes_kubelet_node_ip | default(ansible_facts['default_ipv4'].address) }}" patches: directory: "{{ kubernetes_config_directory }}/patches" timeouts: diff --git a/roles/kubernetes-control-plane/templates/kubeadm-join-1.33.yaml.j2 b/roles/kubernetes-control-plane/templates/kubeadm-join-1.33.yaml.j2 index dcacf5e..652285c 100644 --- a/roles/kubernetes-control-plane/templates/kubeadm-join-1.33.yaml.j2 +++ b/roles/kubernetes-control-plane/templates/kubeadm-join-1.33.yaml.j2 @@ -19,6 +19,8 @@ tlsCipherSuites: podPidsLimit: {{ kubernetes_podpidslimit }} # STIG V-242434 protectKernelDefaults: true +# CIS 4.2.14 +seccompDefault: {{ kubernetes_seccomp_default }} --- apiVersion: kubeadm.k8s.io/v1beta4 kind: JoinConfiguration @@ -26,11 +28,12 @@ discovery: bootstrapToken: apiServerEndpoint: "{{ kubernetes_api_endpoint }}:{{ kubernetes_api_port }}" token: "{{ join_token }}" - unsafeSkipCAVerification: true + caCertHashes: + - "{{ cluster_ca_cert_hash }}" caCertPath: "{{ kubernetes_pki_directory }}/ca.crt" controlPlane: localAPIEndpoint: - advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_host)[0] }}" + advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_facts['fqdn'])[0] }}" bindPort: {{ kubernetes_api_server_port }} certificateKey: "{{ kubeadm_upload_token }}" nodeRegistration: @@ -40,6 +43,6 @@ nodeRegistration: value: "external" {% endif %} - name: node-ip - value: "{{ kubernetes_kubelet_node_ip | default(ansible_default_ipv4.address) }}" + value: "{{ kubernetes_kubelet_node_ip | default(ansible_facts['default_ipv4'].address) }}" patches: directory: {{ kubernetes_config_directory }}/patches diff --git a/roles/kubernetes-control-plane/templates/kubeadm-join-1.34.yaml.j2 b/roles/kubernetes-control-plane/templates/kubeadm-join-1.34.yaml.j2 index dcacf5e..652285c 100644 --- a/roles/kubernetes-control-plane/templates/kubeadm-join-1.34.yaml.j2 +++ b/roles/kubernetes-control-plane/templates/kubeadm-join-1.34.yaml.j2 @@ -19,6 +19,8 @@ tlsCipherSuites: podPidsLimit: {{ kubernetes_podpidslimit }} # STIG V-242434 protectKernelDefaults: true +# CIS 4.2.14 +seccompDefault: {{ kubernetes_seccomp_default }} --- apiVersion: kubeadm.k8s.io/v1beta4 kind: JoinConfiguration @@ -26,11 +28,12 @@ discovery: bootstrapToken: apiServerEndpoint: "{{ kubernetes_api_endpoint }}:{{ kubernetes_api_port }}" token: "{{ join_token }}" - unsafeSkipCAVerification: true + caCertHashes: + - "{{ cluster_ca_cert_hash }}" caCertPath: "{{ kubernetes_pki_directory }}/ca.crt" controlPlane: localAPIEndpoint: - advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_host)[0] }}" + advertiseAddress: "{{ kubernetes_api_server_bind_address if kubernetes_api_server_bind_address != None else query('community.dns.lookup', ansible_facts['fqdn'])[0] }}" bindPort: {{ kubernetes_api_server_port }} certificateKey: "{{ kubeadm_upload_token }}" nodeRegistration: @@ -40,6 +43,6 @@ nodeRegistration: value: "external" {% endif %} - name: node-ip - value: "{{ kubernetes_kubelet_node_ip | default(ansible_default_ipv4.address) }}" + value: "{{ kubernetes_kubelet_node_ip | default(ansible_facts['default_ipv4'].address) }}" patches: directory: {{ kubernetes_config_directory }}/patches diff --git a/roles/kubernetes-defaults/defaults/main.yml b/roles/kubernetes-defaults/defaults/main.yml index 1944c4c..632ccfb 100644 --- a/roles/kubernetes-defaults/defaults/main.yml +++ b/roles/kubernetes-defaults/defaults/main.yml @@ -345,6 +345,12 @@ kubernetes_scheduler_extra_args: [] # Directory to store scripts for Kubernetes on the nodes kubernetes_scripts_directory: /opt/kubernetes/scripts +# CIS 4.2.14 - Ensure that seccomp is enabled by default in the Kubelet +kubernetes_seccomp_default: true + +# CIS 1.2.30 - Ensure that the --service-account-extend-token-expiration argument is set to false +kubernetes_service_account_extend_token_expiration: false + # Subnet for the cluster services running in the Kubernetes cluster kubernetes_service_subnet: 10.96.0.0/16 diff --git a/roles/kubernetes-worker/tasks/main.yml b/roles/kubernetes-worker/tasks/main.yml index 79f58fe..52700fd 100644 --- a/roles/kubernetes-worker/tasks/main.yml +++ b/roles/kubernetes-worker/tasks/main.yml @@ -11,9 +11,10 @@ ansible.builtin.set_fact: kubernetes_needs_to_join_cluster: "{% if inventory_hostname in kubernetes_nodes.stdout_lines or inventory_hostname_short in kubernetes_nodes.stdout_lines %}{{ 'false' | bool }}{% else %}{{ 'true' | bool }}{% endif %}" -- name: Set join token +- name: Set variables from first control plane ansible.builtin.set_fact: join_token: "{{ hostvars[first_kube_control_plane].join_token }}" + cluster_ca_cert_hash: "{{ hostvars[first_kube_control_plane].cluster_ca_cert_hash }}" - name: Create kubeadm file ansible.builtin.template: diff --git a/roles/kubernetes-worker/templates/worker-kubeadm.yaml.j2 b/roles/kubernetes-worker/templates/worker-kubeadm.yaml.j2 index 59dc0b5..1c14b2b 100644 --- a/roles/kubernetes-worker/templates/worker-kubeadm.yaml.j2 +++ b/roles/kubernetes-worker/templates/worker-kubeadm.yaml.j2 @@ -19,6 +19,8 @@ tlsCipherSuites: podPidsLimit: {{ kubernetes_podpidslimit }} # STIG V-242434 protectKernelDefaults: true +# CIS 4.2.14 +seccompDefault: {{ kubernetes_seccomp_default }} --- apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration @@ -29,8 +31,9 @@ kind: JoinConfiguration discovery: bootstrapToken: apiServerEndpoint: "{{ kubernetes_api_endpoint }}:{{ kubernetes_api_port }}" - token: "{{ join_token}}" - unsafeSkipCAVerification: true + token: "{{ join_token }}" + caCertHashes: + - "{{ cluster_ca_cert_hash }}" nodeRegistration: kubeletExtraArgs: {% if kubernetes_cloud_provider == 'external' %} @@ -38,4 +41,4 @@ nodeRegistration: value: "external" {% endif %} - name: node-ip - value: "{{ kubernetes_kubelet_node_ip | default(ansible_default_ipv4.address) }}" + value: "{{ kubernetes_kubelet_node_ip | default(ansible_facts['default_ipv4'].address) }}"