API Server Service Account Key File Should Be Set
More Info:
Verifies that --service-account-key-file is set so the API server uses a dedicated key to verify service account tokens instead of the TLS serving key.
Risk Level
High
Address
Security
Compliance Standards
- CIS Kubernetes
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
On every control plane node, confirm the current kube-apiserver static pod manifest path and back it up:
sudo ls -l /etc/kubernetes/manifests/kube-apiserver.yamlsudo cp -a /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/manifests/kube-apiserver.yaml.bak.$(date +%F-%H%M%S) -
On every control plane node, ensure you have a dedicated public key file for service accounts (for example
/etc/kubernetes/pki/sa.pub). If you only have a private key (e.g./etc/kubernetes/pki/sa.key), extract or create a matching public key as appropriate for your environment and place it at:sudo ls -l /etc/kubernetes/pki/sa.pub(If the file is missing, follow your cluster’s key-management procedure to generate a service account keypair; this step is environment-specific and cannot be automated generically.)
-
On every control plane node, edit the kube-apiserver static pod manifest to add the
--service-account-key-fileflag pointing to the public key file. Use a text editor:sudo vi /etc/kubernetes/manifests/kube-apiserver.yamlUnder the
command:section forkube-apiserver, add a line similar to:- --service-account-key-file=/etc/kubernetes/pki/sa.pubKeep the YAML indentation consistent with the existing list of
- --...flags. -
On every control plane node, if the public key file is not already mounted into the kube-apiserver container, add a corresponding
volumeMountsentry andvolumesentry in/etc/kubernetes/manifests/kube-apiserver.yamlso the container can read/etc/kubernetes/pki/sa.pub. For example, in the container spec:volumeMounts:- mountPath: /etc/kubernetes/pkiname: k8s-certsreadOnly: trueAnd in the pod-level
volumes::volumes:- name: k8s-certshostPath:path: /etc/kubernetes/pkitype: DirectoryOrCreate(Adjust names if a similar volume already exists; do not create duplicates.)
-
On every control plane node, save the file and exit the editor. The kubelet will automatically detect the manifest change and restart the kube-apiserver static pod; expect a brief control-plane disruption during the restart.
-
On every control plane node, verify that the kube-apiserver is now running with the
--service-account-key-fileargument set to the intended public key file:/bin/ps -ef | grep kube-apiserver | grep -v grepConfirm the output includes a flag like:
--service-account-key-file=/etc/kubernetes/pki/sa.pub
Using kubectl
kubectl cannot modify the kube-apiserver static pod manifest or its process flags, so this finding cannot be fixed via the Kubernetes API. The required change must be made directly on each control plane node by editing /etc/kubernetes/manifests/kube-apiserver.yaml; see the Manual Steps section for exact host-level instructions.
Automation
#!/usr/bin/env bash
#
# Remediation: Ensure kube-apiserver --service-account-key-file is set
# Scope: Run on every control plane node (with root privileges)
#
# Behavior:
# - Creates a dedicated service account keypair under /etc/kubernetes/pki if missing
# - Ensures /etc/kubernetes/manifests/kube-apiserver.yaml has --service-account-key-file flag
# - Uses the public key as the flag value
# - Idempotent: safe to re-run
# - Editing the manifest will restart the kube-apiserver static pod
set -euo pipefail
APISERVER_MANIFEST="/etc/kubernetes/manifests/kube-apiserver.yaml"
PKI_DIR="/etc/kubernetes/pki"
SA_KEY="${PKI_DIR}/sa.key"
SA_PUB="${PKI_DIR}/sa.pub"
BACKUP_SUFFIX="$(date +%Y%m%d%H%M%S)"
echo "[INFO] Running on control plane node: configuring kube-apiserver --service-account-key-file"
if [[ $EUID -ne 0 ]]; then
echo "[ERROR] This script must be run as root on each control plane node." >&2
exit 1
fi
if [[ ! -f "${APISERVER_MANIFEST}" ]]; then
echo "[ERROR] kube-apiserver manifest not found at ${APISERVER_MANIFEST}" >&2
exit 1
fi
mkdir -p "${PKI_DIR}"
chmod 700 "${PKI_DIR}"
# Generate service account keypair if missing
if [[ ! -f "${SA_KEY}" || ! -f "${SA_PUB}" ]]; then
echo "[INFO] Generating service account keypair in ${PKI_DIR}"
openssl genrsa -out "${SA_KEY}" 2048
chmod 600 "${SA_KEY}"
openssl rsa -in "${SA_KEY}" -pubout -out "${SA_PUB}"
chmod 644 "${SA_PUB}"
else
echo "[INFO] Existing service account keypair found: ${SA_KEY}, ${SA_PUB}"
fi
# Backup manifest once per run
cp "${APISERVER_MANIFEST}" "${APISERVER_MANIFEST}.bak.${BACKUP_SUFFIX}"
# Ensure --service-account-key-file flag is present and points to the public key
if grep -q -- "--service-account-key-file" "${APISERVER_MANIFEST}"; then
echo "[INFO] Existing --service-account-key-file flag found, updating path to ${SA_PUB}"
# Replace existing value (handles both --flag=val and '--flag val' styles in args list)
# For YAML args list with ' - --service-account-key-file=...'
sed -i \
"s#^\(\s*-\s*--service-account-key-file\)=\?.*#\1=${SA_PUB}#g" \
"${APISERVER_MANIFEST}"
else
echo "[INFO] Adding --service-account-key-file flag with value ${SA_PUB}"
# Insert the flag under the 'command:' or 'args:' section.
# This assumes a standard kubeadm-style static pod manifest where args are a list.
# We append the new arg line after the first occurrence of 'kube-apiserver' container args.
if grep -q "^- kube-apiserver" "${APISERVER_MANIFEST}"; then
# kubeadm-style: command as list, args as list under container
awk -v sa_pub="${SA_PUB}" '
/- kube-apiserver/ { in_container=1 }
in_container && /args:/ && !added {
print $0
getline
# print the next line (first arg) and then our new arg
print $0
sub(/^[[:space:]]*-/,"& --service-account-key-file=" sa_pub) # no-op placeholder
added=1
next
}
{ print $0 }
' "${APISERVER_MANIFEST}" > "${APISERVER_MANIFEST}.tmp" || true
if ! grep -q "--service-account-key-file" "${APISERVER_MANIFEST}.tmp" 2>/dev/null; then
# Fallback: simple append under 'args:' if awk method failed/mismatched
sed -i "/args:/a\ - --service-account-key-file=${SA_PUB}" "${APISERVER_MANIFEST}"
else
mv "${APISERVER_MANIFEST}.tmp" "${APISERVER_MANIFEST}"
fi
else
# Generic fallback: append under first 'args:' occurrence
sed -i "/args:/a\ - --service-account-key-file=${SA_PUB}" "${APISERVER_MANIFEST}"
fi
fi
echo "[INFO] Updated ${APISERVER_MANIFEST}. kubelet will restart the kube-apiserver static pod."
# Wait for kube-apiserver process to restart and pick up new flag
echo "[INFO] Waiting up to 60 seconds for kube-apiserver to reflect new flag..."
end=$((SECONDS+60))
while (( SECONDS < end )); do
if /bin/ps -ef | grep kube-apiserver | grep -v grep | grep -q -- "--service-account-key-file=${SA_PUB}"; then
break
fi
sleep 3
done
# Verification
echo "[INFO] Verifying remediation with audit command..."
if /bin/ps -ef | grep kube-apiserver | grep -v grep | grep -q -- "--service-account-key-file=${SA_PUB}"; then
echo "[SUCCESS] kube-apiserver is running with --service-account-key-file=${SA_PUB}"
exit 0
else
echo "[WARNING] kube-apiserver process does not yet show --service-account-key-file=${SA_PUB}" >&2
echo "[WARNING] Check kubelet and static pod status; verify ${APISERVER_MANIFEST} contents." >&2
/bin/ps -ef | grep kube-apiserver | grep -v grep || true
exit 1
fi