2023-03-15 16:00:52 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2022-11-16 20:24:54 +00:00
package healthcheck
import (
"bytes"
"crypto/x509"
"fmt"
"github.com/hashicorp/go-secure-stdlib/parseutil"
)
type RootIssuedLeaves struct {
Enabled bool
UnsupportedVersion bool
CertsToFetch int
2023-02-24 18:00:09 +00:00
FetchIssues map [ string ] * PathFetch
2022-11-16 20:24:54 +00:00
RootCertMap map [ string ] * x509 . Certificate
LeafCertMap map [ string ] * x509 . Certificate
}
func NewRootIssuedLeavesCheck ( ) Check {
return & RootIssuedLeaves {
2023-02-24 18:00:09 +00:00
FetchIssues : make ( map [ string ] * PathFetch ) ,
2022-11-16 20:24:54 +00:00
RootCertMap : make ( map [ string ] * x509 . Certificate ) ,
LeafCertMap : make ( map [ string ] * x509 . Certificate ) ,
}
}
func ( h * RootIssuedLeaves ) Name ( ) string {
return "root_issued_leaves"
}
func ( h * RootIssuedLeaves ) IsEnabled ( ) bool {
return h . Enabled
}
func ( h * RootIssuedLeaves ) DefaultConfig ( ) map [ string ] interface { } {
return map [ string ] interface { } {
"certs_to_fetch" : 100 ,
}
}
func ( h * RootIssuedLeaves ) LoadConfig ( config map [ string ] interface { } ) error {
count , err := parseutil . SafeParseIntRange ( config [ "certs_to_fetch" ] , 1 , 100000 )
if err != nil {
return fmt . Errorf ( "error parsing %v.certs_to_fetch: %w" , h . Name ( ) , err )
}
h . CertsToFetch = int ( count )
enabled , err := parseutil . ParseBool ( config [ "enabled" ] )
if err != nil {
return fmt . Errorf ( "error parsing %v.enabled: %w" , h . Name ( ) , err )
}
h . Enabled = enabled
return nil
}
func ( h * RootIssuedLeaves ) FetchResources ( e * Executor ) error {
2022-11-18 18:42:48 +00:00
exit , _ , issuers , err := pkiFetchIssuersList ( e , func ( ) {
2022-11-16 20:24:54 +00:00
h . UnsupportedVersion = true
} )
2022-11-17 14:26:13 +00:00
if exit || err != nil {
2022-11-16 20:24:54 +00:00
return err
}
for _ , issuer := range issuers {
2023-02-24 18:00:09 +00:00
skip , pathFetch , cert , err := pkiFetchIssuer ( e , issuer , func ( ) {
2022-11-16 20:24:54 +00:00
h . UnsupportedVersion = true
} )
2023-02-24 18:00:09 +00:00
h . FetchIssues [ issuer ] = pathFetch
2022-11-17 14:26:13 +00:00
if skip || err != nil {
2022-11-16 20:24:54 +00:00
if err != nil {
return err
}
continue
}
// Ensure we only check Root CAs.
if ! bytes . Equal ( cert . RawSubject , cert . RawIssuer ) {
continue
}
if err := cert . CheckSignatureFrom ( cert ) ; err != nil {
continue
}
h . RootCertMap [ issuer ] = cert
}
2023-02-24 18:00:09 +00:00
exit , f , leaves , err := pkiFetchLeavesList ( e , func ( ) {
2022-11-16 20:24:54 +00:00
h . UnsupportedVersion = true
} )
2022-11-17 14:26:13 +00:00
if exit || err != nil {
2023-02-24 18:00:09 +00:00
if f != nil && f . IsSecretPermissionsError ( ) {
for _ , issuer := range issuers {
h . FetchIssues [ issuer ] = f
}
}
2022-11-16 20:24:54 +00:00
return err
}
var leafCount int
for _ , serial := range leaves {
if leafCount >= h . CertsToFetch {
break
}
skip , _ , cert , err := pkiFetchLeaf ( e , serial , func ( ) {
h . UnsupportedVersion = true
} )
2022-11-17 14:26:13 +00:00
if skip || err != nil {
2022-11-16 20:24:54 +00:00
if err != nil {
return err
}
continue
}
// Ignore other CAs.
if cert . BasicConstraintsValid && cert . IsCA {
continue
}
leafCount += 1
h . LeafCertMap [ serial ] = cert
}
return nil
}
func ( h * RootIssuedLeaves ) Evaluate ( e * Executor ) ( results [ ] * Result , err error ) {
if h . UnsupportedVersion {
ret := Result {
Status : ResultInvalidVersion ,
Endpoint : "/{{mount}}/issuers" ,
Message : "This health check requires Vault 1.11+ but an earlier version of Vault Server was contacted, preventing this health check from running." ,
}
return [ ] * Result { & ret } , nil
}
2023-02-24 18:00:09 +00:00
for issuer , fetchPath := range h . FetchIssues {
if fetchPath != nil && fetchPath . IsSecretPermissionsError ( ) {
delete ( h . RootCertMap , issuer )
ret := Result {
Status : ResultInsufficientPermissions ,
Endpoint : fetchPath . Path ,
Message : "Without this information, this health check is unable to function." ,
}
if e . Client . Token ( ) == "" {
ret . Message = "No token available so unable for the endpoint for this mount. " + ret . Message
} else {
ret . Message = "This token lacks permission for the endpoint for this mount. " + ret . Message
}
results = append ( results , & ret )
}
}
2022-11-16 20:24:54 +00:00
issuerHasLeaf := make ( map [ string ] bool )
for serial , leaf := range h . LeafCertMap {
if len ( issuerHasLeaf ) == len ( h . RootCertMap ) {
break
}
for issuer , root := range h . RootCertMap {
if issuerHasLeaf [ issuer ] {
continue
}
if ! bytes . Equal ( leaf . RawIssuer , root . RawSubject ) {
continue
}
if err := leaf . CheckSignatureFrom ( root ) ; err != nil {
continue
}
ret := Result {
Status : ResultWarning ,
Endpoint : "/{{mount}}/issuer/" + issuer ,
Message : fmt . Sprintf ( "Root issuer has directly issued non-CA leaf certificates (%v) instead of via an intermediate CA. This can make rotating the root CA harder as direct cross-signing of the roots must be used, rather than cross-signing of the intermediates. It is encouraged to set up and use an intermediate CA and tidy the mount when all directly issued leaves have expired." , serial ) ,
}
issuerHasLeaf [ issuer ] = true
results = append ( results , & ret )
}
}
2022-11-23 14:42:19 +00:00
if len ( results ) == 0 && len ( h . RootCertMap ) > 0 {
ret := Result {
Status : ResultOK ,
Endpoint : "/{{mount}}/certs" ,
Message : "Root certificate(s) in this mount have not directly issued non-CA leaf certificates." ,
}
results = append ( results , & ret )
}
2022-11-16 20:24:54 +00:00
return
}