Forward PKI revocation requests received by standby nodes to active node (#19624)

* Forward PKI revocation requests received by standby nodes to active node

 - A refactoring that occurred in 1.13 timeframe removed what was
   considered a specific check for standby nodes that wasn't required
   as a writes should be returning ErrReadOnly.
 - That sadly exposed a long standing bug where the errors from the
   storage layer were not being properly wrapped, hiding the ErrReadOnly
   coming from a write and failing the request.

* Add cl

* Add test for basic PKI operations against standby nodes
This commit is contained in:
Steven Clark 2023-03-20 10:58:36 -04:00 committed by GitHub
parent 2381e6be66
commit 2217731266
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 73 additions and 10 deletions

View File

@ -33,6 +33,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/hashicorp/vault/helper/testhelpers"
"github.com/hashicorp/vault/sdk/helper/testhelpers/schema" "github.com/hashicorp/vault/sdk/helper/testhelpers/schema"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -6408,6 +6410,56 @@ func TestUserIDsInLeafCerts(t *testing.T) {
requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid")
} }
// TestStandby_Operations test proper forwarding for PKI requests from a standby node to the
// active node within a cluster.
func TestStandby_Operations(t *testing.T) {
conf := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
}
opts := vault.TestClusterOptions{HandlerFunc: vaulthttp.Handler}
cluster := vault.NewTestCluster(t, conf, &opts)
cluster.Start()
defer cluster.Cleanup()
testhelpers.WaitForActiveNodeAndStandbys(t, cluster)
standbyCores := testhelpers.DeriveStandbyCores(t, cluster)
require.Greater(t, len(standbyCores), 0, "Need at least one standby core.")
client := standbyCores[0].Client
mountPKIEndpoint(t, client, "pki")
_, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"key_type": "ec",
"common_name": "root-ca.com",
"ttl": "600h",
})
require.NoError(t, err, "error setting up pki role: %v", err)
_, err = client.Logical().Write("pki/roles/example", map[string]interface{}{
"allowed_domains": "example.com",
"allow_subdomains": "true",
"no_store": "false", // make sure we store this cert
"ttl": "5h",
"key_type": "ec",
})
require.NoError(t, err, "error setting up pki role: %v", err)
resp, err := client.Logical().Write("pki/issue/example", map[string]interface{}{
"common_name": "test.example.com",
})
require.NoError(t, err, "error issuing certificate: %v", err)
require.NotNil(t, resp, "got nil response from issuing request")
serialOfCert := resp.Data["serial_number"].(string)
resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{
"serial_number": serialOfCert,
})
require.NoError(t, err, "error revoking certificate: %v", err)
require.NotNil(t, resp, "got nil response from revoke request")
}
var ( var (
initTest sync.Once initTest sync.Once
rsaCAKey string rsaCAKey string

View File

@ -156,7 +156,6 @@ func (cb *crlBuilder) reloadConfigIfRequired(sc *storageContext) error {
} }
previousConfig := cb.config previousConfig := cb.config
// Set the default config if none was returned to us. // Set the default config if none was returned to us.
if config != nil { if config != nil {
cb.config = *config cb.config = *config
@ -354,7 +353,7 @@ func (cb *crlBuilder) _getPresentDeltaWALForClearing(sc *storageContext, path st
// Clearing of the delta WAL occurs after a new complete CRL has been built. // Clearing of the delta WAL occurs after a new complete CRL has been built.
walSerials, err := sc.Storage.List(sc.Context, path) walSerials, err := sc.Storage.List(sc.Context, path)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching list of delta WAL certificates to clear: %s", err) return nil, fmt.Errorf("error fetching list of delta WAL certificates to clear: %w", err)
} }
// We _should_ remove the special WAL entries here, but we don't really // We _should_ remove the special WAL entries here, but we don't really
@ -380,7 +379,7 @@ func (cb *crlBuilder) _clearDeltaWAL(sc *storageContext, walSerials []string, pa
} }
if err := sc.Storage.Delete(sc.Context, path+serial); err != nil { if err := sc.Storage.Delete(sc.Context, path+serial); err != nil {
return fmt.Errorf("error clearing delta WAL certificate: %s", err) return fmt.Errorf("error clearing delta WAL certificate: %w", err)
} }
} }
@ -658,7 +657,7 @@ func (cb *crlBuilder) processRevocationQueue(sc *storageContext) error {
(!sc.Backend.System().LocalMount() && sc.Backend.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary)) (!sc.Backend.System().LocalMount() && sc.Backend.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary))
if err := cb.maybeGatherQueueForFirstProcess(sc, isNotPerfPrimary); err != nil { if err := cb.maybeGatherQueueForFirstProcess(sc, isNotPerfPrimary); err != nil {
return fmt.Errorf("failed to gather first queue: %v", err) return fmt.Errorf("failed to gather first queue: %w", err)
} }
revQueue := cb.revQueue.Iterate() revQueue := cb.revQueue.Iterate()
@ -973,13 +972,13 @@ func revokeCert(sc *storageContext, config *crlConfig, cert *x509.Certificate) (
revEntry, err := logical.StorageEntryJSON(revokedPath+hyphenSerial, revInfo) revEntry, err := logical.StorageEntryJSON(revokedPath+hyphenSerial, revInfo)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating revocation entry") return nil, fmt.Errorf("error creating revocation entry: %w", err)
} }
certsCounted := sc.Backend.certsCounted.Load() certsCounted := sc.Backend.certsCounted.Load()
err = sc.Storage.Put(sc.Context, revEntry) err = sc.Storage.Put(sc.Context, revEntry)
if err != nil { if err != nil {
return nil, fmt.Errorf("error saving revoked certificate to new location") return nil, fmt.Errorf("error saving revoked certificate to new location: %w", err)
} }
sc.Backend.ifCountEnabledIncrementTotalRevokedCertificatesCount(certsCounted, revEntry.Key) sc.Backend.ifCountEnabledIncrementTotalRevokedCertificatesCount(certsCounted, revEntry.Key)
@ -1085,7 +1084,7 @@ func writeSpecificRevocationDeltaWALs(sc *storageContext, hyphenSerial string, c
} }
if err = sc.Storage.Put(sc.Context, walEntry); err != nil { if err = sc.Storage.Put(sc.Context, walEntry); err != nil {
return fmt.Errorf("error saving delta CRL WAL entry") return fmt.Errorf("error saving delta CRL WAL entry: %w", err)
} }
// In order for periodic delta rebuild to be mildly efficient, we // In order for periodic delta rebuild to be mildly efficient, we
@ -1097,7 +1096,7 @@ func writeSpecificRevocationDeltaWALs(sc *storageContext, hyphenSerial string, c
return fmt.Errorf("unable to create last delta CRL WAL entry") return fmt.Errorf("unable to create last delta CRL WAL entry")
} }
if err = sc.Storage.Put(sc.Context, lastWALEntry); err != nil { if err = sc.Storage.Put(sc.Context, lastWALEntry); err != nil {
return fmt.Errorf("error saving last delta CRL WAL entry") return fmt.Errorf("error saving last delta CRL WAL entry: %w", err)
} }
return nil return nil
@ -1844,12 +1843,12 @@ func getLocalRevokedCertEntries(sc *storageContext, issuerIDCertMap map[issuerID
// we should update the entry to make future CRL builds faster. // we should update the entry to make future CRL builds faster.
revokedEntry, err = logical.StorageEntryJSON(revokedPath+serial, revInfo) revokedEntry, err = logical.StorageEntryJSON(revokedPath+serial, revInfo)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error creating revocation entry for existing cert: %v", serial) return nil, nil, fmt.Errorf("error creating revocation entry for existing cert: %v: %w", serial, err)
} }
err = sc.Storage.Put(sc.Context, revokedEntry) err = sc.Storage.Put(sc.Context, revokedEntry)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error updating revoked certificate at existing location: %v", serial) return nil, nil, fmt.Errorf("error updating revoked certificate at existing location: %v: %w", serial, err)
} }
} }
} }

View File

@ -16,6 +16,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/helper/errutil"
@ -591,6 +593,13 @@ func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, dat
} }
} }
// Assumption: this check is cheap. Call this twice, in the cert-import
// case, to allow cert verification to get rejected on the standby node,
// but we still need it to protect the serial number case.
if b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) {
return nil, logical.ErrReadOnly
}
b.revokeStorageLock.Lock() b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock() defer b.revokeStorageLock.Unlock()

3
changelog/19624.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
secrets/pki: Fix PKI revocation request forwarding from standby nodes due to an error wrapping bug
```