Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCPBUGS-35231: cert-inspection: parse secret/configmap/files as kubeconfigs #1746

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 116 additions & 32 deletions pkg/certs/cert-inspection/certgraphanalysis/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,34 @@ import (
"github.com/openshift/library-go/pkg/certs/cert-inspection/certgraphapi"
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/util/cert"
)

func InspectSecret(obj *corev1.Secret) (*certgraphapi.CertKeyPair, error) {
resourceString := fmt.Sprintf("secrets/%s[%s]", obj.Name, obj.Namespace)
func InspectSecret(obj *corev1.Secret) ([]*certgraphapi.CertKeyPair, error) {
tlsCrt, isTLS := obj.Data["tls.crt"]
if !isTLS {
return nil, nil
}
//fmt.Printf("%s - tls (%v)\n", resourceString, obj.CreationTimestamp.UTC())
if len(tlsCrt) == 0 {
return nil, fmt.Errorf("%s MISSING tls.crt content\n", resourceString)
}

certificates, err := cert.ParseCertsPEM([]byte(tlsCrt))
if err != nil {
return nil, err
}
for _, certificate := range certificates {
detail, err := toCertKeyPair(certificate)
if err != nil {
return nil, err
if !isTLS || len(tlsCrt) == 0 {
if detail, err := InspectSecretAsKubeConfig(obj); err == nil {
return detail, nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to somehow log that this has been attempted and an error occurred?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any error is treated as critical here, so "collect TLS artifacts" test would fail if errors found. Also, any valid certificate would be treated as invalid kubeconfig, so we should continue instead of returning error.

detail = addSecretLocation(detail, obj.Namespace, obj.Name)
return detail, nil
return nil, nil
}
return nil, fmt.Errorf("didn't see that coming")
return extractCertKeyPairsFromBytes("secret", &obj.ObjectMeta, tlsCrt)
}

func inspectCSR(resourceString, objName string, certificate []byte) (*certgraphapi.CertKeyPair, error) {
func extractCertKeyPairsFromBytes(resourceType string, obj *metav1.ObjectMeta, certificate []byte) ([]*certgraphapi.CertKeyPair, error) {
resourceString := ""
if obj != nil {
resourceString = fmt.Sprintf("%s/%s[%s]", resourceType, obj.Name, obj.Namespace)
}
if len(certificate) == 0 {
return nil, fmt.Errorf("%s MISSING issued certificate\n", resourceString)
}

certKeyPairDetails := []*certgraphapi.CertKeyPair{}
certificates, err := cert.ParseCertsPEM([]byte(certificate))
if err != nil {
return nil, err
Expand All @@ -49,34 +43,124 @@ func inspectCSR(resourceString, objName string, certificate []byte) (*certgrapha
if err != nil {
return nil, err
}
return detail, nil
if resourceType == "secret" && obj != nil {
detail = addSecretLocation(detail, obj.Namespace, obj.Name)
}
certKeyPairDetails = append(certKeyPairDetails, detail)
}
return nil, fmt.Errorf("didn't see that coming")
return certKeyPairDetails, nil
}

func InspectCSR(obj *certificatesv1.CertificateSigningRequest) (*certgraphapi.CertKeyPair, error) {
resourceString := fmt.Sprintf("csr/%s[%s]", obj.Name, obj.Namespace)
return inspectCSR(resourceString, obj.Name, obj.Status.Certificate)
func InspectCSR(obj *certificatesv1.CertificateSigningRequest) ([]*certgraphapi.CertKeyPair, error) {
return extractCertKeyPairsFromBytes("csr", &obj.ObjectMeta, obj.Status.Certificate)
}

func InspectConfigMap(obj *corev1.ConfigMap) (*certgraphapi.CertificateAuthorityBundle, error) {
caBundle, ok := obj.Data["ca-bundle.crt"]
if !ok {
return nil, nil
if details, err := InspectConfigMapAsKubeConfig(obj); err == nil {
return details, nil
}
if len(caBundle) == 0 {

caBundle, ok := obj.Data["ca-bundle.crt"]
if !ok || len(caBundle) == 0 {
return nil, nil
}

certificates, err := cert.ParseCertsPEM([]byte(caBundle))
if err != nil {
return nil, err
return nil, nil
}
caBundleDetail, err := toCABundle(certificates)
if err != nil {
return nil, err
}
caBundleDetail = addConfigMapLocation(caBundleDetail, obj.Namespace, obj.Name)
return caBundleDetail, nil
}

func extractKubeConfigFromConfigMap(obj *corev1.ConfigMap) (*rest.Config, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be more useful to return the actual kubeconfig type since it can contain multiple stanzas and rest.Config only holds one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I'll implement that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would require reworking entire InspectSecret/InspectConfigMap as they are expected to return just one secret. I'll implement this in a separate PR

Copy link
Member Author

@vrutkovs vrutkovs Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followup - #1751 included in 40ee799

if obj == nil {
return nil, fmt.Errorf("empty object")
}
for _, v := range obj.Data {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this appears to be parsing every key of potentially every configmap to see if it's a kubeconfig? Instead of doing this, could we choose specific keys we find valid?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can modify this to a hardcoded list of keys, but I'm worried that it might miss some new kubeconfigs.

kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(v))
if err != nil {
continue
}
if clientConfig, err := kubeConfig.ClientConfig(); err == nil {
return clientConfig, nil
}
}
return nil, nil
}

func extractKubeConfigFromSecret(obj *corev1.Secret) (*clientcmdapi.Config, error) {
if obj == nil {
return nil, fmt.Errorf("empty object")
}
for _, v := range obj.Data {
clientConfig, err := clientcmd.NewClientConfigFromBytes(v)
if err != nil {
continue
}
if kubeConfig, err := clientConfig.RawConfig(); err == nil && kubeConfig.CurrentContext != "" {
return &kubeConfig, nil
}
}
return nil, nil
}

func GetCAFromKubeConfig(kubeConfig *rest.Config, namespace, name string) (*certgraphapi.CertificateAuthorityBundle, error) {
if kubeConfig == nil {
return nil, fmt.Errorf("empty kubeconfig")
}
certificates, err := cert.ParseCertsPEM(kubeConfig.CAData)
if err != nil {
return nil, nil
}
caBundleDetail, err := toCABundle(certificates)
if err != nil {
return nil, err
}
if len(namespace) > 0 && len(name) > 0 {
caBundleDetail = addConfigMapLocation(caBundleDetail, namespace, name)
}
return caBundleDetail, nil
}

func GetCertKeyPairsFromKubeConfig(authInfo *clientcmdapi.AuthInfo, obj *metav1.ObjectMeta) ([]*certgraphapi.CertKeyPair, error) {
if authInfo == nil {
return nil, fmt.Errorf("empty authinfo")
}
return extractCertKeyPairsFromBytes("secret", obj, authInfo.ClientCertificateData)
}

func InspectConfigMapAsKubeConfig(obj *corev1.ConfigMap) (*certgraphapi.CertificateAuthorityBundle, error) {
kubeConfig, err := extractKubeConfigFromConfigMap(obj)
if err != nil {
return nil, err
}
if kubeConfig == nil {
return nil, fmt.Errorf("empty kubeconfig")
}

return GetCAFromKubeConfig(kubeConfig, obj.Namespace, obj.Name)
}

func InspectSecretAsKubeConfig(obj *corev1.Secret) ([]*certgraphapi.CertKeyPair, error) {
kubeConfig, err := extractKubeConfigFromSecret(obj)
if err != nil {
return nil, err
}
if kubeConfig == nil {
return nil, fmt.Errorf("empty kubeconfig")
}
certKeyPairInfos := []*certgraphapi.CertKeyPair{}
for _, v := range kubeConfig.AuthInfos {
certKeyPairInfo, err := GetCertKeyPairsFromKubeConfig(v, &obj.ObjectMeta)
if err != nil {
continue
}
certKeyPairInfos = append(certKeyPairInfos, certKeyPairInfo...)
}
return certKeyPairInfos, nil
}
9 changes: 5 additions & 4 deletions pkg/certs/cert-inspection/certgraphanalysis/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ func gatherFilteredCerts(ctx context.Context, kubeClient kubernetes.Interface, a
continue
}
options.rewriteCABundle(configMap.ObjectMeta, details)

caBundles = append(caBundles, details)

inClusterResourceData.CertificateAuthorityBundles = append(inClusterResourceData.CertificateAuthorityBundles,
Expand Down Expand Up @@ -149,12 +148,14 @@ func gatherFilteredCerts(ctx context.Context, kubeClient kubernetes.Interface, a
errs = append(errs, err)
continue
}
if details == nil {
if len(details) == 0 {
continue
}
options.rewriteCertKeyPair(secret.ObjectMeta, details)
certs = append(certs, details)
for i := range details {
options.rewriteCertKeyPair(secret.ObjectMeta, details[i])
}

certs = append(certs, details...)
inClusterResourceData.CertKeyPairs = append(inClusterResourceData.CertKeyPairs,
certgraphapi.PKIRegistryInClusterCertKeyPair{
SecretLocation: certgraphapi.InClusterSecretLocation{
Expand Down
57 changes: 52 additions & 5 deletions pkg/certs/cert-inspection/certgraphanalysis/disk_certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/openshift/library-go/pkg/certs/cert-inspection/certgraphapi"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/cert"
)

Expand All @@ -38,7 +39,7 @@ func gatherSecretsFromDisk(ctx context.Context, dir string, options certGenerati
}

fmt.Fprintf(os.Stdout, "Checking if %s is a certificate or secret key.\n", path)
if details, err := parseFileAsTLSArtifact(path, dir); err == nil && details != nil {
if details, err := parseFileAsCertKeyPair(path); err == nil && details != nil {
for i, detail := range details {
fmt.Fprintf(os.Stdout, "Found certkeypair #%d in %s.\n", i+1, path)
options.rewriteCertKeyPair(metav1.ObjectMeta{}, detail)
Expand Down Expand Up @@ -174,13 +175,36 @@ func parseBlockAsECDSAPrivateKey(key *ecdsa.PrivateKey, path string) *certgrapha
}
}

func parseFileAsTLSArtifact(path, dir string) ([]*certgraphapi.CertKeyPair, error) {
func parseFileAsCertKeyPair(path string) ([]*certgraphapi.CertKeyPair, error) {
var details []*certgraphapi.CertKeyPair

bytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Parse as kubeconfig
if kubeConfig, err := parseFileAsKubeConfig(path); err == nil {
details := []*certgraphapi.CertKeyPair{}
if rawConfig, err := kubeConfig.RawConfig(); err == nil {
for _, authInfo := range rawConfig.AuthInfos {
if certKeyPairs, err := GetCertKeyPairsFromKubeConfig(authInfo, nil); err == nil {
for i := range certKeyPairs {
certKeyPairs[i].Spec.OnDiskLocations = []certgraphapi.OnDiskCertKeyPairLocation{
{
Cert: certgraphapi.OnDiskLocation{
Path: path,
},
Key: certgraphapi.OnDiskLocation{
Path: path,
},
}}
}
details = append(details, certKeyPairs...)
}
}
return details, nil
}
}
// Parse all blocks
for len(bytes) > 0 {
fmt.Fprintf(os.Stdout, "Parsing block with length %d \n", len(bytes))
Expand All @@ -200,16 +224,31 @@ func parseFileAsTLSArtifact(path, dir string) ([]*certgraphapi.CertKeyPair, erro
return details, nil
}

func parseFileAsCA(path, dir string) (*certgraphapi.CertificateAuthorityBundle, error) {
func parseFileAsCA(path string) (*certgraphapi.CertificateAuthorityBundle, error) {
bytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Parse as kubeconfig
if kubeConfig, err := parseFileAsKubeConfig(path); err == nil {
if clientConfig, err := kubeConfig.ClientConfig(); err == nil {
fmt.Fprintf(os.Stdout, "Found a valid kubeconfig\n")
if detail, err := GetCAFromKubeConfig(clientConfig, "", ""); err == nil {
detail.Spec.OnDiskLocations = []certgraphapi.OnDiskLocation{
{
Path: path,
},
}
return detail, nil
}
}

}
certificates, err := cert.ParseCertsPEM(bytes)
if err != nil {
return nil, nil
}
// Check that the first certificat is a CA
// Check that the first certificate is a CA
if len(certificates) == 0 {
return nil, fmt.Errorf("no certificates found")
}
Expand All @@ -228,6 +267,14 @@ func parseFileAsCA(path, dir string) (*certgraphapi.CertificateAuthorityBundle,
return detail, nil
}

func parseFileAsKubeConfig(path string) (clientcmd.OverridingClientConfig, error) {
bytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return clientcmd.NewClientConfigFromBytes(bytes)
}

func gatherCABundlesFromDisk(ctx context.Context, dir string, options certGenerationOptionList) ([]*certgraphapi.CertificateAuthorityBundle, []*certgraphapi.OnDiskLocationWithMetadata, error) {
ret := []*certgraphapi.CertificateAuthorityBundle{}
metadataList := []*certgraphapi.OnDiskLocationWithMetadata{}
Expand All @@ -246,7 +293,7 @@ func gatherCABundlesFromDisk(ctx context.Context, dir string, options certGenera
return nil
}
fmt.Fprintf(os.Stdout, "Checking if %s is a CA bundle.\n", path)
if detail, err := parseFileAsCA(path, dir); err == nil && detail != nil {
if detail, err := parseFileAsCA(path); err == nil && detail != nil {
fmt.Fprintf(os.Stdout, "Found CA bundle in %s.\n", path)
options.rewriteCABundle(metav1.ObjectMeta{}, detail)

Expand Down