Skip to content

Commit 10496b3

Browse files
authored
Merge pull request kubernetes#126015 from micahhausler/kubelet-cert-validation
Enhance node admission to validate kubelet CSR's CN
2 parents 558c953 + b251efe commit 10496b3

File tree

3 files changed

+121
-2
lines changed

3 files changed

+121
-2
lines changed

pkg/features/kube_features.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ const (
216216
// Disable in-tree functionality in kubelet to authenticate to cloud provider container registries for image pull credentials.
217217
DisableKubeletCloudCredentialProviders featuregate.Feature = "DisableKubeletCloudCredentialProviders"
218218

219+
// owner: @micahhausler
220+
// Deprecated: v1.31
221+
//
222+
// Disable Node Admission plugin validation of CSRs for kubelet signers where CN=system:node:$nodeName.
223+
// Remove in v1.33
224+
DisableKubeletCSRAdmissionValidation featuregate.Feature = "DisableKubeletCSRAdmissionValidation"
225+
219226
// owner: @HirazawaUi
220227
// kep: http://kep.k8s.io/4004
221228
// alpha: v1.29
@@ -1326,6 +1333,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
13261333
// ...
13271334
HPAScaleToZero: {Default: false, PreRelease: featuregate.Alpha},
13281335

1336+
DisableKubeletCSRAdmissionValidation: {Default: false, PreRelease: featuregate.Deprecated}, // remove in 1.33
1337+
13291338
StorageNamespaceIndex: {Default: true, PreRelease: featuregate.Beta},
13301339

13311340
RecursiveReadOnlyMounts: {Default: true, PreRelease: featuregate.Beta},

plugin/pkg/admission/noderestriction/admission.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
kubeletapis "k8s.io/kubelet/pkg/apis"
3939
podutil "k8s.io/kubernetes/pkg/api/pod"
4040
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
41+
certapi "k8s.io/kubernetes/pkg/apis/certificates"
4142
coordapi "k8s.io/kubernetes/pkg/apis/coordination"
4243
api "k8s.io/kubernetes/pkg/apis/core"
4344
"k8s.io/kubernetes/pkg/apis/policy"
@@ -73,8 +74,9 @@ type Plugin struct {
7374
podsGetter corev1lister.PodLister
7475
nodesGetter corev1lister.NodeLister
7576

76-
expansionRecoveryEnabled bool
77-
dynamicResourceAllocationEnabled bool
77+
expansionRecoveryEnabled bool
78+
dynamicResourceAllocationEnabled bool
79+
kubeletCSRAdmissionValidationDisabled bool
7880
}
7981

8082
var (
@@ -87,6 +89,7 @@ var (
8789
func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
8890
p.expansionRecoveryEnabled = featureGates.Enabled(features.RecoverVolumeExpansionFailure)
8991
p.dynamicResourceAllocationEnabled = featureGates.Enabled(features.DynamicResourceAllocation)
92+
p.kubeletCSRAdmissionValidationDisabled = featureGates.Enabled(features.DisableKubeletCSRAdmissionValidation)
9093
}
9194

9295
// SetExternalKubeInformerFactory registers an informer factory into Plugin
@@ -117,6 +120,7 @@ var (
117120
leaseResource = coordapi.Resource("leases")
118121
csiNodeResource = storage.Resource("csinodes")
119122
resourceSliceResource = resource.Resource("resourceslices")
123+
csrResource = certapi.Resource("certificatesigningrequests")
120124
)
121125

122126
// Admit checks the admission policy and triggers corresponding actions
@@ -171,6 +175,11 @@ func (p *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.
171175
case resourceSliceResource:
172176
return p.admitResourceSlice(nodeName, a)
173177

178+
case csrResource:
179+
if p.kubeletCSRAdmissionValidationDisabled {
180+
return nil
181+
}
182+
return p.admitCSR(nodeName, a)
174183
default:
175184
return nil
176185
}
@@ -670,3 +679,31 @@ func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) err
670679

671680
return nil
672681
}
682+
683+
func (p *Plugin) admitCSR(nodeName string, a admission.Attributes) error {
684+
// Create requests for Kubelet serving signer and Kube API server client
685+
// kubelet signer with a CN that begins with "system:node:" must have a CN
686+
// that is exactly the node's name.
687+
// Other CSR attributes get checked in CSR validation by the signer.
688+
if a.GetOperation() != admission.Create {
689+
return nil
690+
}
691+
692+
csr, ok := a.GetObject().(*certapi.CertificateSigningRequest)
693+
if !ok {
694+
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
695+
}
696+
if csr.Spec.SignerName != certapi.KubeletServingSignerName && csr.Spec.SignerName != certapi.KubeAPIServerClientKubeletSignerName {
697+
return nil
698+
}
699+
700+
x509cr, err := certapi.ParseCSR(csr.Spec.Request)
701+
if err != nil {
702+
return admission.NewForbidden(a, fmt.Errorf("unable to parse csr: %w", err))
703+
}
704+
if x509cr.Subject.CommonName != fmt.Sprintf("system:node:%s", nodeName) {
705+
return admission.NewForbidden(a, fmt.Errorf("can only create a node CSR with CN=system:node:%s", nodeName))
706+
}
707+
708+
return nil
709+
}

plugin/pkg/admission/noderestriction/admission_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ package noderestriction
1818

1919
import (
2020
"context"
21+
"crypto/rand"
22+
"crypto/rsa"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"encoding/pem"
2126
"reflect"
2227
"strings"
2328
"testing"
@@ -41,6 +46,7 @@ import (
4146
"k8s.io/component-base/featuregate"
4247
kubeletapis "k8s.io/kubelet/pkg/apis"
4348
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
49+
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
4450
"k8s.io/kubernetes/pkg/apis/coordination"
4551
api "k8s.io/kubernetes/pkg/apis/core"
4652
"k8s.io/kubernetes/pkg/apis/policy"
@@ -213,11 +219,15 @@ type admitTestCase struct {
213219
nodesGetter corev1lister.NodeLister
214220
attributes admission.Attributes
215221
features featuregate.FeatureGate
222+
setupFunc func(t *testing.T)
216223
err string
217224
}
218225

219226
func (a *admitTestCase) run(t *testing.T) {
220227
t.Run(a.name, func(t *testing.T) {
228+
if a.setupFunc != nil {
229+
a.setupFunc(t)
230+
}
221231
c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier())
222232
if a.features != nil {
223233
c.InspectFeatureGates(a.features)
@@ -375,6 +385,8 @@ func Test_nodePlugin_Admit(t *testing.T) {
375385
}
376386
aLabeledPod = withLabels(coremypod, labelsA)
377387
abLabeledPod = withLabels(coremypod, labelsAB)
388+
389+
privKey, _ = rsa.GenerateKey(rand.Reader, 2048)
378390
)
379391

380392
existingPodsIndex.Add(v1mymirrorpod)
@@ -1238,6 +1250,42 @@ func Test_nodePlugin_Admit(t *testing.T) {
12381250
attributes: admission.NewAttributesRecord(nil, nil, csiNodeKind, nodeInfo.Namespace, nodeInfo.Name, csiNodeResource, "", admission.Delete, &metav1.UpdateOptions{}, false, mynode),
12391251
err: "",
12401252
},
1253+
// CSR
1254+
{
1255+
name: "allowed CSR create correct node serving",
1256+
attributes: createCSRAttributes("system:node:mynode", certificatesapi.KubeletServingSignerName, true, privKey, mynode),
1257+
err: "",
1258+
},
1259+
{
1260+
name: "allowed CSR create correct node client",
1261+
attributes: createCSRAttributes("system:node:mynode", certificatesapi.KubeAPIServerClientKubeletSignerName, true, privKey, mynode),
1262+
err: "",
1263+
},
1264+
{
1265+
name: "allowed CSR create non-node CSR",
1266+
attributes: createCSRAttributes("some-other-identity", certificatesapi.KubeAPIServerClientSignerName, true, privKey, mynode),
1267+
err: "",
1268+
},
1269+
{
1270+
name: "deny CSR create incorrect node",
1271+
attributes: createCSRAttributes("system:node:othernode", certificatesapi.KubeletServingSignerName, true, privKey, mynode),
1272+
err: "forbidden: can only create a node CSR with CN=system:node:mynode",
1273+
},
1274+
{
1275+
name: "allow CSR create incorrect node with feature gate disabled",
1276+
attributes: createCSRAttributes("system:node:othernode", certificatesapi.KubeletServingSignerName, true, privKey, mynode),
1277+
err: "",
1278+
features: feature.DefaultFeatureGate,
1279+
setupFunc: func(t *testing.T) {
1280+
t.Helper()
1281+
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.DisableKubeletCSRAdmissionValidation, true)
1282+
},
1283+
},
1284+
{
1285+
name: "deny CSR create invalid",
1286+
attributes: createCSRAttributes("system:node:mynode", certificatesapi.KubeletServingSignerName, false, privKey, mynode),
1287+
err: "unable to parse csr: asn1: syntax error: sequence truncated",
1288+
},
12411289
}
12421290
for _, tt := range tests {
12431291
tt.nodesGetter = existingNodes
@@ -1603,6 +1651,31 @@ func createPodAttributes(pod *api.Pod, user user.Info) admission.Attributes {
16031651
return admission.NewAttributesRecord(pod, nil, podKind, pod.Namespace, pod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, user)
16041652
}
16051653

1654+
func createCSRAttributes(cn, signer string, validCsr bool, key any, user user.Info) admission.Attributes {
1655+
csrResource := certificatesapi.Resource("certificatesigningrequests").WithVersion("v1")
1656+
csrKind := certificatesapi.Kind("CertificateSigningRequest").WithVersion("v1")
1657+
1658+
csrPem := []byte("-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST-----")
1659+
if validCsr {
1660+
structuredCsr := x509.CertificateRequest{
1661+
Subject: pkix.Name{
1662+
CommonName: cn,
1663+
},
1664+
}
1665+
csrDer, _ := x509.CreateCertificateRequest(rand.Reader, &structuredCsr, key)
1666+
csrPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDer})
1667+
}
1668+
1669+
csreq := &certificatesapi.CertificateSigningRequest{
1670+
Spec: certificatesapi.CertificateSigningRequestSpec{
1671+
Request: csrPem,
1672+
SignerName: signer,
1673+
},
1674+
}
1675+
return admission.NewAttributesRecord(csreq, nil, csrKind, "", "", csrResource, "", admission.Create, &metav1.CreateOptions{}, false, user)
1676+
1677+
}
1678+
16061679
func TestAdmitResourceSlice(t *testing.T) {
16071680
apiResource := resourceapi.SchemeGroupVersion.WithResource("resourceslices")
16081681
nodename := "mynode"

0 commit comments

Comments
 (0)