Skip to content

Commit

Permalink
Merge pull request #945 from atc0005/i933-add-support-for-ignoring-ex…
Browse files Browse the repository at this point in the history
…piring-intermediate-and-root-certs

Add support for ignoring expiring non-leaf certs
  • Loading branch information
atc0005 authored Sep 25, 2024
2 parents c12ce0b + cf279b9 commit 54ad582
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 30 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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. |
Expand Down
8 changes: 5 additions & 3 deletions cmd/check_cert/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
55 changes: 48 additions & 7 deletions internal/certs/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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

Expand All @@ -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),
Expand All @@ -840,13 +851,25 @@ 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",
nagios.StateCRITICALLabel,
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",
Expand Down Expand Up @@ -874,6 +897,8 @@ func ShouldCertExpirationBeIgnored(
cert *x509.Certificate,
certChain []*x509.Certificate,
validationOptions CertChainValidationOptions,
ageCriticalThreshold time.Time,
ageWarningThreshold time.Time,
) bool {

if validationOptions.IgnoreValidationResultExpiration {
Expand All @@ -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
Expand Down Expand Up @@ -1117,7 +1152,13 @@ func GenerateCertChainReport(
certificate,
ageCriticalThreshold,
ageWarningThreshold,
ShouldCertExpirationBeIgnored(certificate, certChain, validationOptions),
ShouldCertExpirationBeIgnored(
certificate,
certChain,
validationOptions,
ageCriticalThreshold,
ageWarningThreshold,
),
)

fingerprints := struct {
Expand Down
156 changes: 140 additions & 16 deletions internal/certs/validation-expiration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -173,6 +181,18 @@ func ValidateExpiration(
certsExpireAgeWarning,
)

hasExpiringIntermediateCerts := HasExpiringCert(
IntermediateCerts(certChain),
certsExpireAgeCritical,
certsExpireAgeWarning,
)

hasExpiringRootCerts := HasExpiringCert(
RootCerts(certChain),
certsExpireAgeCritical,
certsExpireAgeWarning,
)

hasExpiredLeafCerts := HasExpiredCert(
LeafCerts(certChain),
)
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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

Expand All @@ -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,
}

}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -654,13 +768,23 @@ 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) {
if IsExpiredCert(cert) &&
evr.validationOptions.IgnoreExpiredRootCertificates {
continue
}

if IsExpiringCert(cert, evr.ageCriticalThreshold, evr.ageWarningThreshold) &&
evr.validationOptions.IgnoreExpiringRootCertificates {
continue
}
}

certChainFiltered = append(certChainFiltered, cert)
Expand Down
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 54ad582

Please sign in to comment.