From 426340e31c99e3d9262e817ee717767e73409741 Mon Sep 17 00:00:00 2001 From: guhaneswaran20 Date: Fri, 30 Oct 2020 10:08:42 +0000 Subject: [PATCH] check node certificate expiration Reference:- https://hackmd.io/aGaz7YXSSHybGcyol8vYEw Relates-To: #391 Change-Id: I8c9c83dfb2eb11af48857fb96404dcf2eb3eaa55 --- .../checkexpiration/checkexpiration.go | 92 ++++++++++++++++++- pkg/cluster/checkexpiration/command.go | 14 +++ pkg/cluster/checkexpiration/command_test.go | 55 ++++++++++- .../checkexpiration/testdata/node.yaml | 6 ++ 4 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 pkg/cluster/checkexpiration/testdata/node.yaml diff --git a/pkg/cluster/checkexpiration/checkexpiration.go b/pkg/cluster/checkexpiration/checkexpiration.go index aa65e3ed9..58cc2887d 100644 --- a/pkg/cluster/checkexpiration/checkexpiration.go +++ b/pkg/cluster/checkexpiration/checkexpiration.go @@ -32,7 +32,9 @@ import ( ) const ( - kubeconfigIdentifierSuffix = "-kubeconfig" + kubeconfigIdentifierSuffix = "-kubeconfig" + timeFormat = "Jan 02, 2006 15:04 MST" + nodeCertExpirationAnnotation = "cert-expiration" ) // CertificateExpirationStore is the customized client store @@ -258,3 +260,91 @@ func (store CertificateExpirationStore) getKubeconfSecrets() ([]corev1.Secret, e kubeconfigs = filterOwners(kubeconfigs, "KubeadmControlPlane") return kubeconfigs, nil } + +// GetExpiringNodeCertificates runs through all the nodes and identifies expiration +func (store CertificateExpirationStore) GetExpiringNodeCertificates() ([]NodeCert, error) { + // Node will be updated with an annotation with the expiry content (Activity + // of HostConfig Operator - 'check-expiry' CR Object) every day (Cron like + // activity is performed by reconcile tag in the Operator) Below code is + // implemented to just read the annotation, parse it, identify expirable + // content and report back + + // Expected Annotation Format: + // "cert-expiration": "{ admin.conf: Aug 06, 2021 12:36 UTC }, + // { apiserver: Aug 06, 2021 12:36 UTC }, + // { apiserver-etcd-client: Aug 06, 2021 12:36 UTC }, + // { apiserver-kubelet-client: Aug 06, 2021 12:36 UTC }, + // { controller-manager.conf: Aug 06, 2021 12:36 UTC }, + // { etcd-healthcheck-client: Aug 06, 2021 12:36 UTC }, + // { etcd-peer: Aug 06, 2021 12:36 UTC }, + // { etcd-server: Aug 06, 2021 12:36 UTC }, + // { front-proxy-client: Aug 06, 2021 12:36 UTC }, + // { scheduler.conf: Aug 06, 2021 12:36 UTC }, + // { ca: Aug 04, 2030 12:36 UTC }, + // { etcd-ca: Aug 04, 2030 12:36 UTC }, + // { front-proxy-ca: Aug 04, 2030 12:36 UTC }" + + nodes, err := store.getNodes(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + nodeData := make([]NodeCert, 0) + for _, node := range nodes.Items { + expiringNodeCertificates := store.getExpiringNodeCertificates(node) + if len(expiringNodeCertificates) > 0 { + nodeData = append(nodeData, NodeCert{ + Name: node.Name, + Namespace: node.Namespace, + ExpiringCertificates: expiringNodeCertificates, + }) + } + } + return nodeData, nil +} + +// getSecrets returns the Nodes list based on the listOptions +func (store CertificateExpirationStore) getNodes(listOptions metav1.ListOptions) (*corev1.NodeList, error) { + return store.Kclient.ClientSet().CoreV1().Nodes().List(listOptions) +} + +// getExpiringNodeCertificates skims through all the node certificates and returns +// the ones lesser than threshold +func (store CertificateExpirationStore) getExpiringNodeCertificates(node corev1.Node) map[string]string { + if cert, found := node.ObjectMeta.Annotations[nodeCertExpirationAnnotation]; found { + certificateList := splitAsList(cert) + + expiringCertificates := map[string]string{} + for _, certificate := range certificateList { + certificateName, expirationDate := identifyCertificateNameAndExpirationDate(certificate) + if certificateName != "" && isWithinDuration(expirationDate, store.ExpirationThreshold) { + expiringCertificates[certificateName] = expirationDate.String() + } + } + return expiringCertificates + } + log.Printf("%s annotation missing for node %s in %s", nodeCertExpirationAnnotation, + node.Name, node.Namespace) + return nil +} + +// splitAsList performes the required string manipulations and returns list of items +func splitAsList(value string) []string { + return strings.Split(strings.ReplaceAll(value, "{", ""), "},") +} + +// identifyCertificateNameAndExpirationDate performs string manipulations and returns +// certificate name and its expiration date +func identifyCertificateNameAndExpirationDate(certificate string) (string, time.Time) { + certificateName := strings.TrimSpace(strings.Split(certificate, ":")[0]) + expirationDate := strings.TrimSpace(strings.Split(certificate, ":")[1]) + + ":" + + strings.TrimSpace(strings.ReplaceAll(strings.Split(certificate, ":")[2], "}", "")) + + formattedExpirationDate, err := time.Parse(timeFormat, expirationDate) + if err != nil { + log.Printf(err.Error()) + return "", time.Time{} + } + return certificateName, formattedExpirationDate +} diff --git a/pkg/cluster/checkexpiration/command.go b/pkg/cluster/checkexpiration/command.go index 524c6e258..0765b237a 100644 --- a/pkg/cluster/checkexpiration/command.go +++ b/pkg/cluster/checkexpiration/command.go @@ -44,6 +44,7 @@ type CheckCommand struct { type ExpirationStore struct { TLSSecrets []TLSSecret `json:"tlsSecrets,omitempty" yaml:"tlsSecrets,omitempty"` Kubeconfs []Kubeconfig `json:"kubeconfs,omitempty" yaml:"kubeconfs,omitempty"` + NodeCerts []NodeCert `json:"nodeCerts,omitempty" yaml:"nodeCerts,omitempty"` } // TLSSecret captures expiration information of certificates embedded in TLS secrets @@ -68,6 +69,13 @@ type kubeconfData struct { ExpirationDate string `json:"expirationDate,omitempty" yaml:"expirationDate,omitempty"` } +// NodeCert captures certificate expiry information for certificates on each node +type NodeCert struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + ExpiringCertificates map[string]string `json:"certificate,omitempty" yaml:"certificate,omitempty"` +} + // RunE is the implementation of check command func (c *CheckCommand) RunE(w io.Writer) error { if !strings.EqualFold(c.Options.FormatType, "json") && !strings.EqualFold(c.Options.FormatType, "yaml") { @@ -112,8 +120,14 @@ func (store CertificateExpirationStore) GetExpiringCertificates() ExpirationStor log.Printf(err.Error()) } + expiringNodeCertificates, err := store.GetExpiringNodeCertificates() + if err != nil { + log.Printf(err.Error()) + } + return ExpirationStore{ TLSSecrets: expiringTLSCertificates, Kubeconfs: expiringKubeConfCertificates, + NodeCerts: expiringNodeCertificates, } } diff --git a/pkg/cluster/checkexpiration/command_test.go b/pkg/cluster/checkexpiration/command_test.go index 67aae85f6..63b1da05d 100644 --- a/pkg/cluster/checkexpiration/command_test.go +++ b/pkg/cluster/checkexpiration/command_test.go @@ -66,7 +66,27 @@ const ( } ] } - ] + ], + "nodeCerts": [ + { + "name": "test-node", + "certificate": { + "admin.conf": "2021-08-06 12:36:00 +0000 UTC", + "apiserver": "2021-08-06 12:36:00 +0000 UTC", + "apiserver-etcd-client": "2021-08-06 12:36:00 +0000 UTC", + "apiserver-kubelet-client": "2021-08-06 12:36:00 +0000 UTC", + "ca": "2021-08-04 12:36:00 +0000 UTC", + "controller-manager.conf": "2021-08-06 12:36:00 +0000 UTC", + "etcd-ca": "2021-08-04 12:36:00 +0000 UTC", + "etcd-healthcheck-client": "2021-08-06 12:36:00 +0000 UTC", + "etcd-peer": "2021-08-06 12:36:00 +0000 UTC", + "etcd-server": "2021-08-06 12:36:00 +0000 UTC", + "front-proxy-ca": "2021-08-04 12:36:00 +0000 UTC", + "front-proxy-client": "2021-08-06 12:36:00 +0000 UTC", + "scheduler.conf": "2021-08-06 12:36:00 +0000 UTC" + } + } + ] }` expectedYAMLOutput = ` @@ -88,6 +108,22 @@ tlsSecrets: tls.crt: 2030-08-31 10:12:49 +0000 UTC name: test-cluster-etcd namespace: default +nodeCerts: +- name: test-node + certificate: + admin.conf: 2021-08-06 12:36:00 +0000 UTC + apiserver: 2021-08-06 12:36:00 +0000 UTC + apiserver-etcd-client: 2021-08-06 12:36:00 +0000 UTC + apiserver-kubelet-client: 2021-08-06 12:36:00 +0000 UTC + ca: 2021-08-04 12:36:00 +0000 UTC + controller-manager.conf: 2021-08-06 12:36:00 +0000 UTC + etcd-ca: 2021-08-04 12:36:00 +0000 UTC + etcd-healthcheck-client: 2021-08-06 12:36:00 +0000 UTC + etcd-peer: 2021-08-06 12:36:00 +0000 UTC + etcd-server: 2021-08-06 12:36:00 +0000 UTC + front-proxy-ca: 2021-08-04 12:36:00 +0000 UTC + front-proxy-client: 2021-08-06 12:36:00 +0000 UTC + scheduler.conf: 2021-08-06 12:36:00 +0000 UTC ... ` ) @@ -143,8 +179,9 @@ func TestRunE(t *testing.T) { for _, tt := range tests { t.Run(tt.testCaseName, func(t *testing.T) { objects := []runtime.Object{ - getObject(t, "testdata/tls-secret.yaml"), - getObject(t, "testdata/kubeconfig.yaml"), + getSecretObject(t, "testdata/tls-secret.yaml"), + getSecretObject(t, "testdata/kubeconfig.yaml"), + getNodeObject(t, "testdata/node.yaml"), } ra := fake.WithTypedObjects(objects...) @@ -176,7 +213,7 @@ func TestRunE(t *testing.T) { } } -func getObject(t *testing.T, fileName string) *v1.Secret { +func getSecretObject(t *testing.T, fileName string) *v1.Secret { t.Helper() object := readObjectFromFile(t, fileName) @@ -186,6 +223,16 @@ func getObject(t *testing.T, fileName string) *v1.Secret { return secret } +func getNodeObject(t *testing.T, fileName string) *v1.Node { + t.Helper() + + object := readObjectFromFile(t, fileName) + node, ok := object.(*v1.Node) + require.True(t, ok) + + return node +} + func readObjectFromFile(t *testing.T, fileName string) runtime.Object { t.Helper() diff --git a/pkg/cluster/checkexpiration/testdata/node.yaml b/pkg/cluster/checkexpiration/testdata/node.yaml new file mode 100644 index 000000000..7d4d174ea --- /dev/null +++ b/pkg/cluster/checkexpiration/testdata/node.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Node +metadata: + annotations: + cert-expiration: "{ admin.conf: Aug 06, 2021 12:36 UTC },{ apiserver: Aug 06, 2021 12:36 UTC },{ apiserver-etcd-client: Aug 06, 2021 12:36 UTC },{ apiserver-kubelet-client: Aug 06, 2021 12:36 UTC },{ controller-manager.conf: Aug 06, 2021 12:36 UTC },{ etcd-healthcheck-client: Aug 06, 2021 12:36 UTC },{ etcd-peer: Aug 06, 2021 12:36 UTC },{ etcd-server: Aug 06, 2021 12:36 UTC },{ front-proxy-client: Aug 06, 2021 12:36 UTC },{ scheduler.conf: Aug 06, 2021 12:36 UTC },{ ca: Aug 04, 2021 12:36 UTC },{ etcd-ca: Aug 04, 2021 12:36 UTC },{ front-proxy-ca: Aug 04, 2021 12:36 UTC }" + name: test-node \ No newline at end of file