Merge "check node certificate expiration"

This commit is contained in:
Zuul 2020-12-04 23:40:32 +00:00 committed by Gerrit Code Review
commit bf4b794502
4 changed files with 162 additions and 5 deletions

View File

@ -33,6 +33,8 @@ import (
const ( const (
kubeconfigIdentifierSuffix = "-kubeconfig" kubeconfigIdentifierSuffix = "-kubeconfig"
timeFormat = "Jan 02, 2006 15:04 MST"
nodeCertExpirationAnnotation = "cert-expiration"
) )
// CertificateExpirationStore is the customized client store // CertificateExpirationStore is the customized client store
@ -258,3 +260,91 @@ func (store CertificateExpirationStore) getKubeconfSecrets() ([]corev1.Secret, e
kubeconfigs = filterOwners(kubeconfigs, "KubeadmControlPlane") kubeconfigs = filterOwners(kubeconfigs, "KubeadmControlPlane")
return kubeconfigs, nil 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
}

View File

@ -44,6 +44,7 @@ type CheckCommand struct {
type ExpirationStore struct { type ExpirationStore struct {
TLSSecrets []TLSSecret `json:"tlsSecrets,omitempty" yaml:"tlsSecrets,omitempty"` TLSSecrets []TLSSecret `json:"tlsSecrets,omitempty" yaml:"tlsSecrets,omitempty"`
Kubeconfs []Kubeconfig `json:"kubeconfs,omitempty" yaml:"kubeconfs,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 // 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"` 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 // RunE is the implementation of check command
func (c *CheckCommand) RunE(w io.Writer) error { func (c *CheckCommand) RunE(w io.Writer) error {
if !strings.EqualFold(c.Options.FormatType, "json") && !strings.EqualFold(c.Options.FormatType, "yaml") { 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()) log.Printf(err.Error())
} }
expiringNodeCertificates, err := store.GetExpiringNodeCertificates()
if err != nil {
log.Printf(err.Error())
}
return ExpirationStore{ return ExpirationStore{
TLSSecrets: expiringTLSCertificates, TLSSecrets: expiringTLSCertificates,
Kubeconfs: expiringKubeConfCertificates, Kubeconfs: expiringKubeConfCertificates,
NodeCerts: expiringNodeCertificates,
} }
} }

View File

@ -66,6 +66,26 @@ 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"
}
}
] ]
}` }`
@ -88,6 +108,22 @@ tlsSecrets:
tls.crt: 2030-08-31 10:12:49 +0000 UTC tls.crt: 2030-08-31 10:12:49 +0000 UTC
name: test-cluster-etcd name: test-cluster-etcd
namespace: default 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 { for _, tt := range tests {
t.Run(tt.testCaseName, func(t *testing.T) { t.Run(tt.testCaseName, func(t *testing.T) {
objects := []runtime.Object{ objects := []runtime.Object{
getObject(t, "testdata/tls-secret.yaml"), getSecretObject(t, "testdata/tls-secret.yaml"),
getObject(t, "testdata/kubeconfig.yaml"), getSecretObject(t, "testdata/kubeconfig.yaml"),
getNodeObject(t, "testdata/node.yaml"),
} }
ra := fake.WithTypedObjects(objects...) 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() t.Helper()
object := readObjectFromFile(t, fileName) object := readObjectFromFile(t, fileName)
@ -186,6 +223,16 @@ func getObject(t *testing.T, fileName string) *v1.Secret {
return 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 { func readObjectFromFile(t *testing.T, fileName string) runtime.Object {
t.Helper() t.Helper()

View File

@ -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