426340e31c
Reference:- https://hackmd.io/aGaz7YXSSHybGcyol8vYEw Relates-To: #391 Change-Id: I8c9c83dfb2eb11af48857fb96404dcf2eb3eaa55
351 lines
12 KiB
Go
351 lines
12 KiB
Go
/*
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package checkexpiration
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
|
|
|
"opendev.org/airship/airshipctl/pkg/config"
|
|
"opendev.org/airship/airshipctl/pkg/k8s/client"
|
|
)
|
|
|
|
const (
|
|
kubeconfigIdentifierSuffix = "-kubeconfig"
|
|
timeFormat = "Jan 02, 2006 15:04 MST"
|
|
nodeCertExpirationAnnotation = "cert-expiration"
|
|
)
|
|
|
|
// CertificateExpirationStore is the customized client store
|
|
type CertificateExpirationStore struct {
|
|
Kclient client.Interface
|
|
Settings config.Factory
|
|
ExpirationThreshold int
|
|
}
|
|
|
|
// NewStore returns an instance of a CertificateExpirationStore
|
|
func NewStore(cfgFactory config.Factory, clientFactory client.Factory,
|
|
kubeconfig, _ string, expirationThreshold int) (CertificateExpirationStore, error) {
|
|
airshipconfig, err := cfgFactory()
|
|
if err != nil {
|
|
return CertificateExpirationStore{}, err
|
|
}
|
|
|
|
// TODO (guhan) Allow kube context to be passed to client Factory
|
|
// 4th argument in NewStore takes kube context and is ignored for now.
|
|
// To be modified post #388. Refer to
|
|
// https://review.opendev.org/#/c/760501/7/pkg/cluster/checkexpiration/command.go@31
|
|
kclient, err := clientFactory(airshipconfig.LoadedConfigPath(), kubeconfig)
|
|
if err != nil {
|
|
return CertificateExpirationStore{}, err
|
|
}
|
|
|
|
return CertificateExpirationStore{
|
|
Kclient: kclient,
|
|
Settings: cfgFactory,
|
|
ExpirationThreshold: expirationThreshold,
|
|
}, nil
|
|
}
|
|
|
|
// GetExpiringTLSCertificates returns the list of TLS certificates whose expiration date
|
|
// falls within the given expirationThreshold
|
|
func (store CertificateExpirationStore) GetExpiringTLSCertificates() ([]TLSSecret, error) {
|
|
secrets, err := store.getAllTLSCertificates()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsData := make([]TLSSecret, 0)
|
|
for _, secret := range secrets.Items {
|
|
expiringCertificates := store.getExpiringCertificates(secret)
|
|
if len(expiringCertificates) > 0 {
|
|
tlsData = append(tlsData, TLSSecret{
|
|
Name: secret.Name,
|
|
Namespace: secret.Namespace,
|
|
ExpiringCertificates: expiringCertificates,
|
|
})
|
|
}
|
|
}
|
|
return tlsData, nil
|
|
}
|
|
|
|
// getAllTLSCertificates juist returns all the k8s secrets with tyoe as TLS
|
|
func (store CertificateExpirationStore) getAllTLSCertificates() (*corev1.SecretList, error) {
|
|
secretTypeFieldSelector := fmt.Sprintf("type=%s", corev1.SecretTypeTLS)
|
|
listOptions := metav1.ListOptions{FieldSelector: secretTypeFieldSelector}
|
|
return store.getSecrets(listOptions)
|
|
}
|
|
|
|
// getSecrets returns the secret list based on the listOptions
|
|
func (store CertificateExpirationStore) getSecrets(listOptions metav1.ListOptions) (*corev1.SecretList, error) {
|
|
return store.Kclient.ClientSet().CoreV1().Secrets("").List(listOptions)
|
|
}
|
|
|
|
// getExpiringCertificates skims through all the TLS certificates and returns the ones
|
|
// lesser than threshold
|
|
func (store CertificateExpirationStore) getExpiringCertificates(secret corev1.Secret) map[string]string {
|
|
expiringCertificates := map[string]string{}
|
|
for _, certName := range []string{corev1.TLSCertKey, corev1.ServiceAccountRootCAKey} {
|
|
if cert, found := secret.Data[certName]; found {
|
|
expirationDate, err := extractExpirationDateFromCertificate(cert)
|
|
if err != nil {
|
|
log.Printf("Unable to parse certificate for %s in secret %s in namespace %s: %v",
|
|
certName, secret.Name, secret.Namespace, err)
|
|
continue
|
|
}
|
|
|
|
if isWithinDuration(expirationDate, store.ExpirationThreshold) {
|
|
expiringCertificates[certName] = expirationDate.String()
|
|
}
|
|
}
|
|
}
|
|
return expiringCertificates
|
|
}
|
|
|
|
// isWithinDuration checks if the certificate expirationDate is within the duration (input)
|
|
func isWithinDuration(expirationDate time.Time, duration int) bool {
|
|
if duration < 0 {
|
|
return true
|
|
}
|
|
daysUntilExpiration := int(time.Until(expirationDate).Hours() / 24)
|
|
return 0 <= daysUntilExpiration && daysUntilExpiration < duration
|
|
}
|
|
|
|
// extractExpirationDateFromCertificate parses the certificate and returns the expiration date
|
|
func extractExpirationDateFromCertificate(certData []byte) (time.Time, error) {
|
|
block, _ := pem.Decode(certData)
|
|
if block == nil {
|
|
return time.Time{}, ErrPEMFail{Context: "decode", Err: "no PEM data could be found"}
|
|
}
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return time.Time{}, ErrPEMFail{Context: "parse", Err: err.Error()}
|
|
}
|
|
return cert.NotAfter, nil
|
|
}
|
|
|
|
// GetExpiringKubeConfigs - fetches all the '-kubeconfig' secrets and identifies expiration
|
|
func (store CertificateExpirationStore) GetExpiringKubeConfigs() ([]Kubeconfig, error) {
|
|
kubeconfigs, err := store.getKubeconfSecrets()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
kSecretData := make([]Kubeconfig, 0)
|
|
|
|
for _, kubeconfig := range kubeconfigs {
|
|
kubecontent, err := clientcmd.Load(kubeconfig.Data["value"])
|
|
if err != nil {
|
|
log.Printf("Failed to read kubeconfig from %s in %s-"+
|
|
"it maybe malformed : %v", kubeconfig.Name, kubeconfig.Namespace, err.Error())
|
|
continue
|
|
}
|
|
|
|
expiringClusters := store.getExpiringClusterCertificates(kubecontent)
|
|
|
|
expiringUsers := store.getExpiringUserCertificates(kubecontent)
|
|
|
|
if len(expiringClusters) > 0 || len(expiringUsers) > 0 {
|
|
kSecretData = append(kSecretData, Kubeconfig{
|
|
SecretName: kubeconfig.Name,
|
|
SecretNamespace: kubeconfig.Namespace,
|
|
Cluster: expiringClusters,
|
|
User: expiringUsers,
|
|
})
|
|
}
|
|
}
|
|
return kSecretData, nil
|
|
}
|
|
|
|
// filterKubeConfigs identifies the kubeconfig secrets based on the kubeconfigIdentifierSuffix
|
|
func filterKubeConfigs(secrets []corev1.Secret) []corev1.Secret {
|
|
filteredSecrets := []corev1.Secret{}
|
|
for _, secret := range secrets {
|
|
if strings.HasSuffix(secret.Name, kubeconfigIdentifierSuffix) {
|
|
filteredSecrets = append(filteredSecrets, secret)
|
|
}
|
|
}
|
|
return filteredSecrets
|
|
}
|
|
|
|
// filterOwners allows only the secrets with Ownerreferences matching ownerKind
|
|
func filterOwners(secrets []corev1.Secret, ownerKind string) []corev1.Secret {
|
|
filteredSecrets := []corev1.Secret{}
|
|
for _, secret := range secrets {
|
|
for _, ownerRef := range secret.OwnerReferences {
|
|
if ownerRef.Kind == ownerKind {
|
|
filteredSecrets = append(filteredSecrets, secret)
|
|
}
|
|
}
|
|
}
|
|
return filteredSecrets
|
|
}
|
|
|
|
func (store CertificateExpirationStore) getExpiringClusterCertificates(
|
|
kubeconfig *clientcmdapi.Config) []kubeconfData {
|
|
expiringClusterCertificates := make([]kubeconfData, 0)
|
|
|
|
// Iterate through each Cluster and identify expiration
|
|
for clusterName, clusterData := range kubeconfig.Clusters {
|
|
expirationDate, err := extractExpirationDateFromCertificate(clusterData.CertificateAuthorityData)
|
|
if err != nil {
|
|
log.Printf("Unable to parse certificate for %s : %v", clusterName, err)
|
|
continue
|
|
}
|
|
|
|
if isWithinDuration(expirationDate, store.ExpirationThreshold) {
|
|
expiringClusterCertificates = append(expiringClusterCertificates, kubeconfData{
|
|
Name: clusterName,
|
|
CertificateName: "CertificateAuthorityData",
|
|
ExpirationDate: expirationDate.String(),
|
|
})
|
|
}
|
|
}
|
|
return expiringClusterCertificates
|
|
}
|
|
|
|
func (store CertificateExpirationStore) getExpiringUserCertificates(
|
|
kubeconfig *clientcmdapi.Config) []kubeconfData {
|
|
expiringUserCertificates := make([]kubeconfData, 0)
|
|
|
|
// Iterate through each User and identify expiration
|
|
for userName, userData := range kubeconfig.AuthInfos {
|
|
expirationDate, err := extractExpirationDateFromCertificate(userData.ClientCertificateData)
|
|
if err != nil {
|
|
log.Printf("Unable to parse certificate for %s : %v", userName, err)
|
|
continue
|
|
}
|
|
|
|
if isWithinDuration(expirationDate, store.ExpirationThreshold) {
|
|
expiringUserCertificates = append(expiringUserCertificates, kubeconfData{
|
|
Name: userName,
|
|
CertificateName: "ClientCertificateData",
|
|
ExpirationDate: expirationDate.String(),
|
|
})
|
|
}
|
|
}
|
|
return expiringUserCertificates
|
|
}
|
|
|
|
// getKubeconfSecrets filters the kubeconf secrets
|
|
func (store CertificateExpirationStore) getKubeconfSecrets() ([]corev1.Secret, error) {
|
|
secrets, err := store.getSecrets(metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
kubeconfigs := filterKubeConfigs(secrets.Items)
|
|
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
|
|
}
|