open-vault/builtin/logical/pki/storage_migrations_test.go
Alexander Scheel 44c3b736bf
Allow tidy to backup legacy CA bundles (#18645)
* Allow tidy to backup legacy CA bundles

With the new tidy_move_legacy_ca_bundle option, we'll use tidy to move
the legacy CA bundle from /config/ca_bundle to /config/ca_bundle.bak.
This does two things:

 1. Removes ca_bundle from the hot-path of initialization after initial
    migration has completed. Because this entry is seal wrapped, this
    may result in performance improvements.
 2. Allows recovery of this value in the event of some other failure
    with migration.

Notably, this cannot occur during migration in the unlikely (and largely
unsupported) case that the operator immediately downgrades to Vault
<1.11.x. Thus, we reuse issuer_safety_buffer; while potentially long,
tidy can always be run manually with a shorter buffer (and only this
flag) to manually move the bundle if necessary.

In the event of needing to recover or undo this operation, it is
sufficient to use sys/raw to read the backed up value and subsequently
write it to its old path (/config/ca_bundle).

The new entry remains seal wrapped, but otherwise isn't used within the
code and so has better performance characteristics.

Performing a fat deletion (DELETE /root) will again remove the backup
like the old legacy bundle, preserving its wipe characteristics.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add documentation about new tidy parameter

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add tests for migration scenarios

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Clean up time comparisons

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
2023-01-11 12:12:53 -05:00

884 lines
33 KiB
Go

package pki
import (
"context"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/require"
)
func Test_migrateStorageEmptyStorage(t *testing.T) {
t.Parallel()
startTime := time.Now()
ctx := context.Background()
b, s := CreateBackendWithStorage(t)
sc := b.makeStorageContext(ctx, s)
// Reset the version the helper above set to 1.
b.pkiStorageVersion.Store(0)
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
request := &logical.InitializationRequest{Storage: s}
err := b.initialize(ctx, request)
require.NoError(t, err)
issuerIds, err := sc.listIssuers()
require.NoError(t, err)
require.Empty(t, issuerIds)
keyIds, err := sc.listKeys()
require.NoError(t, err)
require.Empty(t, keyIds)
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry)
require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion)
require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0,
"Hash value (%s) should not have been empty", logEntry.Hash)
require.True(t, startTime.Before(logEntry.Created),
"created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime)
require.Empty(t, logEntry.CreatedIssuer)
require.Empty(t, logEntry.CreatedKey)
require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage")
// Make sure we can re-run the migration without issues
request = &logical.InitializationRequest{Storage: s}
err = b.initialize(ctx, request)
require.NoError(t, err)
logEntry2, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry2)
// Make sure the hash and created times have not changed.
require.Equal(t, logEntry.Created, logEntry2.Created)
require.Equal(t, logEntry.Hash, logEntry2.Hash)
}
func Test_migrateStorageOnlyKey(t *testing.T) {
t.Parallel()
startTime := time.Now()
ctx := context.Background()
b, s := CreateBackendWithStorage(t)
sc := b.makeStorageContext(ctx, s)
// Reset the version the helper above set to 1.
b.pkiStorageVersion.Store(0)
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
bundle := genCertBundle(t, b, s)
// Clear everything except for the key
bundle.SerialNumber = ""
bundle.CAChain = []string{}
bundle.Certificate = ""
bundle.IssuingCA = ""
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
require.NoError(t, err)
err = s.Put(ctx, json)
require.NoError(t, err)
request := &logical.InitializationRequest{Storage: s}
err = b.initialize(ctx, request)
require.NoError(t, err)
issuerIds, err := sc.listIssuers()
require.NoError(t, err)
require.Equal(t, 0, len(issuerIds))
keyIds, err := sc.listKeys()
require.NoError(t, err)
require.Equal(t, 1, len(keyIds))
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry)
require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion)
require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0,
"Hash value (%s) should not have been empty", logEntry.Hash)
require.True(t, startTime.Before(logEntry.Created),
"created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime)
require.Equal(t, logEntry.CreatedIssuer, issuerID(""))
require.Equal(t, logEntry.CreatedKey, keyIds[0])
keyId := keyIds[0]
key, err := sc.fetchKeyById(keyId)
require.NoError(t, err)
require.True(t, strings.HasPrefix(key.Name, "current-"),
"expected key name to start with current- was %s", key.Name)
require.Equal(t, keyId, key.ID)
require.Equal(t, strings.TrimSpace(bundle.PrivateKey), strings.TrimSpace(key.PrivateKey))
require.Equal(t, bundle.PrivateKeyType, key.PrivateKeyType)
// Make sure we kept the old bundle
_, certBundle, err := getLegacyCertBundle(ctx, s)
require.NoError(t, err)
require.Equal(t, bundle, certBundle)
// Make sure we setup the default values
keysConfig, err := sc.getKeysConfig()
require.NoError(t, err)
require.Equal(t, &keyConfigEntry{DefaultKeyId: keyId}, keysConfig)
issuersConfig, err := sc.getIssuersConfig()
require.NoError(t, err)
require.Equal(t, issuerID(""), issuersConfig.DefaultIssuerId)
// Make sure if we attempt to re-run the migration nothing happens...
err = migrateStorage(ctx, b, s)
require.NoError(t, err)
logEntry2, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry2)
require.Equal(t, logEntry.Created, logEntry2.Created)
require.Equal(t, logEntry.Hash, logEntry2.Hash)
require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage")
}
func Test_migrateStorageSimpleBundle(t *testing.T) {
t.Parallel()
startTime := time.Now()
ctx := context.Background()
b, s := CreateBackendWithStorage(t)
sc := b.makeStorageContext(ctx, s)
// Reset the version the helper above set to 1.
b.pkiStorageVersion.Store(0)
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
bundle := genCertBundle(t, b, s)
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
require.NoError(t, err)
err = s.Put(ctx, json)
require.NoError(t, err)
request := &logical.InitializationRequest{Storage: s}
err = b.initialize(ctx, request)
require.NoError(t, err)
issuerIds, err := sc.listIssuers()
require.NoError(t, err)
require.Equal(t, 1, len(issuerIds))
keyIds, err := sc.listKeys()
require.NoError(t, err)
require.Equal(t, 1, len(keyIds))
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry)
require.Equal(t, latestMigrationVersion, logEntry.MigrationVersion)
require.True(t, len(strings.TrimSpace(logEntry.Hash)) > 0,
"Hash value (%s) should not have been empty", logEntry.Hash)
require.True(t, startTime.Before(logEntry.Created),
"created log entry time (%v) was before our start time(%v)?", logEntry.Created, startTime)
require.Equal(t, logEntry.CreatedIssuer, issuerIds[0])
require.Equal(t, logEntry.CreatedKey, keyIds[0])
issuerId := issuerIds[0]
keyId := keyIds[0]
issuer, err := sc.fetchIssuerById(issuerId)
require.NoError(t, err)
require.True(t, strings.HasPrefix(issuer.Name, "current-"),
"expected issuer name to start with current- was %s", issuer.Name)
require.Equal(t, certutil.ErrNotAfterBehavior, issuer.LeafNotAfterBehavior)
key, err := sc.fetchKeyById(keyId)
require.NoError(t, err)
require.True(t, strings.HasPrefix(key.Name, "current-"),
"expected key name to start with current- was %s", key.Name)
require.Equal(t, issuerId, issuer.ID)
require.Equal(t, bundle.SerialNumber, issuer.SerialNumber)
require.Equal(t, strings.TrimSpace(bundle.Certificate), strings.TrimSpace(issuer.Certificate))
require.Equal(t, keyId, issuer.KeyID)
require.Empty(t, issuer.ManualChain)
require.Equal(t, []string{bundle.Certificate + "\n"}, issuer.CAChain)
require.Equal(t, AllIssuerUsages, issuer.Usage)
require.Equal(t, certutil.ErrNotAfterBehavior, issuer.LeafNotAfterBehavior)
require.Equal(t, keyId, key.ID)
require.Equal(t, strings.TrimSpace(bundle.PrivateKey), strings.TrimSpace(key.PrivateKey))
require.Equal(t, bundle.PrivateKeyType, key.PrivateKeyType)
// Make sure we kept the old bundle
_, certBundle, err := getLegacyCertBundle(ctx, s)
require.NoError(t, err)
require.Equal(t, bundle, certBundle)
// Make sure we setup the default values
keysConfig, err := sc.getKeysConfig()
require.NoError(t, err)
require.Equal(t, &keyConfigEntry{DefaultKeyId: keyId}, keysConfig)
issuersConfig, err := sc.getIssuersConfig()
require.NoError(t, err)
require.Equal(t, issuerId, issuersConfig.DefaultIssuerId)
// Make sure if we attempt to re-run the migration nothing happens...
err = migrateStorage(ctx, b, s)
require.NoError(t, err)
logEntry2, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry2)
require.Equal(t, logEntry.Created, logEntry2.Created)
require.Equal(t, logEntry.Hash, logEntry2.Hash)
require.False(t, b.useLegacyBundleCaStorage(), "post migration we are still told to use legacy storage")
// Make sure we can re-process a migration from scratch for whatever reason
err = s.Delete(ctx, legacyMigrationBundleLogKey)
require.NoError(t, err)
err = migrateStorage(ctx, b, s)
require.NoError(t, err)
logEntry3, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry3)
require.NotEqual(t, logEntry.Created, logEntry3.Created)
require.Equal(t, logEntry.Hash, logEntry3.Hash)
}
func TestMigration_OnceChainRebuild(t *testing.T) {
t.Parallel()
ctx := context.Background()
b, s := CreateBackendWithStorage(t)
sc := b.makeStorageContext(ctx, s)
// Create a legacy CA bundle that we'll migrate to the new layout. We call
// ToParsedCertBundle just to make sure it works and to populate
// bundle.SerialNumber for us.
bundle := &certutil.CertBundle{
PrivateKeyType: certutil.RSAPrivateKey,
Certificate: migIntCA,
IssuingCA: migRootCA,
CAChain: []string{migRootCA},
PrivateKey: migIntPrivKey,
}
_, err := bundle.ToParsedCertBundle()
require.NoError(t, err)
writeLegacyBundle(t, b, s, bundle)
// Do an initial migration. Ensure we end up at least on version 2.
request := &logical.InitializationRequest{Storage: s}
err = b.initialize(ctx, request)
require.NoError(t, err)
issuerIds, err := sc.listIssuers()
require.NoError(t, err)
require.Equal(t, 2, len(issuerIds))
keyIds, err := sc.listKeys()
require.NoError(t, err)
require.Equal(t, 1, len(keyIds))
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry)
require.GreaterOrEqual(t, logEntry.MigrationVersion, 2)
require.GreaterOrEqual(t, latestMigrationVersion, 2)
// Verify the chain built correctly: current should have a CA chain of
// length two.
//
// Afterwards, we mutate these issuers to only point at themselves and
// write back out.
var rootIssuerId issuerID
var intIssuerId issuerID
for _, issuerId := range issuerIds {
issuer, err := sc.fetchIssuerById(issuerId)
require.NoError(t, err)
require.NotNil(t, issuer)
if strings.HasPrefix(issuer.Name, "current-") {
require.Equal(t, 2, len(issuer.CAChain))
require.Equal(t, migIntCA, issuer.CAChain[0])
require.Equal(t, migRootCA, issuer.CAChain[1])
intIssuerId = issuerId
issuer.CAChain = []string{migIntCA}
err = sc.writeIssuer(issuer)
require.NoError(t, err)
} else {
require.Equal(t, 1, len(issuer.CAChain))
require.Equal(t, migRootCA, issuer.CAChain[0])
rootIssuerId = issuerId
}
}
// Reset our migration version back to one, as if this never
// happened...
logEntry.MigrationVersion = 1
err = setLegacyBundleMigrationLog(ctx, s, logEntry)
require.NoError(t, err)
b.pkiStorageVersion.Store(1)
// Re-attempt the migration by reinitializing the mount.
err = b.initialize(ctx, request)
require.NoError(t, err)
newIssuerIds, err := sc.listIssuers()
require.NoError(t, err)
require.Equal(t, 2, len(newIssuerIds))
require.Equal(t, issuerIds, newIssuerIds)
newKeyIds, err := sc.listKeys()
require.NoError(t, err)
require.Equal(t, 1, len(newKeyIds))
require.Equal(t, keyIds, newKeyIds)
logEntry, err = getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
require.NotNil(t, logEntry)
require.Equal(t, logEntry.MigrationVersion, latestMigrationVersion)
// Ensure the chains are correct on the intermediate. By using the
// issuerId saved above, this ensures we didn't change any issuerIds,
// we merely updated the existing issuers.
intIssuer, err := sc.fetchIssuerById(intIssuerId)
require.NoError(t, err)
require.NotNil(t, intIssuer)
require.Equal(t, 2, len(intIssuer.CAChain))
require.Equal(t, migIntCA, intIssuer.CAChain[0])
require.Equal(t, migRootCA, intIssuer.CAChain[1])
rootIssuer, err := sc.fetchIssuerById(rootIssuerId)
require.NoError(t, err)
require.NotNil(t, rootIssuer)
require.Equal(t, 1, len(rootIssuer.CAChain))
require.Equal(t, migRootCA, rootIssuer.CAChain[0])
}
func TestExpectedOpsWork_PreMigration(t *testing.T) {
t.Parallel()
ctx := context.Background()
b, s := CreateBackendWithStorage(t)
// Reset the version the helper above set to 1.
b.pkiStorageVersion.Store(0)
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
bundle := genCertBundle(t, b, s)
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
require.NoError(t, err)
err = s.Put(ctx, json)
require.NoError(t, err)
// generate role
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/allow-all",
Storage: s,
Data: map[string]interface{}{
"allow_any_name": "true",
"no_store": "false",
},
MountPoint: "pki/",
})
require.NoError(t, err, "error from creating role")
require.NotNil(t, resp, "got nil response object from creating role")
// List roles
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ListOperation,
Path: "roles",
Storage: s,
MountPoint: "pki/",
})
require.NoError(t, err, "error from listing roles")
require.NotNil(t, resp, "got nil response object from listing roles")
require.False(t, resp.IsError(), "got error response from listing roles: %#v", resp)
require.Contains(t, resp.Data["keys"], "allow-all", "failed to list our roles")
// Read roles
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: "roles/allow-all",
Storage: s,
MountPoint: "pki/",
})
require.NoError(t, err, "error from reading role")
require.NotNil(t, resp, "got nil response object from reading role")
require.False(t, resp.IsError(), "got error response from reading role: %#v", resp)
require.NotEmpty(t, resp.Data, "data map should not have been empty of reading role")
// Issue a cert from our legacy bundle.
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "issue/allow-all",
Storage: s,
Data: map[string]interface{}{
"common_name": "test.com",
"ttl": "60s",
},
MountPoint: "pki/",
})
require.NoError(t, err, "error issue on allow-all")
require.NotNil(t, resp, "got nil response object from issue allow-all")
require.False(t, resp.IsError(), "got error response from issue on allow-all: %#v", resp)
serialNum := resp.Data["serial_number"].(string)
require.NotEmpty(t, serialNum)
// Make sure we can list
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ListOperation,
Path: "certs",
Storage: s,
MountPoint: "pki/",
})
require.NoError(t, err, "error listing certs")
require.NotNil(t, resp, "got nil response object from listing certs")
require.False(t, resp.IsError(), "got error response from listing certs: %#v", resp)
require.Contains(t, resp.Data["keys"], serialNum, "failed to list our cert")
// Revoke the cert now.
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "revoke",
Storage: s,
Data: map[string]interface{}{
"serial_number": serialNum,
},
MountPoint: "pki/",
})
require.NoError(t, err, "error revoking cert")
require.NotNil(t, resp, "got nil response object from revoke cert")
require.False(t, resp.IsError(), "got error response from revoke cert: %#v", resp)
// Check our CRL includes the revoked cert.
resp = requestCrlFromBackend(t, s, b)
crl := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte))
requireSerialNumberInCRL(t, crl, serialNum)
// Set CRL config
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/crl",
Storage: s,
Data: map[string]interface{}{
"expiry": "72h",
"disable": "false",
},
MountPoint: "pki/",
})
require.NoError(t, err, "error setting CRL config")
require.NotNil(t, resp, "got nil response setting CRL config")
// Set URL config
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/urls",
Storage: s,
Data: map[string]interface{}{
"ocsp_servers": []string{"https://localhost:8080"},
},
MountPoint: "pki/",
})
requireSuccessNonNilResponse(t, resp, err)
// Make sure we can fetch the old values...
for _, path := range []string{"ca/pem", "ca_chain", "cert/" + serialNum, "cert/ca", "cert/crl", "cert/ca_chain", "config/crl", "config/urls"} {
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: path,
Storage: s,
MountPoint: "pki/",
})
require.NoError(t, err, "error reading cert %s", path)
require.NotNil(t, resp, "got nil response object from reading cert %s", path)
require.False(t, resp.IsError(), "got error response from reading cert %s: %#v", path, resp)
}
// Sign CSR
_, csr := generateTestCsr(t, certutil.ECPrivateKey, 224)
for _, path := range []string{"sign/allow-all", "root/sign-intermediate", "sign-verbatim"} {
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: path,
Storage: s,
Data: map[string]interface{}{
"csr": csr,
},
MountPoint: "pki/",
})
require.NoError(t, err, "error signing csr from path %s", path)
require.NotNil(t, resp, "got nil response object from path %s", path)
require.NotEmpty(t, resp.Data, "data map response was empty from path %s", path)
}
// Sign self-issued
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "root/sign-self-issued",
Storage: s,
Data: map[string]interface{}{
"certificate": csr,
},
MountPoint: "pki/",
})
require.NoError(t, err, "error signing csr from path root/sign-self-issued")
require.NotNil(t, resp, "got nil response object from path root/sign-self-issued")
require.NotEmpty(t, resp.Data, "data map response was empty from path root/sign-self-issued")
// Delete Role
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.DeleteOperation,
Path: "roles/allow-all",
Storage: s,
MountPoint: "pki/",
})
require.NoError(t, err, "error deleting role")
require.Nil(t, resp, "got non-nil response object from deleting role")
// Delete Root
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.DeleteOperation,
Path: "root",
Storage: s,
MountPoint: "pki/",
})
require.NoError(t, err, "error deleting root")
require.NotNil(t, resp, "got nil response object from deleting root")
require.NotEmpty(t, resp.Warnings, "expected warnings set on delete root")
///////////////////////////////
// Legacy calls we expect to fail when in migration mode
///////////////////////////////
requireFailInMigration(t, b, s, logical.UpdateOperation, "config/ca")
requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/generate/internal")
requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/set-signed")
requireFailInMigration(t, b, s, logical.UpdateOperation, "root/generate/internal")
///////////////////////////////
// New apis should be unavailable
///////////////////////////////
requireFailInMigration(t, b, s, logical.ListOperation, "issuers")
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/generate/root/internal")
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/generate/intermediate/internal")
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuers/import/cert")
requireFailInMigration(t, b, s, logical.ReadOperation, "issuer/default/json")
requireFailInMigration(t, b, s, logical.ReadOperation, "issuer/default/crl/pem")
requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/test-role")
// The following calls work as they are shared handlers with existing paths.
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/issue/test-role")
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign/test-role")
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign-verbatim")
// requireFailInMigration(t, b, s, logical.UpdateOperation, "issuer/default/sign-self-issued")
requireFailInMigration(t, b, s, logical.UpdateOperation, "root/replace")
requireFailInMigration(t, b, s, logical.UpdateOperation, "root/rotate/internal")
requireFailInMigration(t, b, s, logical.UpdateOperation, "intermediate/cross-sign")
requireFailInMigration(t, b, s, logical.UpdateOperation, "config/issuers")
requireFailInMigration(t, b, s, logical.ReadOperation, "config/issuers")
requireFailInMigration(t, b, s, logical.ListOperation, "keys")
requireFailInMigration(t, b, s, logical.UpdateOperation, "keys/generate/internal")
requireFailInMigration(t, b, s, logical.UpdateOperation, "keys/import")
requireFailInMigration(t, b, s, logical.ReadOperation, "key/default")
requireFailInMigration(t, b, s, logical.UpdateOperation, "config/keys")
requireFailInMigration(t, b, s, logical.ReadOperation, "config/keys")
}
func TestBackupBundle(t *testing.T) {
t.Parallel()
ctx := context.Background()
b, s := CreateBackendWithStorage(t)
sc := b.makeStorageContext(ctx, s)
// Reset the version the helper above set to 1.
b.pkiStorageVersion.Store(0)
require.True(t, b.useLegacyBundleCaStorage(), "pre migration we should have been told to use legacy storage.")
// Create an empty request and tidy configuration for us.
req := &logical.Request{
Storage: s,
MountPoint: "pki/",
}
cfg := &tidyConfig{
BackupBundle: true,
IssuerSafetyBuffer: 120 * time.Second,
}
// Migration should do nothing if we're on an empty mount.
err := b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
require.NoError(t, err)
requireFileNotExists(t, sc, legacyCertBundlePath)
requireFileNotExists(t, sc, legacyCertBundleBackupPath)
issuerIds, err := sc.listIssuers()
require.NoError(t, err)
require.Empty(t, issuerIds)
keyIds, err := sc.listKeys()
require.NoError(t, err)
require.Empty(t, keyIds)
// Create a legacy CA bundle and write it out.
bundle := genCertBundle(t, b, s)
json, err := logical.StorageEntryJSON(legacyCertBundlePath, bundle)
require.NoError(t, err)
err = s.Put(ctx, json)
require.NoError(t, err)
legacyContents := requireFileExists(t, sc, legacyCertBundlePath, nil)
// Doing another tidy should maintain the status quo since we've
// still not done our migration.
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
require.NoError(t, err)
requireFileExists(t, sc, legacyCertBundlePath, legacyContents)
requireFileNotExists(t, sc, legacyCertBundleBackupPath)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.Empty(t, issuerIds)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.Empty(t, keyIds)
// Do a migration; this should provision an issuer and key.
initReq := &logical.InitializationRequest{Storage: s}
err = b.initialize(ctx, initReq)
require.NoError(t, err)
requireFileExists(t, sc, legacyCertBundlePath, legacyContents)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
// Doing another tidy should maintain the status quo since we've
// done our migration too recently relative to the safety buffer.
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
require.NoError(t, err)
requireFileExists(t, sc, legacyCertBundlePath, legacyContents)
requireFileNotExists(t, sc, legacyCertBundleBackupPath)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
// Shortening our buffer should ensure the migration occurs, removing
// the legacy bundle but creating the backup one.
time.Sleep(2 * time.Second)
cfg.IssuerSafetyBuffer = 1 * time.Second
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
require.NoError(t, err)
requireFileNotExists(t, sc, legacyCertBundlePath)
requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
// A new initialization should do nothing.
err = b.initialize(ctx, initReq)
require.NoError(t, err)
requireFileNotExists(t, sc, legacyCertBundlePath)
requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
require.Equal(t, len(issuerIds), 1)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
require.Equal(t, len(keyIds), 1)
// Restoring the legacy bundles with new issuers should redo the
// migration.
newBundle := genCertBundle(t, b, s)
json, err = logical.StorageEntryJSON(legacyCertBundlePath, newBundle)
require.NoError(t, err)
err = s.Put(ctx, json)
require.NoError(t, err)
newLegacyContents := requireFileExists(t, sc, legacyCertBundlePath, nil)
// -> reinit
err = b.initialize(ctx, initReq)
require.NoError(t, err)
requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents)
requireFileExists(t, sc, legacyCertBundleBackupPath, legacyContents)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
require.Equal(t, len(issuerIds), 2)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
require.Equal(t, len(keyIds), 2)
// -> when we tidy again, we'll overwrite the old backup with the new
// one.
time.Sleep(2 * time.Second)
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
require.NoError(t, err)
requireFileNotExists(t, sc, legacyCertBundlePath)
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
// Finally, restoring the legacy bundle and re-migrating should redo
// the migration.
err = s.Put(ctx, json)
require.NoError(t, err)
requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents)
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
// -> overwrite the version and re-migrate
logEntry, err := getLegacyBundleMigrationLog(ctx, s)
require.NoError(t, err)
logEntry.MigrationVersion = 0
err = setLegacyBundleMigrationLog(ctx, s, logEntry)
require.NoError(t, err)
err = b.initialize(ctx, initReq)
require.NoError(t, err)
requireFileExists(t, sc, legacyCertBundlePath, newLegacyContents)
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
require.Equal(t, len(issuerIds), 2)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
require.Equal(t, len(keyIds), 2)
// -> Re-tidy should remove the legacy one.
time.Sleep(2 * time.Second)
err = b.doTidyMoveCABundle(ctx, req, b.Logger(), cfg)
require.NoError(t, err)
requireFileNotExists(t, sc, legacyCertBundlePath)
requireFileExists(t, sc, legacyCertBundleBackupPath, newLegacyContents)
issuerIds, err = sc.listIssuers()
require.NoError(t, err)
require.NotEmpty(t, issuerIds)
keyIds, err = sc.listKeys()
require.NoError(t, err)
require.NotEmpty(t, keyIds)
}
// requireFailInMigration validate that we fail the operation with the appropriate error message to the end-user
func requireFailInMigration(t *testing.T, b *backend, s logical.Storage, operation logical.Operation, path string) {
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: operation,
Path: path,
Storage: s,
MountPoint: "pki/",
})
require.NoError(t, err, "error from op:%s path:%s", operation, path)
require.NotNil(t, resp, "got nil response from op:%s path:%s", operation, path)
require.True(t, resp.IsError(), "error flag was not set from op:%s path:%s resp: %#v", operation, path, resp)
require.Contains(t, resp.Error().Error(), "migration has completed",
"error message did not contain migration test for op:%s path:%s resp: %#v", operation, path, resp)
}
func requireFileNotExists(t *testing.T, sc *storageContext, path string) {
entry, err := sc.Storage.Get(sc.Context, path)
require.NoError(t, err)
if entry != nil {
require.Empty(t, entry.Value)
} else {
require.Empty(t, entry)
}
}
func requireFileExists(t *testing.T, sc *storageContext, path string, contents []byte) []byte {
entry, err := sc.Storage.Get(sc.Context, path)
require.NoError(t, err)
require.NotNil(t, entry)
require.NotEmpty(t, entry.Value)
if contents != nil {
require.Equal(t, entry.Value, contents)
}
return entry.Value
}
// Keys to simulate an intermediate CA mount with also-imported root (parent).
const (
migIntPrivKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqu88Jcct/EyT8gDF+jdWuAwFplvanQ7KXAO5at58G6Y39UUz
fwnMS3P3VRBUoV5BDX+13wI2ldskbTKITsl6IXBPXUz0sKrdEKzXRVY4D6P2JR7W
YO1IUytfTgR+3F4sotFNQB++3ivT66AYLW7lOkoa+5lxsPM/oJ82DOlD2uGtDVTU
gQy1zugMBgPDlj+8tB562J9MTIdcKe9JpYrN0eO+aHzhbfvaSpScU4aZBgkS0kDv
8G4FxVfrBSDeD/JjCWaC48rLdgei1YrY0NFuw/8p/nPfA9vf2AtHMsWZRSwukQfq
I5HhQu+0OHQy3NWqXaBPzJNu3HnpKLykPHW7sQIDAQABAoIBAHNJy/2G66MhWx98
Ggt7S4fyw9TCWx5XHXEWKfbEfFyBrXhF5kemqh2x5319+DamRaX/HwF8kqhcF6N2
06ygAzmOcFjzUI3fkB5xFPh1AHa8FYZP2DOjloZR2IPcUFv9QInINRwszSU31kUz
w1rRUtYPqUdM5Pt99Mo219O5eMSlGtPKXm09uDAR8ZPuUx4jwGw90pSgeRB1Bg7X
Dt3YXx3X+OOs3Hbir1VDLSqCuy825l6Kn79h3eB8LAi+FUwCBvnTqyOEWyH2XjgP
z+tbz7lwnhGeKtxUl6Jb3m3SHtXpylot/4fwPisRV/9vaEDhVjKTmySH1WM+TRNR
CQLCJekCgYEA3b67DBhAYsFFdUd/4xh4QhHBanOcepV1CwaRln+UUjw1618ZEsTS
DKb9IS72C+ukUusGhQqxjFJlhOdXeMXpEbnEUY3PlREevWwm3bVAxtoAVRcmkQyK
PM4Oj9ODi2z8Cds0NvEXdX69uVutcbvm/JRZr/dsERWcLsfwdV/QqYcCgYEAxVce
d4ylsqORLm0/gcLnEyB9zhEPwmiJe1Yj5sH7LhGZ6JtLCqbOJO4jXmIzCrkbGyuf
BA/U7klc6jSprkBMgYhgOIuaULuFJvtKzJUzoATGFqX4r8WJm2ZycXgooAwZq6SZ
ySXOuQe9V7hlpI0fJfNhw+/HIjivL1jrnjBoXwcCgYEAtTv6LLx1g0Frv5scj0Ok
pntUlei/8ADPlJ9dxp+nXj8P4rvrBkgPVX/2S3TSbJO/znWA8qP20TVW+/UIrRE0
mOQ37F/3VWKUuUT3zyUhOGVc+C7fupWBNolDpZG+ZepBZNzgJDeQcNuRvTmM3PQy
qiWl2AhlLuF2sVWA1q3lIWkCgYEAnuHWgNA3dE1nDWceE351hxvIzklEU/TQhAHF
o/uYHO5E6VdmoqvMG0W0KkCL8d046rZDMAUDHdrpOROvbcENF9lSBxS26LshqFH4
ViDmULanOgLk57f2Y6ynBZ6Frt4vKNe8jYuoFacale67vzFz251JoHSD8pSKz2cb
ROCal68CgYA51hKqvki4r5rmS7W/Yvc3x3Wc0wpDEHTgLMoH+EV7AffJ8dy0/+po
AHK0nnRU63++1JmhQczBR0yTI6PUyeegEBk/d5CgFlY7UJQMTFPsMsiuM0Xw5nAv
KMPykK01D28UAkUxhwF7CqFrwwEv9GislgjewbdF5Za176+EuMEwIw==
-----END RSA PRIVATE KEY-----
`
migIntCA = `-----BEGIN CERTIFICATE-----
MIIDHTCCAgWgAwIBAgIUfxlNBmrI7jsgH2Sdle1nVTqn5YQwDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAxMHUm9vdCBYMTAeFw0yMjExMDIxMjI2MjhaFw0yMjEyMDQx
MjI2NThaMBoxGDAWBgNVBAMTD0ludGVybWVkaWF0ZSBSMTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAKrvPCXHLfxMk/IAxfo3VrgMBaZb2p0OylwDuWre
fBumN/VFM38JzEtz91UQVKFeQQ1/td8CNpXbJG0yiE7JeiFwT11M9LCq3RCs10VW
OA+j9iUe1mDtSFMrX04EftxeLKLRTUAfvt4r0+ugGC1u5TpKGvuZcbDzP6CfNgzp
Q9rhrQ1U1IEMtc7oDAYDw5Y/vLQeetifTEyHXCnvSaWKzdHjvmh84W372kqUnFOG
mQYJEtJA7/BuBcVX6wUg3g/yYwlmguPKy3YHotWK2NDRbsP/Kf5z3wPb39gLRzLF
mUUsLpEH6iOR4ULvtDh0MtzVql2gT8yTbtx56Si8pDx1u7ECAwEAAaNjMGEwDgYD
VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFusWj3piAiY
CR7tszR6uNYSMLe2MB8GA1UdIwQYMBaAFMNRNkLozstIhNhXCefi+WnaQApbMA0G
CSqGSIb3DQEBCwUAA4IBAQCmH852E/pDGBhf2VI1JAPZy9VYaRkKoqn4+5R1Gnoq
b90zhdCGueIm/usC1wAa0OOn7+xdQXFNfeI8UUB9w10q0QnM/A/G2v8UkdlLPPQP
zPjIYLalOOIOHf8hU2O5lwj0IA4JwjwDQ4xj69eX/N+x2LEI7SHyVVUZWAx0Y67a
QdyubpIJZlW/PI7kMwGyTx3tdkZxk1nTNtf/0nKvNuXKKcVzBCEMfvXyx4LFEM+U
nc2vdWN7PAoXcjUbxD3ZNGinr7mSBpQg82+nur/8yuSwu6iHomnfGxjUsEHic2GC
ja9siTbR+ONvVb4xUjugN/XmMSSaZnxig2vM9xcV8OMG
-----END CERTIFICATE-----
`
migRootCA = `-----BEGIN CERTIFICATE-----
MIIDFTCCAf2gAwIBAgIURDTnXp8u78jWMe770Jj6Ac1paxkwDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAxMHUm9vdCBYMTAeFw0yMjExMDIxMjI0NTVaFw0yMjEyMDQx
MjI1MjRaMBIxEDAOBgNVBAMTB1Jvb3QgWDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQC/+dh/o1qKTOua/OkHRMIvHiyBxjjoqrLqFSBYhjYKs+alA0qS
lLVzNqIKU8jm3fT73orx7yk/6acWaEYv/6owMaUn51xwS3gQhTHdFR/fLJwXnu2O
PZNqAs6tjAM3Q08aqR0qfxnjDvcgO7TOWSyOvVT2cTRK+uKYzxJEY52BDMUbp+iC
WJdXca9UwKRzi2wFqGliDycYsBBt/tr8tHSbTSZ5Qx6UpFrKpjZn+sT5KhKUlsdd
BYFmRegc0wXq4/kRjum0oEUigUMlHADIEhRasyXPEKa19sGP8nAZfo/hNOusGhj7
z7UPA0Cbe2uclpYPxsKgvcqQmgKugqKLL305AgMBAAGjYzBhMA4GA1UdDwEB/wQE
AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTDUTZC6M7LSITYVwnn4vlp
2kAKWzAfBgNVHSMEGDAWgBTDUTZC6M7LSITYVwnn4vlp2kAKWzANBgkqhkiG9w0B
AQsFAAOCAQEAu7qdM1Li6V6iDCPpLg5zZReRtcxhUdwb5Xn4sDa8GJCy35f1voew
n0TQgM3Uph5x/djCR/Sj91MyAJ1/Q1PQQTyKGyUjSHvkcOBg628IAnLthn8Ua1fL
oQC/F/mlT1Yv+/W8eNPtD453/P0z8E0xMT5K3kpEDW/6K9RdHZlDJMW/z3UJ+4LN
6ONjIBmgffmLz9sVMpgCFyL7+w3W01bGP7w5AfKj2duoVG/Ekf2yUwmm6r9NgTQ1
oke0ShbZuMocwO8anq7k0R42FoluH3ipv9Qzzhsy+KdK5/fW5oqy1tKFaZsc67Q6
0UmD9DiDpCtn2Wod3nwxn0zW5HvDAWuDwg==
-----END CERTIFICATE-----
`
)