check kubeconf certificate expiration

Reference:- https://hackmd.io/aGaz7YXSSHybGcyol8vYEw

Relates-To: #391

Change-Id: I66c1cce8a506b763d6ac18c055f3162381c9fd70
This commit is contained in:
guhaneswaran20 2020-10-30 09:32:47 +00:00
parent 47ada2c541
commit 0302462450
4 changed files with 231 additions and 9 deletions

View File

@ -19,15 +19,22 @@ import (
"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"
)
// CertificateExpirationStore is the customized client store
type CertificateExpirationStore struct {
Kclient client.Interface
@ -85,6 +92,11 @@ func (store CertificateExpirationStore) GetExpiringTLSCertificates() ([]TLSSecre
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)
}
@ -130,3 +142,119 @@ func extractExpirationDateFromCertificate(certData []byte) (time.Time, 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
}

View File

@ -21,6 +21,7 @@ import (
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/log"
"opendev.org/airship/airshipctl/pkg/util/yaml"
)
@ -39,6 +40,12 @@ type CheckCommand struct {
ClientFactory client.Factory
}
// ExpirationStore captures expiration information of all expirable entities in the cluster
type ExpirationStore struct {
TLSSecrets []TLSSecret `json:"tlsSecrets,omitempty" yaml:"tlsSecrets,omitempty"`
Kubeconfs []Kubeconfig `json:"kubeconfs,omitempty" yaml:"kubeconfs,omitempty"`
}
// TLSSecret captures expiration information of certificates embedded in TLS secrets
type TLSSecret struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
@ -46,6 +53,21 @@ type TLSSecret struct {
ExpiringCertificates map[string]string `json:"certificate,omitempty" yaml:"certificate,omitempty"`
}
// Kubeconfig captures expiration information of all kubeconfigs
type Kubeconfig struct {
SecretName string `json:"secretName,omitempty" yaml:"secretName,omitempty"`
SecretNamespace string `json:"secretNamespace,omitempty" yaml:"secretNamespace,omitempty"`
Cluster []kubeconfData `json:"cluster,omitempty" yaml:"cluster,omitempty"`
User []kubeconfData `json:"user,omitempty" yaml:"user,omitempty"`
}
// kubeconfData captures cluster ca certificate expiration information and kubeconfig's user's certificate
type kubeconfData struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
CertificateName string `json:"certificateName,omitempty" yaml:"certificateName,omitempty"`
ExpirationDate string `json:"expirationDate,omitempty" yaml:"expirationDate,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") {
@ -58,10 +80,7 @@ func (c *CheckCommand) RunE(w io.Writer) error {
return err
}
expirationInfo, err := secretStore.GetExpiringTLSCertificates()
if err != nil {
return err
}
expirationInfo := secretStore.GetExpiringCertificates()
if c.Options.FormatType == "yaml" {
err = yaml.WriteOut(w, expirationInfo)
@ -80,3 +99,21 @@ func (c *CheckCommand) RunE(w io.Writer) error {
}
return nil
}
// GetExpiringCertificates encapsulates all the different expirable entities in the cluster
func (store CertificateExpirationStore) GetExpiringCertificates() ExpirationStore {
expiringTLSCertificates, err := store.GetExpiringTLSCertificates()
if err != nil {
log.Printf(err.Error())
}
expiringKubeConfCertificates, err := store.GetExpiringKubeConfigs()
if err != nil {
log.Printf(err.Error())
}
return ExpirationStore{
TLSSecrets: expiringTLSCertificates,
Kubeconfs: expiringKubeConfCertificates,
}
}

View File

@ -36,7 +36,8 @@ import (
const (
testThreshold = 5000
expectedJSONOutput = `[
expectedJSONOutput = ` {
"tlsSecrets": [
{
"name": "test-cluster-etcd",
"namespace": "default",
@ -45,10 +46,43 @@ const (
"tls.crt": "2030-08-31 10:12:49 +0000 UTC"
}
}
]`
],
"kubeconfs": [
{
"secretName": "test-cluster-kubeconfig",
"secretNamespace": "default",
"cluster": [
{
"name": "workload-cluster",
"certificateName": "CertificateAuthorityData",
"expirationDate": "2030-08-31 10:12:48 +0000 UTC"
}
],
"user": [
{
"name": "workload-cluster-admin",
"certificateName": "ClientCertificateData",
"expirationDate": "2021-09-02 10:12:50 +0000 UTC"
}
]
}
]
}`
expectedYAMLOutput = `
---
kubeconfs:
- cluster:
- certificateName: CertificateAuthorityData
expirationDate: 2030-08-31 10:12:48 +0000 UTC
name: workload-cluster
secretName: test-cluster-kubeconfig
secretNamespace: default
user:
- certificateName: ClientCertificateData
expirationDate: 2021-09-02 10:12:50 +0000 UTC
name: workload-cluster-admin
tlsSecrets:
- certificate:
ca.crt: 2030-08-31 10:12:49 +0000 UTC
tls.crt: 2030-08-31 10:12:49 +0000 UTC
@ -108,7 +142,10 @@ func TestRunE(t *testing.T) {
for _, tt := range tests {
t.Run(tt.testCaseName, func(t *testing.T) {
objects := []runtime.Object{getTLSSecret(t)}
objects := []runtime.Object{
getObject(t, "testdata/tls-secret.yaml"),
getObject(t, "testdata/kubeconfig.yaml"),
}
ra := fake.WithTypedObjects(objects...)
command := checkexpiration.CheckCommand{
@ -127,6 +164,7 @@ func TestRunE(t *testing.T) {
assert.Contains(t, err.Error(), tt.testErr)
} else {
require.NoError(t, err)
t.Log(buffer.String())
switch tt.checkFlags.FormatType {
case "json":
assert.JSONEq(t, tt.expectedOutput, buffer.String())
@ -138,11 +176,13 @@ func TestRunE(t *testing.T) {
}
}
func getTLSSecret(t *testing.T) *v1.Secret {
func getObject(t *testing.T, fileName string) *v1.Secret {
t.Helper()
object := readObjectFromFile(t, "testdata/tls-secret.yaml")
object := readObjectFromFile(t, fileName)
secret, ok := object.(*v1.Secret)
require.True(t, ok)
return secret
}

File diff suppressed because one or more lines are too long