mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Merge pull request #126015 from micahhausler/kubelet-cert-validation
Enhance node admission to validate kubelet CSR's CN
This commit is contained in:
commit
10496b35a8
@ -216,6 +216,13 @@ const (
|
|||||||
// Disable in-tree functionality in kubelet to authenticate to cloud provider container registries for image pull credentials.
|
// Disable in-tree functionality in kubelet to authenticate to cloud provider container registries for image pull credentials.
|
||||||
DisableKubeletCloudCredentialProviders featuregate.Feature = "DisableKubeletCloudCredentialProviders"
|
DisableKubeletCloudCredentialProviders featuregate.Feature = "DisableKubeletCloudCredentialProviders"
|
||||||
|
|
||||||
|
// owner: @micahhausler
|
||||||
|
// Deprecated: v1.31
|
||||||
|
//
|
||||||
|
// Disable Node Admission plugin validation of CSRs for kubelet signers where CN=system:node:$nodeName.
|
||||||
|
// Remove in v1.33
|
||||||
|
DisableKubeletCSRAdmissionValidation featuregate.Feature = "DisableKubeletCSRAdmissionValidation"
|
||||||
|
|
||||||
// owner: @HirazawaUi
|
// owner: @HirazawaUi
|
||||||
// kep: http://kep.k8s.io/4004
|
// kep: http://kep.k8s.io/4004
|
||||||
// alpha: v1.29
|
// alpha: v1.29
|
||||||
@ -1326,6 +1333,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
|||||||
// ...
|
// ...
|
||||||
HPAScaleToZero: {Default: false, PreRelease: featuregate.Alpha},
|
HPAScaleToZero: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
|
||||||
|
DisableKubeletCSRAdmissionValidation: {Default: false, PreRelease: featuregate.Deprecated}, // remove in 1.33
|
||||||
|
|
||||||
StorageNamespaceIndex: {Default: true, PreRelease: featuregate.Beta},
|
StorageNamespaceIndex: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|
||||||
RecursiveReadOnlyMounts: {Default: true, PreRelease: featuregate.Beta},
|
RecursiveReadOnlyMounts: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
@ -38,6 +38,7 @@ import (
|
|||||||
kubeletapis "k8s.io/kubelet/pkg/apis"
|
kubeletapis "k8s.io/kubelet/pkg/apis"
|
||||||
podutil "k8s.io/kubernetes/pkg/api/pod"
|
podutil "k8s.io/kubernetes/pkg/api/pod"
|
||||||
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
||||||
|
certapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||||
coordapi "k8s.io/kubernetes/pkg/apis/coordination"
|
coordapi "k8s.io/kubernetes/pkg/apis/coordination"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/policy"
|
"k8s.io/kubernetes/pkg/apis/policy"
|
||||||
@ -73,8 +74,9 @@ type Plugin struct {
|
|||||||
podsGetter corev1lister.PodLister
|
podsGetter corev1lister.PodLister
|
||||||
nodesGetter corev1lister.NodeLister
|
nodesGetter corev1lister.NodeLister
|
||||||
|
|
||||||
expansionRecoveryEnabled bool
|
expansionRecoveryEnabled bool
|
||||||
dynamicResourceAllocationEnabled bool
|
dynamicResourceAllocationEnabled bool
|
||||||
|
kubeletCSRAdmissionValidationDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -87,6 +89,7 @@ var (
|
|||||||
func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||||
p.expansionRecoveryEnabled = featureGates.Enabled(features.RecoverVolumeExpansionFailure)
|
p.expansionRecoveryEnabled = featureGates.Enabled(features.RecoverVolumeExpansionFailure)
|
||||||
p.dynamicResourceAllocationEnabled = featureGates.Enabled(features.DynamicResourceAllocation)
|
p.dynamicResourceAllocationEnabled = featureGates.Enabled(features.DynamicResourceAllocation)
|
||||||
|
p.kubeletCSRAdmissionValidationDisabled = featureGates.Enabled(features.DisableKubeletCSRAdmissionValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetExternalKubeInformerFactory registers an informer factory into Plugin
|
// SetExternalKubeInformerFactory registers an informer factory into Plugin
|
||||||
@ -117,6 +120,7 @@ var (
|
|||||||
leaseResource = coordapi.Resource("leases")
|
leaseResource = coordapi.Resource("leases")
|
||||||
csiNodeResource = storage.Resource("csinodes")
|
csiNodeResource = storage.Resource("csinodes")
|
||||||
resourceSliceResource = resource.Resource("resourceslices")
|
resourceSliceResource = resource.Resource("resourceslices")
|
||||||
|
csrResource = certapi.Resource("certificatesigningrequests")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Admit checks the admission policy and triggers corresponding actions
|
// 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.
|
|||||||
case resourceSliceResource:
|
case resourceSliceResource:
|
||||||
return p.admitResourceSlice(nodeName, a)
|
return p.admitResourceSlice(nodeName, a)
|
||||||
|
|
||||||
|
case csrResource:
|
||||||
|
if p.kubeletCSRAdmissionValidationDisabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.admitCSR(nodeName, a)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -670,3 +679,31 @@ func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) err
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) admitCSR(nodeName string, a admission.Attributes) error {
|
||||||
|
// Create requests for Kubelet serving signer and Kube API server client
|
||||||
|
// kubelet signer with a CN that begins with "system:node:" must have a CN
|
||||||
|
// that is exactly the node's name.
|
||||||
|
// Other CSR attributes get checked in CSR validation by the signer.
|
||||||
|
if a.GetOperation() != admission.Create {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, ok := a.GetObject().(*certapi.CertificateSigningRequest)
|
||||||
|
if !ok {
|
||||||
|
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
||||||
|
}
|
||||||
|
if csr.Spec.SignerName != certapi.KubeletServingSignerName && csr.Spec.SignerName != certapi.KubeAPIServerClientKubeletSignerName {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
x509cr, err := certapi.ParseCSR(csr.Spec.Request)
|
||||||
|
if err != nil {
|
||||||
|
return admission.NewForbidden(a, fmt.Errorf("unable to parse csr: %w", err))
|
||||||
|
}
|
||||||
|
if x509cr.Subject.CommonName != fmt.Sprintf("system:node:%s", nodeName) {
|
||||||
|
return admission.NewForbidden(a, fmt.Errorf("can only create a node CSR with CN=system:node:%s", nodeName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -18,6 +18,11 @@ package noderestriction
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -41,6 +46,7 @@ import (
|
|||||||
"k8s.io/component-base/featuregate"
|
"k8s.io/component-base/featuregate"
|
||||||
kubeletapis "k8s.io/kubelet/pkg/apis"
|
kubeletapis "k8s.io/kubelet/pkg/apis"
|
||||||
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
|
||||||
|
certificatesapi "k8s.io/kubernetes/pkg/apis/certificates"
|
||||||
"k8s.io/kubernetes/pkg/apis/coordination"
|
"k8s.io/kubernetes/pkg/apis/coordination"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/policy"
|
"k8s.io/kubernetes/pkg/apis/policy"
|
||||||
@ -213,11 +219,15 @@ type admitTestCase struct {
|
|||||||
nodesGetter corev1lister.NodeLister
|
nodesGetter corev1lister.NodeLister
|
||||||
attributes admission.Attributes
|
attributes admission.Attributes
|
||||||
features featuregate.FeatureGate
|
features featuregate.FeatureGate
|
||||||
|
setupFunc func(t *testing.T)
|
||||||
err string
|
err string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *admitTestCase) run(t *testing.T) {
|
func (a *admitTestCase) run(t *testing.T) {
|
||||||
t.Run(a.name, func(t *testing.T) {
|
t.Run(a.name, func(t *testing.T) {
|
||||||
|
if a.setupFunc != nil {
|
||||||
|
a.setupFunc(t)
|
||||||
|
}
|
||||||
c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier())
|
c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier())
|
||||||
if a.features != nil {
|
if a.features != nil {
|
||||||
c.InspectFeatureGates(a.features)
|
c.InspectFeatureGates(a.features)
|
||||||
@ -375,6 +385,8 @@ func Test_nodePlugin_Admit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
aLabeledPod = withLabels(coremypod, labelsA)
|
aLabeledPod = withLabels(coremypod, labelsA)
|
||||||
abLabeledPod = withLabels(coremypod, labelsAB)
|
abLabeledPod = withLabels(coremypod, labelsAB)
|
||||||
|
|
||||||
|
privKey, _ = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
)
|
)
|
||||||
|
|
||||||
existingPodsIndex.Add(v1mymirrorpod)
|
existingPodsIndex.Add(v1mymirrorpod)
|
||||||
@ -1238,6 +1250,42 @@ func Test_nodePlugin_Admit(t *testing.T) {
|
|||||||
attributes: admission.NewAttributesRecord(nil, nil, csiNodeKind, nodeInfo.Namespace, nodeInfo.Name, csiNodeResource, "", admission.Delete, &metav1.UpdateOptions{}, false, mynode),
|
attributes: admission.NewAttributesRecord(nil, nil, csiNodeKind, nodeInfo.Namespace, nodeInfo.Name, csiNodeResource, "", admission.Delete, &metav1.UpdateOptions{}, false, mynode),
|
||||||
err: "",
|
err: "",
|
||||||
},
|
},
|
||||||
|
// CSR
|
||||||
|
{
|
||||||
|
name: "allowed CSR create correct node serving",
|
||||||
|
attributes: createCSRAttributes("system:node:mynode", certificatesapi.KubeletServingSignerName, true, privKey, mynode),
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allowed CSR create correct node client",
|
||||||
|
attributes: createCSRAttributes("system:node:mynode", certificatesapi.KubeAPIServerClientKubeletSignerName, true, privKey, mynode),
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allowed CSR create non-node CSR",
|
||||||
|
attributes: createCSRAttributes("some-other-identity", certificatesapi.KubeAPIServerClientSignerName, true, privKey, mynode),
|
||||||
|
err: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deny CSR create incorrect node",
|
||||||
|
attributes: createCSRAttributes("system:node:othernode", certificatesapi.KubeletServingSignerName, true, privKey, mynode),
|
||||||
|
err: "forbidden: can only create a node CSR with CN=system:node:mynode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow CSR create incorrect node with feature gate disabled",
|
||||||
|
attributes: createCSRAttributes("system:node:othernode", certificatesapi.KubeletServingSignerName, true, privKey, mynode),
|
||||||
|
err: "",
|
||||||
|
features: feature.DefaultFeatureGate,
|
||||||
|
setupFunc: func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.DisableKubeletCSRAdmissionValidation, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deny CSR create invalid",
|
||||||
|
attributes: createCSRAttributes("system:node:mynode", certificatesapi.KubeletServingSignerName, false, privKey, mynode),
|
||||||
|
err: "unable to parse csr: asn1: syntax error: sequence truncated",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt.nodesGetter = existingNodes
|
tt.nodesGetter = existingNodes
|
||||||
@ -1603,6 +1651,31 @@ func createPodAttributes(pod *api.Pod, user user.Info) admission.Attributes {
|
|||||||
return admission.NewAttributesRecord(pod, nil, podKind, pod.Namespace, pod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, user)
|
return admission.NewAttributesRecord(pod, nil, podKind, pod.Namespace, pod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createCSRAttributes(cn, signer string, validCsr bool, key any, user user.Info) admission.Attributes {
|
||||||
|
csrResource := certificatesapi.Resource("certificatesigningrequests").WithVersion("v1")
|
||||||
|
csrKind := certificatesapi.Kind("CertificateSigningRequest").WithVersion("v1")
|
||||||
|
|
||||||
|
csrPem := []byte("-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST-----")
|
||||||
|
if validCsr {
|
||||||
|
structuredCsr := x509.CertificateRequest{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: cn,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
csrDer, _ := x509.CreateCertificateRequest(rand.Reader, &structuredCsr, key)
|
||||||
|
csrPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDer})
|
||||||
|
}
|
||||||
|
|
||||||
|
csreq := &certificatesapi.CertificateSigningRequest{
|
||||||
|
Spec: certificatesapi.CertificateSigningRequestSpec{
|
||||||
|
Request: csrPem,
|
||||||
|
SignerName: signer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return admission.NewAttributesRecord(csreq, nil, csrKind, "", "", csrResource, "", admission.Create, &metav1.CreateOptions{}, false, user)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestAdmitResourceSlice(t *testing.T) {
|
func TestAdmitResourceSlice(t *testing.T) {
|
||||||
apiResource := resourceapi.SchemeGroupVersion.WithResource("resourceslices")
|
apiResource := resourceapi.SchemeGroupVersion.WithResource("resourceslices")
|
||||||
nodename := "mynode"
|
nodename := "mynode"
|
||||||
|
Loading…
Reference in New Issue
Block a user