Kubelet CSR approver is a Kubernetes controller whose sole purpose is to
auto-approve kubelet-serving Certificate Signing Request
(CSR),
provided these CSRs comply with a series of configurable, provider-specific,
checks/verifications.
Inspired by existing projects (such as
kubelet-rubber-stamp), it
implements additional verifications to prevent an attacker from forging
Certificates.
Kubelet CSR approver is being kept up-to-date in accordance with the most recent three Kubernetes minor releases.
- deploy
kubelet-csr-approveron your k8s cluster using the manifests present indeploy/k8s - change the
/var/lib/kubelet/config.yamlfile and restart your kubelet once having included the following field:yaml serverTLSBootstrap: true - at that point, there should be a number of CSRs on your cluster, that the
kubelet-csr-approverwill approve (or deny) depending on the deployment parameters you have set.
The most important parameters (configurable through either flags or environment variables) are:
--provider-regexorPROVIDER_REGEXlets you decide which hostnames can be approved or not
e.g. if all your nodes follow a naming convention (saynode-randomstr1234.int.company.ch), your regex could look like^node-\w*\.int\.company\.ch$--max-expiration-secorMAX_EXPIRATION_SEClets you specify the maximumexpirationSecondsthe kubelet can ask for.
Per default it is hardcoded to a maximum of 367 days, and can be reduced with this parameter.--bypass-dns-resolutionorBYPASS_DNS_RESOLUTION-> permits to bypass DNS resolution check.
the default value of the boolean is false, and you can enable it by setting it totrue(or any other option listed in GoLang'sParseBoolfunction)--bypass-hostname-checkorBYPASS_HOSTNAME_CHECK: when set to true, it permits having a DNS name that differs (i.e. isn't prefixed) by the hostname--provider-ip-prefixesorPROVIDER_IP_PREFIXESpermits to specify a comma-separated list of IP (v4 or/and v6) subnets/prefixes, that CSR IP addresses shall fall into. left unspecified, all IP addresses are allowed.
you can for example set it to192.168.0.0/16,fc00::/7if this reflects your local network IP ranges.--ignore-non-system-nodeorIGNORE_NON_SYSTEM_NODEpermits ignoring CSRs with a Username different thansystem:node:.......
the default value of the boolean is false, and if you want to use this feature you need to set this flag totrue--skip-deny-steporSKIP_DENY_STEPpermits skipping denial of CSRs. when set to true, kubelet-csr-approver will only ever approve a CSR, never deny it.--allowed-dns-namesorALLOWED_DNS_NAMESpermits allowing more than one DNS name in the certificate request. the default value is set to 1.--leader-electionorLEADER_ELECTIONpermits enabling leader election when running with multiple replicas
It is important to understand that the node DNS name needs to be
resolvable for the kubelet-csr-approver to work properly. If this is an issue
for you, you can use the --bypass-dns-resolution flag, which will disable the DNS
check altogether.
ℹ have a look below in this README to understand which other validation mechanisms are put in place.
Adjust providerRegex, providerIpPrefixes and maxExpirationSeconds as needed.
helm repo add kubelet-csr-approver https://postfinance.github.io/kubelet-csr-approver
helm install kubelet-csr-approver kubelet-csr-approver/kubelet-csr-approver -n kube-system \
--set providerRegex='^node-\w*\.int\.company\.ch$' \
--set providerIpPrefixes='192.168.8.0/22' \
--set maxExpirationSeconds='86400' \
--set bypassDnsResolution='false'Shall our CSR auto-approver not be implemented correctly, it might permit an attacker to get forged CSRs to be approved and later on signed by the K8s certificate controller.
Indeed, while there are some verifications done by the final certificate signer
controller (for the details,
here
and
here),
nothing prevents a CSR impersonating a DNSName or an IPAddress from getting
signed.
Put more concretely, if a CSR requesting the DNS name auth.company.com or
control-plane.k8s.local (or both!) was to be approved, it would get signed
and the attacker would have a very valid certificate to make use of. (and
depending on the ca.key used on your cluster, this could have a measurable
impact)
Taking inspiration from Kubernetes built-in CSR approver, we check the following criteria:
CSR.Spec.SignerNamemust be"kubernetes.io/kubelet-serving"CSR.Spec.ExpirationSeconds, if specified, must be smaller thanMAX_EXPIRATION_SEC
(the default value and hard-coded maximum for this controller is 367 days)CSR.Spec.Usernamemust be prefixed withsystem:node:(i.e. we only want to treat CSRs originating from the nodes themselves)- x509 CR
CommonNamemust be equal to theCSR.Spec.Username - CSR DNS SubjectAlternativeNames (SAN) contains at most one entry
- at least one SAN IP address or SAN DNS Name must be specified
- CSR SAN DNS Name (if specified) must comply with a provider-specific regex.
- CSR SAN DNS Name (if specified) must be prefixed with the node hostname
(where the hostname corresponds to
CSR.Spec.Usernametrimmed of thesystem:node:prefix) - CSR SAN IP Addresses must all be part of the set of IP addresses resolved from the SAN DNS Name
- the CSR SAN DNS Name (if specified) must resolve to IP address(es) that fall within the set of provider-specified IP ranges.
- the CSR SAN IP Address(es) must fall within a set of provider-specified IP ranges
With those verifications in place, it makes it quite hard for an attacker to get a forged hostname to be signed, it would indeed require:
- to impersonate a user on the Kubernetes API server with a
Usernamethat prefixes the SAN DNS Name request. \ concretely, if the attacker wants to forge a CSR for theauth.company.chdomain, s/he would need to create a CSR with the usernamesystem:node:a(remember, we only check the that the DNS name is prefixed by the node name) \ it might then be possible to create a CSR from a nodea(not a smart name for a node, I agree), or a nodeauth, already more plausible - to modify the provider-specific regex of the
kubelet-csr-approver(requires API access or direct access to the node where the controller is running).
however with API access, the attacker could as well also directly approve the CSR, and with full node access, the attacker could retrieve the controller's ServiceAccount and approve the CSR as well.
Provided that the provider-specific regex is strict, that the IP ranges set is correctly specified, that the DNS system is not compromised, this automatic CSR approver would make it quite hard for an attacker to start forging CSRs.
For sure, this simply requires modifying the ProviderChecks(csr , x509csr))
function to implement additional checks (such as validating the node identity
in an external inventory)
When building locally to run the CSR approver on an actual cluster with e.g. the
oidc authentication provider, you need to use the tag debug to import all
authentication providers. You will then build as follows:
go build -tags debug ./cmd/kubelet-csr-approver/