Skip to content

Commit bc63c41

Browse files
committed
kubelet request certificates if at least one IP exist
A Kubernetes Node requires to have at minimum one IP address because those are used on the Pods field HostIPs and in some cases, when pods uses hostNetwork: true, as PodIPs. Nodes that use IP addresses as Hostname are interpreted as an IP address, so it is possible that are nodes that don't hane any DNSname. The feature gate AllowDNSOnlyNodeCSR will allow user to opt-in for the old behavior. Change-Id: I094531d87246f1e7a5ef4fe57bd5d9840cb1375d
1 parent 29e4f5a commit bc63c41

File tree

3 files changed

+183
-15
lines changed

3 files changed

+183
-15
lines changed

pkg/features/kube_features.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ const (
4545
// Enable usage of Provision of PVCs from snapshots in other namespaces
4646
CrossNamespaceVolumeDataSource featuregate.Feature = "CrossNamespaceVolumeDataSource"
4747

48+
// owner: @aojea
49+
// Deprecated: v1.31
50+
//
51+
// Allow kubelet to request a certificate without any Node IP available, only
52+
// with DNS names.
53+
AllowDNSOnlyNodeCSR featuregate.Feature = "AllowDNSOnlyNodeCSR"
54+
4855
// owner: @thockin
4956
// deprecated: v1.29
5057
//
@@ -983,6 +990,8 @@ func init() {
983990
var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
984991
CrossNamespaceVolumeDataSource: {Default: false, PreRelease: featuregate.Alpha},
985992

993+
AllowDNSOnlyNodeCSR: {Default: false, PreRelease: featuregate.Deprecated}, // remove after 1.33
994+
986995
AllowServiceLBStatusOnNonLB: {Default: false, PreRelease: featuregate.Deprecated}, // remove after 1.29
987996

988997
AnyVolumeDataSource: {Default: true, PreRelease: featuregate.Beta}, // on by default in 1.24

pkg/kubelet/certificate/kubelet.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,44 @@ import (
3232
v1 "k8s.io/api/core/v1"
3333
"k8s.io/apimachinery/pkg/types"
3434
"k8s.io/apiserver/pkg/server/dynamiccertificates"
35+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3536
clientset "k8s.io/client-go/kubernetes"
3637
"k8s.io/client-go/util/certificate"
3738
compbasemetrics "k8s.io/component-base/metrics"
3839
"k8s.io/component-base/metrics/legacyregistry"
3940
"k8s.io/klog/v2"
41+
"k8s.io/kubernetes/pkg/features"
4042
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
4143
"k8s.io/kubernetes/pkg/kubelet/metrics"
4244
netutils "k8s.io/utils/net"
4345
)
4446

47+
func newGetTemplateFn(nodeName types.NodeName, getAddresses func() []v1.NodeAddress) func() *x509.CertificateRequest {
48+
return func() *x509.CertificateRequest {
49+
hostnames, ips := addressesToHostnamesAndIPs(getAddresses())
50+
// by default, require at least one IP before requesting a serving certificate
51+
hasRequiredAddresses := len(ips) > 0
52+
53+
// optionally allow requesting a serving certificate with just a DNS name
54+
if utilfeature.DefaultFeatureGate.Enabled(features.AllowDNSOnlyNodeCSR) {
55+
hasRequiredAddresses = hasRequiredAddresses || len(hostnames) > 0
56+
}
57+
58+
// don't return a template if we have no addresses to request for
59+
if !hasRequiredAddresses {
60+
return nil
61+
}
62+
return &x509.CertificateRequest{
63+
Subject: pkix.Name{
64+
CommonName: fmt.Sprintf("system:node:%s", nodeName),
65+
Organization: []string{"system:nodes"},
66+
},
67+
DNSNames: hostnames,
68+
IPAddresses: ips,
69+
}
70+
}
71+
}
72+
4573
// NewKubeletServerCertificateManager creates a certificate manager for the kubelet when retrieving a server certificate
4674
// or returns an error.
4775
func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg *kubeletconfig.KubeletConfiguration, nodeName types.NodeName, getAddresses func() []v1.NodeAddress, certDirectory string) (certificate.Manager, error) {
@@ -92,21 +120,7 @@ func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg
92120
)
93121
legacyregistry.MustRegister(certificateRotationAge)
94122

95-
getTemplate := func() *x509.CertificateRequest {
96-
hostnames, ips := addressesToHostnamesAndIPs(getAddresses())
97-
// don't return a template if we have no addresses to request for
98-
if len(hostnames) == 0 && len(ips) == 0 {
99-
return nil
100-
}
101-
return &x509.CertificateRequest{
102-
Subject: pkix.Name{
103-
CommonName: fmt.Sprintf("system:node:%s", nodeName),
104-
Organization: []string{"system:nodes"},
105-
},
106-
DNSNames: hostnames,
107-
IPAddresses: ips,
108-
}
109-
}
123+
getTemplate := newGetTemplateFn(nodeName, getAddresses)
110124

111125
m, err := certificate.NewManager(&certificate.Config{
112126
ClientsetFn: clientsetFn,

pkg/kubelet/certificate/kubelet_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package certificate
1919
import (
2020
"bytes"
2121
"context"
22+
"crypto/x509"
23+
"crypto/x509/pkix"
2224
"fmt"
2325
"net"
2426
"os"
@@ -28,8 +30,12 @@ import (
2830
"time"
2931

3032
v1 "k8s.io/api/core/v1"
33+
"k8s.io/apimachinery/pkg/types"
3134
"k8s.io/apimachinery/pkg/util/wait"
35+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3236
"k8s.io/client-go/util/cert"
37+
featuregatetesting "k8s.io/component-base/featuregate/testing"
38+
"k8s.io/kubernetes/pkg/features"
3339
netutils "k8s.io/utils/net"
3440
)
3541

@@ -261,3 +267,142 @@ func TestKubeletServerCertificateFromFiles(t *testing.T) {
261267
})
262268
}
263269
}
270+
271+
func TestNewCertificateManagerConfigGetTemplate(t *testing.T) {
272+
nodeName := "fake-node"
273+
nodeIP := netutils.ParseIPSloppy("192.168.1.1")
274+
tests := []struct {
275+
name string
276+
nodeAddresses []v1.NodeAddress
277+
want *x509.CertificateRequest
278+
featuregate bool
279+
}{
280+
{
281+
name: "node addresses or hostnames and gate enabled",
282+
featuregate: true,
283+
},
284+
{
285+
name: "node addresses or hostnames and gate disabled",
286+
featuregate: false,
287+
},
288+
{
289+
name: "only hostnames and gate enabled",
290+
nodeAddresses: []v1.NodeAddress{
291+
{
292+
Type: v1.NodeHostName,
293+
Address: nodeName,
294+
},
295+
},
296+
want: &x509.CertificateRequest{
297+
Subject: pkix.Name{
298+
CommonName: fmt.Sprintf("system:node:%s", nodeName),
299+
Organization: []string{"system:nodes"},
300+
},
301+
DNSNames: []string{nodeName},
302+
},
303+
featuregate: true,
304+
},
305+
{
306+
name: "only hostnames and gate disabled",
307+
nodeAddresses: []v1.NodeAddress{
308+
{
309+
Type: v1.NodeHostName,
310+
Address: nodeName,
311+
},
312+
},
313+
featuregate: false,
314+
},
315+
{
316+
name: "only IP addresses and gate enabled",
317+
nodeAddresses: []v1.NodeAddress{
318+
{
319+
Type: v1.NodeInternalIP,
320+
Address: nodeIP.String(),
321+
},
322+
},
323+
want: &x509.CertificateRequest{
324+
Subject: pkix.Name{
325+
CommonName: fmt.Sprintf("system:node:%s", nodeName),
326+
Organization: []string{"system:nodes"},
327+
},
328+
IPAddresses: []net.IP{nodeIP},
329+
},
330+
featuregate: true,
331+
},
332+
{
333+
name: "only IP addresses and gate disabled",
334+
nodeAddresses: []v1.NodeAddress{
335+
{
336+
Type: v1.NodeInternalIP,
337+
Address: nodeIP.String(),
338+
},
339+
},
340+
want: &x509.CertificateRequest{
341+
Subject: pkix.Name{
342+
CommonName: fmt.Sprintf("system:node:%s", nodeName),
343+
Organization: []string{"system:nodes"},
344+
},
345+
IPAddresses: []net.IP{nodeIP},
346+
},
347+
featuregate: false,
348+
},
349+
{
350+
name: "IP addresses and hostnames and gate enabled",
351+
nodeAddresses: []v1.NodeAddress{
352+
{
353+
Type: v1.NodeHostName,
354+
Address: nodeName,
355+
},
356+
{
357+
Type: v1.NodeInternalIP,
358+
Address: nodeIP.String(),
359+
},
360+
},
361+
want: &x509.CertificateRequest{
362+
Subject: pkix.Name{
363+
CommonName: fmt.Sprintf("system:node:%s", nodeName),
364+
Organization: []string{"system:nodes"},
365+
},
366+
DNSNames: []string{nodeName},
367+
IPAddresses: []net.IP{nodeIP},
368+
},
369+
featuregate: true,
370+
},
371+
{
372+
name: "IP addresses and hostnames and gate disabled",
373+
nodeAddresses: []v1.NodeAddress{
374+
{
375+
Type: v1.NodeHostName,
376+
Address: nodeName,
377+
},
378+
{
379+
Type: v1.NodeInternalIP,
380+
Address: nodeIP.String(),
381+
},
382+
},
383+
want: &x509.CertificateRequest{
384+
Subject: pkix.Name{
385+
CommonName: fmt.Sprintf("system:node:%s", nodeName),
386+
Organization: []string{"system:nodes"},
387+
},
388+
DNSNames: []string{nodeName},
389+
IPAddresses: []net.IP{nodeIP},
390+
},
391+
featuregate: false,
392+
},
393+
}
394+
for _, tt := range tests {
395+
t.Run(tt.name, func(t *testing.T) {
396+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AllowDNSOnlyNodeCSR, tt.featuregate)
397+
getAddresses := func() []v1.NodeAddress {
398+
return tt.nodeAddresses
399+
}
400+
getTemplate := newGetTemplateFn(types.NodeName(nodeName), getAddresses)
401+
got := getTemplate()
402+
if !reflect.DeepEqual(got, tt.want) {
403+
t.Errorf("Wrong certificate, got %v expected %v", got, tt.want)
404+
return
405+
}
406+
})
407+
}
408+
}

0 commit comments

Comments
 (0)