---
controls:
version: rh-1.0
id: 4
text: "Worker Node Security Configuration"
type: "node"
groups:
  - id: 4.1
    text: "Worker Node Configuration Files"
    checks:
      - id: 4.1.1
        text: "Ensure that the kubelet service file permissions are set to 644 or more restrictive (Automated)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n permissions=%a" /etc/systemd/system/kubelet.service 2> /dev/null
        tests:
          test_items:
            - flag: "permissions"
              compare:
                op: bitmask
                value: "644"
        remediation: |
          By default, the kubelet service file has permissions of 644.
        scored: true

      - id: 4.1.2
        text: "Ensure that the kubelet service file ownership is set to root:root (Automated)"
        audit: |
          # Should return root:root for each node
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n %U:%G" /etc/systemd/system/kubelet.service 2> /dev/null
        tests:
          test_items:
            - flag: root:root
        remediation: |
          By default, the kubelet service file has ownership of root:root.
        scored: true

      - id: 4.1.3
        text: "If proxy kubeconfig file exists ensure permissions are set to 644 or more restrictive (Manual)"
        audit: |
          # Get the node name where the pod is running
          NODE_NAME=$(oc get pod "$HOSTNAME" -o=jsonpath='{.spec.nodeName}')
          # Get the pod name in the openshift-sdn namespace
          POD_NAME=$(oc get pods -n openshift-sdn -l app=sdn --field-selector spec.nodeName="$NODE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)

          if [ -z "$POD_NAME" ]; then
          echo "No matching pods found on the current node."
          else
          # Execute the stat command
          oc exec -n openshift-sdn "$POD_NAME" -- stat -Lc "$i %n permissions=%a" /config/kube-proxy-config.yaml  2>/dev/null
          fi
        tests:
          bin_op: or
          test_items:
            - flag: "permissions"
              set: true
              compare:
                op: bitmask
                value: "644"
        remediation: |
          None needed.
        scored: false

      - id: 4.1.4
        text: "Ensure that the proxy kubeconfig file ownership is set to root:root (Manual)"
        audit: |
          # Get the node name where the pod is running
          NODE_NAME=$(oc get pod "$HOSTNAME" -o=jsonpath='{.spec.nodeName}')
          # Get the pod name in the openshift-sdn namespace
          POD_NAME=$(oc get pods -n openshift-sdn -l app=sdn --field-selector spec.nodeName="$NODE_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)

          if [ -z "$POD_NAME" ]; then
          echo "No matching pods found on the current node."
          else
          # Execute the stat command
          oc exec -n openshift-sdn "$POD_NAME"  -- stat -Lc "$i %n %U:%G" /config/kube-proxy-config.yaml  2>/dev/null
          fi
        use_multiple_values: true
        tests:
          bin_op: or
          test_items:
            - flag: root:root
        remediation: |
          None required. The configuration is managed by OpenShift operators.
        scored: false

      - id: 4.1.5
        text: "Ensure that the --kubeconfig kubelet.conf file permissions are set to 644 or more restrictive (Manual)"
        audit: |
          # Check permissions
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n permissions=%a" /etc/kubernetes/kubelet.conf 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: "permissions"
              compare:
                op: bitmask
                value: "644"
        remediation: |
          None required.
        scored: false

      - id: 4.1.6
        text: "Ensure that the --kubeconfig kubelet.conf file ownership is set to root:root (Manual)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n %U:%G" /etc/kubernetes/kubelet.conf 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: root:root
        remediation: |
          None required.
        scored: false

      - id: 4.1.7
        text: "Ensure that the certificate authorities file permissions are set to 644 or more restrictive (Automated)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n permissions=%a" /etc/kubernetes/kubelet-ca.crt 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: "permissions"
              compare:
                op: bitmask
                value: "644"
        remediation: |
          None required.
        scored: true

      - id: 4.1.8
        text: "Ensure that the client certificate authorities file ownership is set to root:root (Automated)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n %U:%G" /etc/kubernetes/kubelet-ca.crt 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: root:root
        remediation: |
          None required.
        scored: true

      - id: 4.1.9
        text: "Ensure that the kubelet --config configuration file has permissions set to 644 or more restrictive (Automated)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n permissions=%a" /var/lib/kubelet/kubeconfig 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: "permissions"
              compare:
                op: bitmask
                value: "644"
        remediation: |
          None required.
        scored: true

      - id: 4.1.10
        text: "Ensure that the kubelet configuration file ownership is set to root:root (Automated)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host stat -c "$NODE_NAME %n %U:%G" /var/lib/kubelet/kubeconfig 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: root:root
        remediation: |
          None required.
        scored: true

  - id: 4.2
    text: "Kubelet"
    checks:
      - id: 4.2.1
        text: "Ensure that the --anonymous-auth argument is set to false (Automated)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host grep -B4 -A1 anonymous /etc/kubernetes/kubelet.conf 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: "enabled: true"
              set: false
        remediation: |
          Follow the instructions in the documentation to create a Kubelet config CRD
          and set the anonymous-auth is set to false.
        scored: true

      - id: 4.2.2
        text: "Ensure that the --authorization-mode argument is not set to AlwaysAllow (Manual)"
        type: manual
        # Takes a lot of time for connection to fail and
        audit: |
          POD=$(oc -n openshift-kube-apiserver get pod -l app=openshift-kube-apiserver -o jsonpath='{.items[0].metadata.name}')
          TOKEN=$(oc whoami -t)
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc exec -n openshift-kube-apiserver $POD -- curl -sS https://172.25.0.1/api/v1/nodes/$NODE_NAME/proxy/configz -k -H "Authorization:Bearer $TOKEN" | jq -r '.kubeletconfig.authorization.mode' 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: "Connection timed out"
        remediation: |
          None required. Unauthenticated/Unauthorized users have no access to OpenShift nodes.
        scored: false

      - id: 4.2.3
        text: "Ensure that the --client-ca-file argument is set as appropriate (Automated)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host grep clientCAFile /etc/kubernetes/kubelet.conf 2> /dev/null
        use_multiple_values: true
        tests:
          test_items:
            - flag: '"clientCAFile": "/etc/kubernetes/kubelet-ca.crt"'
        remediation: |
          None required. Changing the clientCAFile value is unsupported.
        scored: true

      - id: 4.2.4
        text: "Verify that the read only port is not used or is set to 0 (Automated)"
        audit: |
          echo `oc -n openshift-kube-apiserver get cm kube-apiserver-pod -o yaml | grep --color read-only-port` 2> /dev/null
          echo `oc -n openshift-kube-apiserver get cm config -o yaml | grep --color "read-only-port"` 2> /dev/null
        tests:
          bin_op: or
          test_items:
            - flag: "read-only-port"
              compare:
                op: has
                value: "[\"0\"]"
            - flag: "read-only-port"
              set: false
        remediation: |
          In earlier versions of OpenShift 4, the read-only-port argument is not used.
          Follow the instructions in the documentation to create a Kubelet config CRD
          and set the --read-only-port is set to 0.
        scored: true

      - id: 4.2.5
        text: "Ensure that the --streaming-connection-idle-timeout argument is not set to 0 (Automated)"
        audit: |
          # Should return 1 for node
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/${NODE_NAME} -- chroot /host ps -ef | grep kubelet | grep streaming-connection-idle-timeout 2> /dev/null
          echo exit_code=$?
          # Should return 1 for node
          oc debug node/${NODE_NAME} -- chroot /host grep streamingConnectionIdleTimeout /etc/kubernetes/kubelet.conf 2> /dev/null
          echo exit_code=$?
        use_multiple_values: true
        tests:
          bin_op: or
          test_items:
            - flag: --streaming-connection-idle-timeout
              compare:
                op: noteq
                value: 0
            - flag: streamingConnectionIdleTimeout
              compare:
                op: noteq
                value: 0s
            - flag: "exit_code"
              compare:
                op: eq
                value: 1
        remediation: |
          Follow the instructions in the documentation to create a Kubelet config CRD and set
          the --streaming-connection-idle-timeout to the desired value. Do not set the value to 0.
        scored: true

      - id: 4.2.6
        text: "Ensure that the --protect-kernel-defaults argument is not set (Manual)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/$NODE_NAME -- chroot /host more /etc/kubernetes/kubelet.conf 2> /dev/null
        tests:
          test_items:
            - flag: protectKernelDefaults
              set: false
        remediation: |
          None required. The OpenShift 4 kubelet modifies the system tunable;
          using the protect-kernel-defaults flag will cause the kubelet to fail on start if the tunables
          don't match the kubelet configuration and the OpenShift node will fail to start.
        scored: false

      - id: 4.2.7
        text: "Ensure that the --make-iptables-util-chains argument is set to true (Manual)"
        audit: |
          /bin/bash
          flag=make-iptables-util-chains
          opt=makeIPTablesUtilChains
          # look at each machineconfigpool
          while read -r pool nodeconfig; do
            # true by default
            value='true'
            # first look for the flag
            oc get machineconfig $nodeconfig -o json | jq -r '.spec.config.systemd[][] | select(.name=="kubelet.service") | .contents' | sed -n "/^ExecStart=/,/^\$/ { /^\\s*--$flag=false/ q 100 }"
            # if the above command exited with 100, the flag was false
            [ $? == 100 ] && value='false'
            # now look in the yaml KubeletConfig
            yamlconfig=$(oc get machineconfig $nodeconfig -o json | jq -r '.spec.config.storage.files[] | select(.path=="/etc/kubernetes/kubelet.conf") | .contents.source ' | sed 's/^data:,//' | while read; do echo -e ${REPLY//%/\\x}; done)
            echo "$yamlconfig" | sed -n "/^$opt:\\s*false\\s*$/ q 100"
            [ $? == 100 ] && value='false'
            echo "Pool $pool has $flag ($opt) set to $value"
          done < <(oc get machineconfigpools -o json | jq -r '.items[] | select(.status.machineCount>0) | .metadata.name + " " + .spec.configuration.name')
        use_multiple_values: true
        tests:
          test_items:
            - flag: "set to true"
        remediation: |
          None required. The --make-iptables-util-chains argument is set to true by default.
        scored: false

      - id: 4.2.8
        text: "Ensure that the --hostname-override argument is not set (Manual)"
        audit: |
          echo `oc get machineconfig 01-worker-kubelet -o yaml | grep hostname-override`
          echo `oc get machineconfig 01-master-kubelet -o yaml | grep hostname-override`
        tests:
          test_items:
            - flag: hostname-override
              set: false
        remediation: |
          By default, --hostname-override argument is not set.
        scored: false

      - id: 4.2.9
        text: "Ensure that the kubeAPIQPS [--event-qps] argument is set to 0 or a level which ensures appropriate event capture (Manual)"
        audit: |
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/${NODE_NAME} -- chroot /host cat /etc/kubernetes/kubelet.conf;
          oc get machineconfig 01-worker-kubelet -o yaml | grep --color kubeAPIQPS%3A%2050
          oc get machineconfig 01-master-kubelet -o yaml | grep --color kubeAPIQPS%3A%2050
        type: "manual"
        remediation: |
          Follow the documentation to edit kubelet parameters
          https://docs.openshift.com/container-platform/4.5/scalability_and_performance/recommended-host-practices.html#create-a-kubeletconfig-crd-to-edit-kubelet-parameters
          KubeAPIQPS: <QPS>
        scored: false

      - id: 4.2.10
        text: "Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate (Automated)"
        audit: |
          oc get configmap config -n openshift-kube-apiserver -o json \
            | jq -r '.data["config.yaml"]' \
            | jq -r '.apiServerArguments |
                .["kubelet-client-certificate"][0],
                .["kubelet-client-key"][0]
              '
        tests:
          bin_op: and
          test_items:
            - flag: "/etc/kubernetes/static-pod-certs/secrets/kubelet-client/tls.crt"
            - flag: "/etc/kubernetes/static-pod-certs/secrets/kubelet-client/tls.key"
        remediation: |
          OpenShift automatically manages TLS authentication for the API server communication with the node/kublet.
          This is not configurable.
        scored: true

      - id: 4.2.11
        text: "Ensure that the --rotate-certificates argument is not set to false (Manual)"
        audit: |
          #Verify the rotateKubeletClientCertificate feature gate is not set to false
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/${NODE_NAME} -- chroot /host cat /etc/kubernetes/kubelet.conf | grep RotateKubeletClientCertificate 2> /dev/null
          # Verify the rotateCertificates argument is set to true
          oc debug node/${NODE_NAME} -- chroot host grep rotate /etc/kubernetes/kubelet.conf 2> /dev/null
        use_multiple_values: true
        tests:
          bin_op: or
          test_items:
            - flag: rotateCertificates
              compare:
                op: eq
                value: true
            - flag: rotateKubeletClientCertificates
              compare:
                op: noteq
                value: false
            - flag: rotateKubeletClientCertificates
              set: false
        remediation: |
          None required.
        scored: false

      - id: 4.2.12
        text: "Verify that the RotateKubeletServerCertificate argument is set to true (Manual)"
        audit: |
          #Verify the rotateKubeletServerCertificate feature gate is on
          NODE_NAME=$(oc get pod $HOSTNAME -o=jsonpath='{.spec.nodeName}')
          oc debug node/${NODE_NAME} -- chroot /host grep RotateKubeletServerCertificate /etc/kubernetes/kubelet.conf 2> /dev/null
          # Verify the rotateCertificates argument is set to true
          oc debug node/${NODE_NAME} -- chroot host grep rotate /etc/kubernetes/kubelet.conf 2> /dev/null
        use_multiple_values: true
        tests:
          bin_op: or
          test_items:
            - flag: rotateCertificates
              compare:
                op: eq
                value: true
            - flag: RotateKubeletServerCertificate
              compare:
                op: eq
                value: true
        remediation: |
          By default, kubelet server certificate rotation is disabled.
        scored: false

      - id: 4.2.13
        text: "Ensure that the Kubelet only makes use of Strong Cryptographic Ciphers (Manual)"
        audit: |
          # needs verification
          # verify cipher suites
          oc describe --namespace=openshift-ingress-operator ingresscontroller/default
          oc get kubeapiservers.operator.openshift.io cluster -o json |jq .spec.observedConfig.servingInfo
          oc get openshiftapiservers.operator.openshift.io cluster -o json |jq .spec.observedConfig.servingInfo
          oc get cm -n openshift-authentication v4-0-config-system-cliconfig -o jsonpath='{.data.v4\-0\-config\-system\-cliconfig}' | jq .servingInfo
          #check value for tlsSecurityProfile; null is returned if default is used
          oc get kubeapiservers.operator.openshift.io cluster -o json |jq .spec.tlsSecurityProfile
        type: manual
        remediation: |
          Follow the directions above and in the OpenShift documentation to configure the tlsSecurityProfile.
          Configuring Ingress
        scored: false
