From cf279b94e77a13e69783c52073cabc750a5c595d Mon Sep 17 00:00:00 2001 From: Adam Chalkley Date: Wed, 25 Sep 2024 04:47:55 -0500 Subject: [PATCH] Add support for ignoring expiring non-leaf certs - add `ignore-expiring-intermediate-certs` flag to allow explicitly ignoring expiring intermediate certificates in a chain - add `ignore-expiring-root-certs` flag to allow explicitly ignoring expiring root certificates in a chain - update README to cover new flags - explicitly mark *expiring* intermediate and root certs as `EXPIRING` and `IGNORED` when either of the new flags are used *OR* the sysadmin explicitly requests that expiration validation results be ignored refs GH-933 --- README.md | 8 +- cmd/check_cert/validate.go | 8 +- internal/certs/certs.go | 55 +++++++-- internal/certs/validation-expiration.go | 156 +++++++++++++++++++++--- internal/config/config.go | 8 ++ internal/config/constants.go | 15 ++- internal/config/flags.go | 14 +++ 7 files changed, 234 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 0b293939..e3e258a3 100644 --- a/README.md +++ b/README.md @@ -269,8 +269,10 @@ accessible to this tool. Use FQDNs in order to retrieve certificates using - Optional support for skipping hostname verification for a certificate when the SANs list is empty -- Optional support for ignoring expiration of intermediate certificates -- Optional support for ignoring expiration of root certificates +- Optional support for ignoring expiring intermediate certificates +- Optional support for ignoring expired intermediate certificates +- Optional support for ignoring expiring root certificates +- Optional support for ignoring expired of root certificates ### `lscert` @@ -625,6 +627,8 @@ validation checks and any behavior changes at that time noted. | `ignore-hostname-verification-if-empty-sans` | No | `false` | No | `true`, `false` | Whether a hostname verification failure should be ignored if Subject Alternate Names (SANs) list is empty. | | `ignore-expired-intermediate-certs` | No | `false` | No | `true`, `false` | Whether expired intermediate certificates should be ignored. | | `ignore-expired-root-certs` | No | `false` | No | `true`, `false` | Whether expired root certificates should be ignored. | +| `ignore-expiring-intermediate-certs` | No | `false` | No | `true`, `false` | Whether expiring intermediate certificates should be ignored. | +| `ignore-expiring-root-certs` | No | `false` | No | `true`, `false` | Whether expiring root certificates should be ignored. | | `ignore-validation-result` | No | | No | `sans`, `expiration`, `hostname` | List of keywords for certificate chain validation check result that should be explicitly ignored and not used to determine final validation state. | | `apply-validation-result` | No | | No | `sans`, `expiration`, `hostname` | List of keywords for certificate chain validation check results that should be explicitly applied and used to determine final validation state. | | `list-ignored-errors` | No | `false` | No | `true`, `false` | Toggles emission of ignored validation check result errors. Disabled by default to reduce confusion. | diff --git a/cmd/check_cert/validate.go b/cmd/check_cert/validate.go index 90ea7a10..21e3a8bd 100644 --- a/cmd/check_cert/validate.go +++ b/cmd/check_cert/validate.go @@ -92,9 +92,11 @@ func runValidationChecks(cfg *config.Config, certChain []*x509.Certificate, log } expirationValidationOptions := certs.CertChainValidationOptions{ - IgnoreExpiredIntermediateCertificates: cfg.IgnoreExpiredIntermediateCertificates, - IgnoreExpiredRootCertificates: cfg.IgnoreExpiredRootCertificates, - IgnoreValidationResultExpiration: !cfg.ApplyCertExpirationValidationResults(), + IgnoreExpiredIntermediateCertificates: cfg.IgnoreExpiredIntermediateCertificates, + IgnoreExpiredRootCertificates: cfg.IgnoreExpiredRootCertificates, + IgnoreExpiringIntermediateCertificates: cfg.IgnoreExpiringIntermediateCertificates, + IgnoreExpiringRootCertificates: cfg.IgnoreExpiringRootCertificates, + IgnoreValidationResultExpiration: !cfg.ApplyCertExpirationValidationResults(), } log.Debug(). diff --git a/internal/certs/certs.go b/internal/certs/certs.go index 2bdf5c67..393d566f 100644 --- a/internal/certs/certs.go +++ b/internal/certs/certs.go @@ -134,14 +134,25 @@ type CertChainValidationOptions struct { // Names (SANs) validation against a leaf certificate in a chain. IgnoreValidationResultSANs bool + // IgnoreExpiringIntermediateCertificates tracks whether a request was + // made to ignore validation check results for certificate expiration + // against intermediate certificates in a certificate chain which are + // expiring. + IgnoreExpiringIntermediateCertificates bool + + // IgnoreExpiringRootCertificates tracks whether a request was made to + // ignore validation check results for certificate expiration against root + // certificates in a certificate chain which are expiring. + IgnoreExpiringRootCertificates bool + // IgnoreExpiredIntermediateCertificates tracks whether a request was made // to ignore validation check results for certificate expiration against - // intermediate certificates in a certificate chain. + // intermediate certificates in a certificate chain which have expired. IgnoreExpiredIntermediateCertificates bool // IgnoreExpiredRootCertificates tracks whether a request was made to // ignore validation check results for certificate expiration against root - // certificates in a certificate chain. + // certificates in a certificate chain which have expired. IgnoreExpiredRootCertificates bool } @@ -816,9 +827,9 @@ func FormatCertSerialNumber(sn *big.Int) string { // ExpirationStatus receives a certificate and the expiration threshold values // for CRITICAL and WARNING states and returns a human-readable string -// indicating the overall status at a glance. If requested, an expired -// certificate is marked as ignored. -func ExpirationStatus(cert *x509.Certificate, ageCritical time.Time, ageWarning time.Time, ignoreExpired bool) string { +// indicating the overall status at a glance. If requested, an expiring or +// expired certificate is marked as ignored. +func ExpirationStatus(cert *x509.Certificate, ageCritical time.Time, ageWarning time.Time, ignoreExpiration bool) string { var expiresText string certExpiration := cert.NotAfter @@ -828,7 +839,7 @@ func ExpirationStatus(cert *x509.Certificate, ageCritical time.Time, ageWarning } switch { - case certExpiration.Before(time.Now()) && ignoreExpired: + case certExpiration.Before(time.Now()) && ignoreExpiration: expiresText = fmt.Sprintf( "[EXPIRED, IGNORED] %s%s", FormattedExpiration(certExpiration), @@ -840,6 +851,12 @@ func ExpirationStatus(cert *x509.Certificate, ageCritical time.Time, ageWarning FormattedExpiration(certExpiration), lifeRemainingText, ) + case certExpiration.Before(ageCritical) && ignoreExpiration: + expiresText = fmt.Sprintf( + "[EXPIRING, IGNORED] %s%s", + FormattedExpiration(certExpiration), + lifeRemainingText, + ) case certExpiration.Before(ageCritical): expiresText = fmt.Sprintf( "[%s] %s%s", @@ -847,6 +864,12 @@ func ExpirationStatus(cert *x509.Certificate, ageCritical time.Time, ageWarning FormattedExpiration(certExpiration), lifeRemainingText, ) + case certExpiration.Before(ageWarning) && ignoreExpiration: + expiresText = fmt.Sprintf( + "[EXPIRING, IGNORED] %s%s", + FormattedExpiration(certExpiration), + lifeRemainingText, + ) case certExpiration.Before(ageWarning): expiresText = fmt.Sprintf( "[%s] %s%s", @@ -874,6 +897,8 @@ func ShouldCertExpirationBeIgnored( cert *x509.Certificate, certChain []*x509.Certificate, validationOptions CertChainValidationOptions, + ageCriticalThreshold time.Time, + ageWarningThreshold time.Time, ) bool { if validationOptions.IgnoreValidationResultExpiration { @@ -885,12 +910,22 @@ func ShouldCertExpirationBeIgnored( validationOptions.IgnoreExpiredRootCertificates { return true } + + if IsExpiringCert(cert, ageCriticalThreshold, ageWarningThreshold) && + validationOptions.IgnoreExpiringRootCertificates { + return true + } } if IsIntermediateCert(cert, certChain) { if IsExpiredCert(cert) && validationOptions.IgnoreExpiredIntermediateCertificates { return true } + + if IsExpiringCert(cert, ageCriticalThreshold, ageWarningThreshold) && + validationOptions.IgnoreExpiringIntermediateCertificates { + return true + } } return false @@ -1117,7 +1152,13 @@ func GenerateCertChainReport( certificate, ageCriticalThreshold, ageWarningThreshold, - ShouldCertExpirationBeIgnored(certificate, certChain, validationOptions), + ShouldCertExpirationBeIgnored( + certificate, + certChain, + validationOptions, + ageCriticalThreshold, + ageWarningThreshold, + ), ) fingerprints := struct { diff --git a/internal/certs/validation-expiration.go b/internal/certs/validation-expiration.go index 1e554e02..ed3f1a83 100644 --- a/internal/certs/validation-expiration.go +++ b/internal/certs/validation-expiration.go @@ -58,6 +58,14 @@ type ExpirationValidationResult struct { // chain have expired. hasExpiredRootCerts bool + // hasExpiringIntermediateCerts indicates whether any intermediate + // certificates in the chain are expiring. + hasExpiringIntermediateCerts bool + + // hasExpiringRootCerts indicates whether any root certificates in the + // chain are expiring. + hasExpiringRootCerts bool + // numExpiredCerts indicates how many certificates in the chain have // expired. numExpiredCerts int @@ -173,6 +181,18 @@ func ValidateExpiration( certsExpireAgeWarning, ) + hasExpiringIntermediateCerts := HasExpiringCert( + IntermediateCerts(certChain), + certsExpireAgeCritical, + certsExpireAgeWarning, + ) + + hasExpiringRootCerts := HasExpiringCert( + RootCerts(certChain), + certsExpireAgeCritical, + certsExpireAgeWarning, + ) + hasExpiredLeafCerts := HasExpiredCert( LeafCerts(certChain), ) @@ -212,6 +232,28 @@ func ValidateExpiration( priorityModifier = priorityModifierMinimum } + case hasExpiringIntermediateCerts && + !validationOptions.IgnoreExpiringIntermediateCertificates: + err = fmt.Errorf( + "expiration validation failed: %w", + ErrExpiringCertsFound, + ) + + if !validationOptions.IgnoreValidationResultExpiration { + priorityModifier = priorityModifierMinimum + } + + case hasExpiringRootCerts && + !validationOptions.IgnoreExpiringRootCertificates: + err = fmt.Errorf( + "expiration validation failed: %w", + ErrExpiringCertsFound, + ) + + if !validationOptions.IgnoreValidationResultExpiration { + priorityModifier = priorityModifierMinimum + } + case hasExpiredIntermediateCerts && !validationOptions.IgnoreExpiredIntermediateCertificates: @@ -246,6 +288,17 @@ func ValidateExpiration( ErrExpiredCertsFound, ) + // NOTE: + // + // Because we set this, we end up flagging the entire expiration + // validation check as ignored. This excludes the next expiring + // certificate from the ServiceOutput and thus the service check + // one-line summary. + // + // When the *leaf* certificate approaches expiration or is expired + // then that will cause the expiration validation check to take + // precedence again and no longer be ignored. This seems acceptable + // behavior for now. ignored = validationOptions.IgnoreExpiredIntermediateCertificates priorityModifier = priorityModifierBaseline @@ -259,28 +312,89 @@ func ValidateExpiration( ErrExpiredCertsFound, ) + // NOTE: + // + // Because we set this, we end up flagging the entire expiration + // validation check as ignored. This excludes the next expiring + // certificate from the ServiceOutput and thus the service check + // one-line summary. + // + // When the *leaf* certificate approaches expiration or is expired + // then that will cause the expiration validation check to take + // precedence again and no longer be ignored. This seems acceptable + // behavior for now. ignored = validationOptions.IgnoreExpiredRootCertificates priorityModifier = priorityModifierBaseline + case hasExpiringIntermediateCerts && + validationOptions.IgnoreExpiringIntermediateCertificates: + + // Even if we're opting to ignore this validation result, we still + // note that expiring certificates were found in the chain. + err = fmt.Errorf( + "expiration validation failed: %w", + ErrExpiringCertsFound, + ) + + // NOTE: + // + // Because we set this, we end up flagging the entire expiration + // validation check as ignored. This excludes the next expiring + // certificate from the ServiceOutput and thus the service check + // one-line summary. + // + // When the *leaf* certificate approaches expiration or is expired + // then that will cause the expiration validation check to take + // precedence again and no longer be ignored. This seems acceptable + // behavior for now. + ignored = validationOptions.IgnoreExpiringIntermediateCertificates + priorityModifier = priorityModifierBaseline + + case hasExpiringRootCerts && + validationOptions.IgnoreExpiringRootCertificates: + + // Even if we're opting to ignore this validation result, we still + // note that expiring certificates were found in the chain. + err = fmt.Errorf( + "expiration validation failed: %w", + ErrExpiringCertsFound, + ) + + // NOTE: + // + // Because we set this, we end up flagging the entire expiration + // validation check as ignored. This excludes the next expiring + // certificate from the ServiceOutput and thus the service check + // one-line summary. + // + // When the *leaf* certificate approaches expiration or is expired + // then that will cause the expiration validation check to take + // precedence again and no longer be ignored. This seems acceptable + // behavior for now. + ignored = validationOptions.IgnoreExpiringRootCertificates + priorityModifier = priorityModifierBaseline + default: // Neither expired nor expiring certificates. } return ExpirationValidationResult{ - certChain: certChain, - err: err, - validationOptions: validationOptions, - ignored: ignored, - verboseOutput: verboseOutput, - ageWarningThreshold: certsExpireAgeWarning, - ageCriticalThreshold: certsExpireAgeCritical, - hasExpiredCerts: hasExpiredCerts, - hasExpiringCerts: hasExpiringCerts, - hasExpiredIntermediateCerts: hasExpiredIntermediateCerts, - hasExpiredRootCerts: hasExpiredRootCerts, - numExpiredCerts: numExpiredCerts, - numExpiringCerts: numExpiringCerts, - priorityModifier: priorityModifier, + certChain: certChain, + err: err, + validationOptions: validationOptions, + ignored: ignored, + verboseOutput: verboseOutput, + ageWarningThreshold: certsExpireAgeWarning, + ageCriticalThreshold: certsExpireAgeCritical, + hasExpiredCerts: hasExpiredCerts, + hasExpiringCerts: hasExpiringCerts, + hasExpiredIntermediateCerts: hasExpiredIntermediateCerts, + hasExpiredRootCerts: hasExpiredRootCerts, + hasExpiringIntermediateCerts: hasExpiringIntermediateCerts, + hasExpiringRootCerts: hasExpiringRootCerts, + numExpiredCerts: numExpiredCerts, + numExpiringCerts: numExpiringCerts, + priorityModifier: priorityModifier, } } @@ -435,8 +549,8 @@ func (evr ExpirationValidationResult) Status() string { var summaryTemplate string // We evaluate the filtered certificate chain instead of the original in - // case the sysadmin opted to exclude expired intermediate or root - // certificates. + // case the sysadmin opted to exclude intermediate or root certificates + // based on expiring or expired status. switch { case HasExpiredCert(certChainFiltered): summaryTemplate = ExpirationValidationOneLineSummaryExpiredTmpl @@ -654,6 +768,11 @@ func (evr ExpirationValidationResult) FilteredCertificateChain() []*x509.Certifi evr.validationOptions.IgnoreExpiredIntermediateCertificates { continue } + + if IsExpiringCert(cert, evr.ageCriticalThreshold, evr.ageWarningThreshold) && + evr.validationOptions.IgnoreExpiringIntermediateCertificates { + continue + } } if IsRootCert(cert, evr.certChain) { @@ -661,6 +780,11 @@ func (evr ExpirationValidationResult) FilteredCertificateChain() []*x509.Certifi evr.validationOptions.IgnoreExpiredRootCertificates { continue } + + if IsExpiringCert(cert, evr.ageCriticalThreshold, evr.ageWarningThreshold) && + evr.validationOptions.IgnoreExpiringRootCertificates { + continue + } } certChainFiltered = append(certChainFiltered, cert) diff --git a/internal/config/config.go b/internal/config/config.go index d2cff95c..85f701ec 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -318,6 +318,14 @@ type Config struct { // failure. IgnoreHostnameVerificationFailureIfEmptySANsList bool + // IgnoreExpiringIntermediateCertificates indicates whether expiring + // intermediate certificates should be ignored. + IgnoreExpiringIntermediateCertificates bool + + // IgnoreExpiringRootCertificates indicates whether expiring root + // certificates should be ignored. + IgnoreExpiringRootCertificates bool + // IgnoreExpiredIntermediateCertificates indicates whether expired // intermediate certificates should be ignored. IgnoreExpiredIntermediateCertificates bool diff --git a/internal/config/constants.go b/internal/config/constants.go index 3d1ab7a2..e7465ffe 100644 --- a/internal/config/constants.go +++ b/internal/config/constants.go @@ -53,6 +53,8 @@ const ( listIgnoredErrorsFlagHelp string = "Toggles emission of ignored validation check result errors. Disabled by default to reduce confusion." ignoreExpiredIntermediateCertificatesFlagHelp string = "Whether expired intermediate certificates should be ignored." ignoreExpiredRootCertificatesFlagHelp string = "Whether expired root certificates should be ignored." + ignoreExpiringIntermediateCertificatesFlagHelp string = "Whether expiring intermediate certificates should be ignored." + ignoreExpiringRootCertificatesFlagHelp string = "Whether expiring root certificates should be ignored." ) // shorthandFlagSuffix is appended to short flag help text to emphasize that @@ -71,8 +73,10 @@ const ( // certificate chain validation state. IgnoreHostnameVerificationFailureIfEmptySANsListFlag string = "ignore-hostname-verification-if-empty-sans" - IgnoreExpiredIntermediateCertificatesFlag string = "ignore-expired-intermediate-certs" - IgnoreExpiredRootCertificatesFlag string = "ignore-expired-root-certs" + IgnoreExpiredIntermediateCertificatesFlag string = "ignore-expired-intermediate-certs" + IgnoreExpiredRootCertificatesFlag string = "ignore-expired-root-certs" + IgnoreExpiringIntermediateCertificatesFlag string = "ignore-expiring-intermediate-certs" + IgnoreExpiringRootCertificatesFlag string = "ignore-expiring-root-certs" VersionFlagLong string = "version" VerboseFlagLong string = "verbose" @@ -169,6 +173,13 @@ const ( // Default choice of whether expired root certificates should be ignored. defaultIgnoreExpiredRootCertificates bool = false + // Default choice of whether expiring intermediate certificates should be + // ignored. + defaultIgnoreExpiringIntermediateCertificates bool = false + + // Default choice of whether expiring root certificates should be ignored. + defaultIgnoreExpiringRootCertificates bool = false + // Whether validation check result errors should be included in the final // plugin report output. By default, ignored errors are not included as // this may prove confusing (e.g., when all results are either successful diff --git a/internal/config/flags.go b/internal/config/flags.go index 9c8050b0..e024411a 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -80,6 +80,20 @@ func (c *Config) handleFlagsConfig(appType AppType) { ignoreExpiredRootCertificatesFlagHelp, ) + flag.BoolVar( + &c.IgnoreExpiringIntermediateCertificates, + IgnoreExpiringIntermediateCertificatesFlag, + defaultIgnoreExpiringIntermediateCertificates, + ignoreExpiringIntermediateCertificatesFlagHelp, + ) + + flag.BoolVar( + &c.IgnoreExpiringRootCertificates, + IgnoreExpiringRootCertificatesFlag, + defaultIgnoreExpiringRootCertificates, + ignoreExpiringRootCertificatesFlagHelp, + ) + flag.BoolVar(&c.VerboseOutput, VerboseFlagShort, defaultVerboseOutput, verboseOutputFlagHelp+shorthandFlagSuffix) flag.BoolVar(&c.VerboseOutput, VerboseFlagLong, defaultVerboseOutput, verboseOutputFlagHelp)