Skip to content

Commit

Permalink
Forward PKI revocation requests received by standby nodes to active n…
Browse files Browse the repository at this point in the history
…ode (#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
  • Loading branch information
stevendpclark committed Mar 20, 2023
1 parent e257377 commit d767d4c
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 10 deletions.
52 changes: 52 additions & 0 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"testing"
"time"

"github.com/hashicorp/vault/helper/testhelpers"

"github.com/stretchr/testify/require"

"github.com/armon/go-metrics"
Expand Down Expand Up @@ -6346,6 +6348,56 @@ func TestUserIDsInLeafCerts(t *testing.T) {
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 (
initTest sync.Once
rsaCAKey string
Expand Down
19 changes: 9 additions & 10 deletions builtin/logical/pki/crl_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ func (cb *crlBuilder) reloadConfigIfRequired(sc *storageContext) error {
}

previousConfig := cb.config

// Set the default config if none was returned to us.
if config != nil {
cb.config = *config
Expand Down Expand Up @@ -351,7 +350,7 @@ func (cb *crlBuilder) _getPresentDeltaWALForClearing(sc *storageContext, path st
// Clearing of the delta WAL occurs after a new complete CRL has been built.
walSerials, err := sc.Storage.List(sc.Context, path)
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
Expand All @@ -377,7 +376,7 @@ func (cb *crlBuilder) _clearDeltaWAL(sc *storageContext, walSerials []string, pa
}

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)
}
}

Expand Down Expand Up @@ -655,7 +654,7 @@ func (cb *crlBuilder) processRevocationQueue(sc *storageContext) error {
(!sc.Backend.System().LocalMount() && sc.Backend.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary))

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()
Expand Down Expand Up @@ -970,13 +969,13 @@ func revokeCert(sc *storageContext, config *crlConfig, cert *x509.Certificate) (

revEntry, err := logical.StorageEntryJSON(revokedPath+hyphenSerial, revInfo)
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()
err = sc.Storage.Put(sc.Context, revEntry)
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.incrementTotalRevokedCertificatesCount(certsCounted, revEntry.Key)

Expand Down Expand Up @@ -1082,7 +1081,7 @@ func writeSpecificRevocationDeltaWALs(sc *storageContext, hyphenSerial string, c
}

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
Expand All @@ -1094,7 +1093,7 @@ func writeSpecificRevocationDeltaWALs(sc *storageContext, hyphenSerial string, c
return fmt.Errorf("unable to create last delta CRL WAL entry")
}
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
Expand Down Expand Up @@ -1841,12 +1840,12 @@ func getLocalRevokedCertEntries(sc *storageContext, issuerIDCertMap map[issuerID
// we should update the entry to make future CRL builds faster.
revokedEntry, err = logical.StorageEntryJSON(revokedPath+serial, revInfo)
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)
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)
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions builtin/logical/pki/path_revoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"strings"
"time"

"github.com/hashicorp/vault/sdk/helper/consts"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/errutil"
Expand Down Expand Up @@ -490,6 +492,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()
defer b.revokeStorageLock.Unlock()

Expand Down
3 changes: 3 additions & 0 deletions changelog/19624.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
secrets/pki: Fix PKI revocation request forwarding from standby nodes due to an error wrapping bug
```

0 comments on commit d767d4c

Please sign in to comment.