Backport Add an authenticated mode to rekey endpoints into ce/main (#12925)

This commit is contained in:
Vault Automation 2026-03-11 15:28:36 -04:00 committed by GitHub
parent 2ef4c50221
commit 99fec82771
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1520 additions and 512 deletions

3
changelog/_12712.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:change
core: sys/rekey endpoints are now authenticated by default, with the old unauthenticated behaviour enabled by setting the new HCL config key enable_unauthenticated_access to include the value "rekey".
```

View file

@ -2350,6 +2350,9 @@ func (c *ServerCommand) Reload(lock *sync.RWMutex, reloadFuncs *map[string][]rel
// Set Introspection Endpoint to enabled with new value in the config after reload
core.ReloadIntrospectionEndpointEnabled()
// Reload unauthenticated endpoints override configuration
core.ReloadEnableUnauthenticatedAccess()
// Send a message that we reloaded. This prevents "guessing" sleep times
// in tests.
select {
@ -3002,6 +3005,7 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical.
AdministrativeNamespacePath: config.AdministrativeNamespacePath,
ObservationSystemConfig: config.Observations,
ReportingScanDirectory: config.ReportingScanDirectory,
EnableUnauthenticatedAccess: config.EnableUnauthenticatedAccess,
}
if c.flagDev {

View file

@ -56,6 +56,8 @@ type Config struct {
Experiments []string `hcl:"experiments"`
EnableUnauthenticatedAccess []string `hcl:"enable_unauthenticated_access"`
CacheSize int `hcl:"cache_size"`
DisableCache bool `hcl:"-"`
DisableCacheRaw interface{} `hcl:"disable_cache"`
@ -520,6 +522,11 @@ func (c *Config) Merge(c2 *Config) *Config {
result.Experiments = mergeExperiments(c.Experiments, c2.Experiments)
result.EnableUnauthenticatedAccess = c.EnableUnauthenticatedAccess
if len(c2.EnableUnauthenticatedAccess) > 0 {
result.EnableUnauthenticatedAccess = c2.EnableUnauthenticatedAccess
}
return result
}
@ -1415,6 +1422,8 @@ func (c *Config) Sanitized() map[string]interface{} {
"log_requests_level": c.LogRequestsLevel,
"experiments": c.Experiments,
"enable_unauthenticated_access": c.EnableUnauthenticatedAccess,
"detect_deadlocks": c.DetectDeadlocks,
"imprecise_lease_role_tracking": c.ImpreciseLeaseRoleTracking,

View file

@ -943,6 +943,7 @@ func testConfig_Sanitized(t *testing.T) {
"post_unseal_trace_directory": "/tmp",
"remove_irrevocable_lease_after": (30 * 24 * time.Hour) / time.Second,
"allow_audit_log_prefixing": false,
"enable_unauthenticated_access": []string(nil),
}
addExpectedEntSanitizedConfig(expected, []string{"http"})

View file

@ -236,6 +236,19 @@ var _ vault.HandlerHandler = HandlerFunc(func(props *vault.HandlerProperties) ht
// handler returns an http.Handler for the API. This can be used on
// its own to mount the Vault API within another web server.
func handler(props *vault.HandlerProperties) http.Handler {
handlerUnauth := handlerWithUnauthRekey(props, true)
handlerAuth := handlerWithUnauthRekey(props, false)
return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
if props.Core.GetEnableUnauthRekey() {
handlerUnauth.ServeHTTP(writer, req)
} else {
handlerAuth.ServeHTTP(writer, req)
}
})
}
func handlerWithUnauthRekey(props *vault.HandlerProperties, unauthRekey bool) http.Handler {
core := props.Core
// Create the muxer to handle the actual endpoints
@ -275,12 +288,18 @@ func handler(props *vault.HandlerProperties) http.Handler {
handleAuditNonLogical(core, handleSysGenerateRootAttempt(core, vault.GenerateStandardRootTokenStrategy))))
mux.Handle("/v1/sys/generate-root/update", handleRequestForwarding(core,
handleAuditNonLogical(core, handleSysGenerateRootUpdate(core, vault.GenerateStandardRootTokenStrategy))))
mux.Handle("/v1/sys/rekey/init", handleRequestForwarding(core, handleSysRekeyInit(core, false)))
mux.Handle("/v1/sys/rekey/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, false)))
mux.Handle("/v1/sys/rekey/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, false)))
mux.Handle("/v1/sys/rekey-recovery-key/init", handleRequestForwarding(core, handleSysRekeyInit(core, true)))
mux.Handle("/v1/sys/rekey-recovery-key/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, true)))
mux.Handle("/v1/sys/rekey-recovery-key/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, true)))
// Register rekey endpoints as unauthenticated handlers only if unauthRekey is true.
// When false (the default), these endpoints will be handled by the sys backend as authenticated endpoints.
if unauthRekey {
mux.Handle("/v1/sys/rekey/init", handleRequestForwarding(core, handleSysRekeyInit(core, false)))
mux.Handle("/v1/sys/rekey/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, false)))
mux.Handle("/v1/sys/rekey/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, false)))
mux.Handle("/v1/sys/rekey-recovery-key/init", handleRequestForwarding(core, handleSysRekeyInit(core, true)))
mux.Handle("/v1/sys/rekey-recovery-key/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, true)))
mux.Handle("/v1/sys/rekey-recovery-key/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, true)))
}
mux.Handle("/v1/sys/storage/raft/bootstrap", handleSysRaftBootstrap(core))
mux.Handle("/v1/sys/storage/raft/join", handleSysRaftJoin(core))
mux.Handle("/v1/sys/internal/ui/feature-flags", handleSysInternalFeatureFlags(core))
@ -1482,6 +1501,23 @@ func respondErrorCommon(w http.ResponseWriter, req *logical.Request, resp *logic
respondErrorAndData(w, statusCode, data, newErr)
return true
}
if body := resp.Data[logical.HTTPRawBodyError]; body != nil {
if code := resp.Data[logical.HTTPStatusCode]; code != nil {
if i, ok := code.(int); ok {
// Defensively ignore non-int status codes
statusCode = i
}
}
switch v := body.(type) {
case string:
logical.RespondWithBody(w, statusCode, v)
case []byte:
logical.RespondWithBody(w, statusCode, string(v))
default:
respondError(w, http.StatusInternalServerError, fmt.Errorf("unable to decode body: %w", newErr))
}
return true
}
}
respondError(w, statusCode, newErr)
return true

View file

@ -183,6 +183,7 @@ func TestSysConfigState_Sanitized(t *testing.T) {
"post_unseal_trace_directory": "",
"remove_irrevocable_lease_after": json.Number("0"),
"allow_audit_log_prefixing": false,
"enable_unauthenticated_access": nil,
}
if tc.expectedHAStorageOutput != nil {

View file

@ -5,14 +5,10 @@ package http
import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net/http"
"time"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/vault"
)
@ -51,94 +47,25 @@ func handleSysRekeyInit(core *vault.Core, recovery bool) http.Handler {
}
func handleSysRekeyInitGet(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
barrierConfig, barrierConfErr := core.SealAccess().BarrierConfig(ctx)
if barrierConfErr != nil {
respondError(w, http.StatusInternalServerError, barrierConfErr)
return
}
if barrierConfig == nil {
respondError(w, http.StatusBadRequest, fmt.Errorf("server is not yet initialized"))
return
}
// Get the rekey configuration
rekeyConf, err := core.RekeyConfig(recovery)
status, code, err := vault.HandleSysRekeyInitGet(ctx, core, recovery, true)
if err != nil {
respondError(w, err.Code(), err)
respondError(w, code, err)
return
}
sealThreshold, err := core.RekeyThreshold(ctx, recovery)
if err != nil {
respondError(w, err.Code(), err)
return
}
// Format the status
status := &RekeyStatusResponse{
Started: false,
T: 0,
N: 0,
Required: sealThreshold,
}
if rekeyConf != nil {
// Get the progress
started, progress, err := core.RekeyProgress(recovery, false)
if err != nil {
respondError(w, err.Code(), err)
return
}
status.Nonce = rekeyConf.Nonce
status.Started = started
status.T = rekeyConf.SecretThreshold
status.N = rekeyConf.SecretShares
status.Progress = progress
status.VerificationRequired = rekeyConf.VerificationRequired
status.VerificationNonce = rekeyConf.VerificationNonce
if rekeyConf.PGPKeys != nil && len(rekeyConf.PGPKeys) != 0 {
pgpFingerprints, err := pgpkeys.GetFingerprints(rekeyConf.PGPKeys, nil)
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
status.PGPFingerprints = pgpFingerprints
status.Backup = rekeyConf.Backup
}
}
respondOk(w, status)
}
func handleSysRekeyInitPut(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
// Parse the request
var req RekeyRequest
var req *vault.RekeyRequest
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
if req.Backup && len(req.PGPKeys) == 0 {
respondError(w, http.StatusBadRequest, fmt.Errorf("cannot request a backup of the new keys without providing PGP keys for encryption"))
return
}
if len(req.PGPKeys) > 0 && len(req.PGPKeys) != req.SecretShares {
respondError(w, http.StatusBadRequest, fmt.Errorf("incorrect number of PGP keys for rekey"))
return
}
// Initialize the rekey
err := core.RekeyInit(&vault.SealConfig{
SecretShares: req.SecretShares,
SecretThreshold: req.SecretThreshold,
StoredShares: req.StoredShares,
PGPKeys: req.PGPKeys,
Backup: req.Backup,
VerificationRequired: req.RequireVerification,
Created: time.Now().UTC(),
}, recovery)
code, err := vault.HandleSysRekeyInitPut(core, recovery, req, true)
if err != nil {
respondError(w, err.Code(), err)
respondError(w, code, err)
return
}
@ -146,13 +73,13 @@ func handleSysRekeyInitPut(ctx context.Context, core *vault.Core, recovery bool,
}
func handleSysRekeyInitDelete(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
var req RekeyDeleteRequest
var req vault.RekeyDeleteRequest
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
if err := core.RekeyCancel(recovery, req.Nonce, 10*time.Minute); err != nil {
if err := core.RekeyCancel(recovery, req.Nonce, 10*time.Minute, true); err != nil {
respondError(w, err.Code(), err)
return
}
@ -168,67 +95,26 @@ func handleSysRekeyUpdate(core *vault.Core, recovery bool) http.Handler {
}
// Parse the request
var req RekeyUpdateRequest
var req vault.RekeyUpdateRequest
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
if req.Key == "" {
respondError(
w, http.StatusBadRequest,
errors.New("'key' must be specified in request body as JSON"))
return
}
// Decode the key, which is base64 or hex encoded
min, max := core.BarrierKeyLength()
key, err := hex.DecodeString(req.Key)
// We check min and max here to ensure that a string that is base64
// encoded but also valid hex will not be valid and we instead base64
// decode it
if err != nil || len(key) < min || len(key) > max {
key, err = base64.StdEncoding.DecodeString(req.Key)
if err != nil {
respondError(
w, http.StatusBadRequest,
errors.New("'key' must be a valid hex or base64 string"))
return
}
}
ctx, cancel := core.GetContext()
defer cancel()
// Use the key to make progress on rekey
result, rekeyErr := core.RekeyUpdate(ctx, key, req.Nonce, recovery)
if rekeyErr != nil {
respondError(w, rekeyErr.Code(), rekeyErr)
result, code, err := vault.HandleSysRekeyUpdatePut(ctx, core, recovery, &req, true)
if err != nil {
respondError(w, code, err)
return
}
if result != nil {
respondOk(w, result)
return
}
// Format the response
resp := &RekeyUpdateResponse{}
if result != nil {
resp.Complete = true
resp.Nonce = req.Nonce
resp.Backup = result.Backup
resp.PGPFingerprints = result.PGPFingerprints
resp.VerificationRequired = result.VerificationRequired
resp.VerificationNonce = result.VerificationNonce
// Encode the keys
keys := make([]string, 0, len(result.SecretShares))
keysB64 := make([]string, 0, len(result.SecretShares))
for _, k := range result.SecretShares {
keys = append(keys, hex.EncodeToString(k))
keysB64 = append(keysB64, base64.StdEncoding.EncodeToString(k))
}
resp.Keys = keys
resp.KeysB64 = keysB64
respondOk(w, resp)
} else {
handleSysRekeyInitGet(ctx, core, recovery, w, r)
}
handleSysRekeyInitGet(r.Context(), core, recovery, w, r)
})
}
@ -266,47 +152,17 @@ func handleSysRekeyVerify(core *vault.Core, recovery bool) http.Handler {
}
func handleSysRekeyVerifyGet(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
barrierConfig, barrierConfErr := core.SealAccess().BarrierConfig(ctx)
if barrierConfErr != nil {
respondError(w, http.StatusInternalServerError, barrierConfErr)
return
}
if barrierConfig == nil {
respondError(w, http.StatusBadRequest, fmt.Errorf("server is not yet initialized"))
return
}
// Get the rekey configuration
rekeyConf, err := core.RekeyConfig(recovery)
status, code, err := vault.HandleSysRekeyVerifyGet(ctx, core, recovery, true)
if err != nil {
respondError(w, err.Code(), err)
return
}
if rekeyConf == nil {
respondError(w, http.StatusBadRequest, errors.New("no rekey configuration found"))
respondError(w, code, err)
return
}
// Get the progress
started, progress, err := core.RekeyProgress(recovery, true)
if err != nil {
respondError(w, err.Code(), err)
return
}
// Format the status
status := &RekeyVerificationStatusResponse{
Started: started,
Nonce: rekeyConf.VerificationNonce,
T: rekeyConf.SecretThreshold,
N: rekeyConf.SecretShares,
Progress: progress,
}
respondOk(w, status)
}
func handleSysRekeyVerifyDelete(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
if err := core.RekeyVerifyRestart(recovery); err != nil {
if err := core.RekeyVerifyRestart(recovery, true); err != nil {
respondError(w, err.Code(), err)
return
}
@ -316,112 +172,25 @@ func handleSysRekeyVerifyDelete(ctx context.Context, core *vault.Core, recovery
func handleSysRekeyVerifyPut(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
// Parse the request
var req RekeyVerificationUpdateRequest
var req vault.RekeyVerificationUpdateRequest
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
if req.Key == "" {
respondError(
w, http.StatusBadRequest,
errors.New("'key' must be specified in request body as JSON"))
return
}
// Decode the key, which is base64 or hex encoded
min, max := core.BarrierKeyLength()
key, err := hex.DecodeString(req.Key)
// We check min and max here to ensure that a string that is base64
// encoded but also valid hex will not be valid and we instead base64
// decode it
if err != nil || len(key) < min || len(key) > max {
key, err = base64.StdEncoding.DecodeString(req.Key)
if err != nil {
respondError(
w, http.StatusBadRequest,
errors.New("'key' must be a valid hex or base64 string"))
return
}
}
ctx, cancel := core.GetContext()
defer cancel()
// Use the key to make progress on rekey
result, rekeyErr := core.RekeyVerify(ctx, key, req.Nonce, recovery)
if rekeyErr != nil {
respondError(w, rekeyErr.Code(), rekeyErr)
resp, code, err := vault.HandleSysRekeyVerifyPut(ctx, core, recovery, true, &req)
if err != nil {
respondError(w, code, err)
return
}
// Format the response
resp := &RekeyVerificationUpdateResponse{}
if result != nil {
resp.Complete = true
resp.Nonce = result.Nonce
if resp != nil {
respondOk(w, resp)
} else {
handleSysRekeyVerifyGet(ctx, core, recovery, w, r)
}
}
type RekeyRequest struct {
SecretShares int `json:"secret_shares"`
SecretThreshold int `json:"secret_threshold"`
StoredShares int `json:"stored_shares"`
PGPKeys []string `json:"pgp_keys"`
Backup bool `json:"backup"`
RequireVerification bool `json:"require_verification"`
}
type RekeyStatusResponse struct {
Nonce string `json:"nonce"`
Started bool `json:"started"`
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
Required int `json:"required"`
PGPFingerprints []string `json:"pgp_fingerprints"`
Backup bool `json:"backup"`
VerificationRequired bool `json:"verification_required"`
VerificationNonce string `json:"verification_nonce,omitempty"`
}
type RekeyUpdateRequest struct {
Nonce string
Key string
}
type RekeyUpdateResponse struct {
Nonce string `json:"nonce"`
Complete bool `json:"complete"`
Keys []string `json:"keys"`
KeysB64 []string `json:"keys_base64"`
PGPFingerprints []string `json:"pgp_fingerprints"`
Backup bool `json:"backup"`
VerificationRequired bool `json:"verification_required"`
VerificationNonce string `json:"verification_nonce,omitempty"`
}
type RekeyVerificationUpdateRequest struct {
Nonce string `json:"nonce"`
Key string `json:"key"`
}
type RekeyVerificationStatusResponse struct {
Nonce string `json:"nonce"`
Started bool `json:"started"`
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
}
type RekeyVerificationUpdateResponse struct {
Nonce string `json:"nonce"`
Complete bool `json:"complete"`
}
type RekeyDeleteRequest struct {
Nonce string `json:"nonce"`
Key string `json:"key"`
}

View file

@ -7,20 +7,21 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/vault/sdk/helper/testhelpers/schema"
"github.com/hashicorp/vault/vault"
"github.com/stretchr/testify/require"
)
// Test to check if the API errors out when wrong number of PGP keys are
// supplied for rekey
func TestSysRekey_Init_pgpKeysEntriesForRekey(t *testing.T) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: Handler,
RequestResponseCallback: schema.ResponseValidatingCallback(t),
HandlerFunc: Handler,
NumCores: 1,
})
cluster.Start()
defer cluster.Cleanup()
@ -37,44 +38,93 @@ func TestSysRekey_Init_pgpKeysEntriesForRekey(t *testing.T) {
}
func TestSysRekey_Init_Status(t *testing.T) {
t.Run("status-barrier-default", func(t *testing.T) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: Handler,
RequestResponseCallback: schema.ResponseValidatingCallback(t),
})
cluster.Start()
defer cluster.Cleanup()
cl := cluster.Cores[0].Client
resp, err := cl.Logical().Read("sys/rekey/init")
if err != nil {
t.Fatalf("err: %s", err)
}
actual := resp.Data
expected := map[string]interface{}{
"started": false,
"t": json.Number("0"),
"n": json.Number("0"),
"progress": json.Number("0"),
"required": json.Number("3"),
"pgp_fingerprints": interface{}(nil),
"backup": false,
"nonce": "",
"verification_required": false,
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: Handler,
NumCores: 1,
})
defer cluster.Cleanup()
cl := cluster.Cores[0].Client
testCases := []struct {
name string
enableUnauthRekey bool
useToken bool
expectError bool
}{
{
name: "default-unauthenticated",
enableUnauthRekey: true,
useToken: false,
expectError: false,
},
{
name: "default-authenticated",
enableUnauthRekey: true,
useToken: true,
expectError: false,
},
{
name: "auth-required-without-token",
enableUnauthRekey: false,
useToken: false,
expectError: true,
},
{
name: "auth-required-with-token",
enableUnauthRekey: false,
useToken: true,
expectError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cluster.Cores[0].Core.SetEnableUnauthRekey(tc.enableUnauthRekey)
if tc.useToken {
cl.SetToken(cluster.RootToken)
} else {
cl.SetToken("")
}
resp, err := cl.Logical().Read("sys/rekey/init")
if tc.expectError {
if err == nil {
t.Fatal("expected error but got none")
}
return
}
if err != nil {
t.Fatalf("err: %s", err)
}
actual := resp.Data
expected := map[string]interface{}{
"started": false,
"t": json.Number("0"),
"n": json.Number("0"),
"progress": json.Number("0"),
"required": json.Number("3"),
"pgp_fingerprints": interface{}(nil),
"backup": false,
"nonce": "",
"verification_required": false,
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual)
}
})
}
}
func TestSysRekey_Init_Setup(t *testing.T) {
t.Run("init-barrier-barrier-key", func(t *testing.T) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: Handler,
RequestResponseCallback: schema.ResponseValidatingCallback(t),
HandlerFunc: Handler,
NumCores: 1,
})
cluster.Start()
defer cluster.Cleanup()
@ -142,10 +192,9 @@ func TestSysRekey_Init_Setup(t *testing.T) {
func TestSysRekey_Init_Cancel(t *testing.T) {
t.Run("cancel-barrier-barrier-key", func(t *testing.T) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: Handler,
RequestResponseCallback: schema.ResponseValidatingCallback(t),
HandlerFunc: Handler,
NumCores: 1,
})
cluster.Start()
defer cluster.Cleanup()
cl := cluster.Cores[0].Client
@ -198,73 +247,147 @@ func TestSysRekey_badKey(t *testing.T) {
}
func TestSysRekey_Update(t *testing.T) {
t.Run("rekey-barrier-barrier-key", func(t *testing.T) {
core, keys, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
testCases := []struct {
name string
enableUnauthRekey bool
useToken bool
expectInitError bool
expectUpdateError bool
}{
{
name: "unauthenticated",
enableUnauthRekey: true,
useToken: false,
expectInitError: false,
expectUpdateError: false,
},
{
name: "authenticated",
enableUnauthRekey: true,
useToken: true,
expectInitError: false,
expectUpdateError: false,
},
{
name: "auth-required",
enableUnauthRekey: false,
useToken: true,
expectInitError: false,
expectUpdateError: false,
},
{
name: "auth-required-no-token",
enableUnauthRekey: false,
useToken: false,
expectInitError: true,
expectUpdateError: true,
},
}
resp := testHttpPut(t, token, addr+"/v1/sys/rekey/init", map[string]interface{}{
"secret_shares": 5,
"secret_threshold": 3,
})
var rekeyStatus map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &rekeyStatus)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conf := &vault.CoreConfig{}
if tc.enableUnauthRekey {
conf.EnableUnauthenticatedAccess = []string{"rekey"}
}
cluster := vault.NewTestCluster(t, conf, &vault.TestClusterOptions{
DisableTLS: true,
HandlerFunc: Handler,
NumCores: 1,
})
defer cluster.Cleanup()
cl := cluster.Cores[0].Client
var actual map[string]interface{}
var expected map[string]interface{}
reqToken := ""
if tc.useToken {
reqToken = cl.Token()
}
addr := cl.Address()
for i, key := range keys {
resp = testHttpPut(t, token, addr+"/v1/sys/rekey/update", map[string]interface{}{
"nonce": rekeyStatus["nonce"].(string),
"key": hex.EncodeToString(key),
resp := testHttpPut(t, reqToken, addr+"/v1/sys/rekey/init", map[string]interface{}{
"secret_shares": 5,
"secret_threshold": 3,
})
actual = map[string]interface{}{}
expected = map[string]interface{}{
"started": true,
"nonce": rekeyStatus["nonce"].(string),
"backup": false,
"pgp_fingerprints": interface{}(nil),
"required": json.Number("3"),
"t": json.Number("3"),
"n": json.Number("5"),
"progress": json.Number(fmt.Sprintf("%d", i+1)),
"verification_required": false,
if tc.expectInitError {
testResponseStatus(t, resp, http.StatusForbidden)
return
}
var rekeyStatus map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
testResponseBody(t, resp, &rekeyStatus)
if i+1 == len(keys) {
delete(expected, "started")
delete(expected, "required")
delete(expected, "t")
delete(expected, "n")
delete(expected, "progress")
expected["complete"] = true
expected["keys"] = actual["keys"]
expected["keys_base64"] = actual["keys_base64"]
var actual map[string]interface{}
var expected map[string]interface{}
if !tc.expectUpdateError {
// Test with a bad key to ensure that we format errors the same way
resp = testHttpPut(t, reqToken, addr+"/v1/sys/rekey/update", map[string]interface{}{
"nonce": rekeyStatus["nonce"].(string),
"key": hex.EncodeToString([]byte("badkey")),
})
testResponseStatus(t, resp, http.StatusBadRequest)
testResponseBody(t, resp, &actual)
require.Equal(t, map[string]any{"errors": []any{"key is shorter than minimum 16 bytes"}}, actual)
}
if i+1 < len(keys) && (actual["nonce"] == nil || actual["nonce"].(string) == "") {
t.Fatalf("expected a nonce, i is %d, actual is %#v", i, actual)
for i, key := range cluster.BarrierKeys {
resp = testHttpPut(t, reqToken, addr+"/v1/sys/rekey/update", map[string]interface{}{
"nonce": rekeyStatus["nonce"].(string),
"key": hex.EncodeToString(key),
})
if tc.expectUpdateError {
testResponseStatus(t, resp, http.StatusForbidden)
return
}
actual = map[string]interface{}{}
expected = map[string]interface{}{
"started": true,
"nonce": rekeyStatus["nonce"].(string),
"backup": false,
"pgp_fingerprints": interface{}(nil),
"required": json.Number("3"),
"t": json.Number("3"),
"n": json.Number("5"),
"progress": json.Number(fmt.Sprintf("%d", i+1)),
"verification_required": false,
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if i+1 == len(cluster.BarrierKeys) {
delete(expected, "started")
delete(expected, "required")
delete(expected, "t")
delete(expected, "n")
delete(expected, "progress")
expected["complete"] = true
expected["keys"] = actual["keys"]
expected["keys_base64"] = actual["keys_base64"]
}
if i+1 < len(cluster.BarrierKeys) && (actual["nonce"] == nil || actual["nonce"].(string) == "") {
t.Fatalf("expected a nonce, i is %d, actual is %#v", i, actual)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: \n%#v\nactual: \n%#v", expected, actual)
}
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("\nexpected: \n%#v\nactual: \n%#v", expected, actual)
retKeys := actual["keys"].([]interface{})
if len(retKeys) != 5 {
t.Fatalf("bad: %#v", retKeys)
}
}
retKeys := actual["keys"].([]interface{})
if len(retKeys) != 5 {
t.Fatalf("bad: %#v", retKeys)
}
keysB64 := actual["keys_base64"].([]interface{})
if len(keysB64) != 5 {
t.Fatalf("bad: %#v", keysB64)
}
})
keysB64 := actual["keys_base64"].([]interface{})
if len(keysB64) != 5 {
t.Fatalf("bad: %#v", keysB64)
}
})
}
}
func TestSysRekey_ReInitUpdate(t *testing.T) {

View file

@ -443,7 +443,7 @@ func (d *Runner) Start(ctx context.Context, addSuffix, forceLocalAddr bool) (*St
}
for from, to := range d.RunOptions.CopyFromTo {
if err := copyToContainer(ctx, d.DockerAPI, c.ID, from, to); err != nil {
if err := CopyToContainer(ctx, d.DockerAPI, c.ID, from, to); err != nil {
_ = d.DockerAPI.ContainerRemove(ctx, c.ID, container.RemoveOptions{})
return nil, err
}
@ -500,7 +500,7 @@ func (d *Runner) Start(ctx context.Context, addSuffix, forceLocalAddr bool) (*St
func (d *Runner) RefreshFiles(ctx context.Context, containerID string) error {
for from, to := range d.RunOptions.CopyFromTo {
if err := copyToContainer(ctx, d.DockerAPI, containerID, from, to); err != nil {
if err := CopyToContainer(ctx, d.DockerAPI, containerID, from, to); err != nil {
// TODO too drastic?
_ = d.DockerAPI.ContainerRemove(ctx, containerID, container.RemoveOptions{})
return err
@ -555,7 +555,7 @@ func (d *Runner) Restart(ctx context.Context, containerID string) error {
return d.DockerAPI.NetworkConnect(ctx, d.RunOptions.NetworkID, containerID, ends)
}
func copyToContainer(ctx context.Context, dapi *client.Client, containerID, from, to string) error {
func CopyToContainer(ctx context.Context, dapi *client.Client, containerID, from, to string) error {
srcInfo, err := archive.CopyInfoSourcePath(from, false)
if err != nil {
return fmt.Errorf("error copying from source %q: %v", from, err)

View file

@ -1021,6 +1021,31 @@ func (n *DockerClusterNode) Restart(ctx context.Context) error {
return nil
}
func (n *DockerClusterNode) Signal(ctx context.Context, signal string) error {
return n.DockerAPI.ContainerKill(ctx, n.Container.ID, signal)
}
func (n *DockerClusterNode) UpdateConfig(ctx context.Context, config *testcluster.VaultNodeConfig) error {
// Marshal the config to JSON
configJSON, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// Write the config to the work directory
configPath := filepath.Join(n.WorkDir, "user.json")
if err := os.WriteFile(configPath, configJSON, 0o644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
// Copy the updated config to the container
if err := dockhelper.CopyToContainer(ctx, n.DockerAPI, n.Container.ID, configPath, "/vault/config/user.json"); err != nil {
return fmt.Errorf("failed to copy config to container: %w", err)
}
return nil
}
func (n *DockerClusterNode) AddNetworkDelay(ctx context.Context, delay time.Duration, targetIP string) error {
ip := net.ParseIP(targetIP)
if ip == nil {

View file

@ -83,6 +83,7 @@ type VaultNodeConfig struct {
EnableResponseHeaderRaftNodeID bool `json:"enable_response_header_raft_node_id"`
LicensePath string `json:"license_path"`
FeatureFlags []string `json:"feature_flags,omitempty"`
EnableUnauthenticatedAccess []string `json:"enable_unauthenticated_access,omitempty"`
}
type ClusterNode struct {

View file

@ -29,6 +29,12 @@ const (
// avoided like the HTTPContentType. The value must be a byte slice.
HTTPRawBody = "http_raw_body"
// HTTPRawBodyError is similar to HTTPRawBody. The difference is that
// HTTPRawBodyError is specifically intended for endpoints that want to manage
// their error response directly. This was added to mitigate the risk of
// causing regressions in the error responses of existing HTTPRawBody users.
HTTPRawBodyError = "http_raw_body_error"
// HTTPStatusCode is the response code of the HTTP body that goes with the HTTPContentType.
// This can only be specified for non-secrets, and should should be similarly
// avoided like the HTTPContentType. The value must be an integer.

View file

@ -4,6 +4,7 @@
package logical
import (
"bytes"
"encoding/json"
"errors"
"fmt"
@ -196,24 +197,38 @@ func AdjustErrorStatusCode(status *int, err error) {
}
}
type errorResponse struct {
Errors []string `json:"errors"`
}
// GenerateNonLogicalErrorResponse returns a struct that can be serialized to
// JSON as part of reporting an error to callers. It is used by some older APIs
// that live in the http layer which don't use logical.Response, as well as by
// some that do but are emulating the older ones.
func GenerateNonLogicalErrorResponse(status int, err error) *errorResponse {
resp := &errorResponse{Errors: make([]string, 0, 1)}
if err != nil {
resp.Errors = append(resp.Errors, err.Error())
}
return resp
}
func RespondError(w http.ResponseWriter, status int, err error) {
AdjustErrorStatusCode(&status, err)
defer IncrementResponseStatusCodeMetric(status)
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.Encode(GenerateNonLogicalErrorResponse(status, err))
RespondWithBody(w, status, b.String())
}
func RespondWithBody(w http.ResponseWriter, status int, body string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
type ErrorResponse struct {
Errors []string `json:"errors"`
}
resp := &ErrorResponse{Errors: make([]string, 0, 1)}
if err != nil {
resp.Errors = append(resp.Errors, err.Error())
}
enc := json.NewEncoder(w)
enc.Encode(resp)
w.Write([]byte(body))
}
func RespondErrorAndData(w http.ResponseWriter, status int, data interface{}, err error) {

View file

@ -274,6 +274,11 @@ type Core struct {
// the generate-root process simply to talk to the new follower cluster.
devToken string
// enableUnauthRekey controls whether rekey endpoints are registered as
// unauthenticated endpoints (true) or as authenticated sys backend
// endpoints (false, default).
enableUnauthRekey *atomic.Bool
// HABackend may be available depending on the physical backend
ha physical.HABackend
@ -971,6 +976,12 @@ type CoreConfig struct {
// ReportingScanDirectory is where files generated by /sys/reporting/scan will go.
ReportingScanDirectory string
// EnableUnauthenticatedAccess is a list of endpoint names that should be
// accessible without authentication, despite them being by default authenticated.
// These aren't the actual paths to endpoints, but rather specific values that
// identify groups of endpoints, e.g. "rekey" refers to the sys/rekey/* endpoints.
EnableUnauthenticatedAccess []string
}
// GetServiceRegistration returns the config's ServiceRegistration, or nil if it does
@ -1155,6 +1166,7 @@ func CreateCore(conf *CoreConfig) (*Core, error) {
periodicLeaderRefreshInterval: conf.PeriodicLeaderRefreshInterval,
rpcLastSuccessfulHeartbeat: new(atomic.Value),
reportingScanDirectory: conf.ReportingScanDirectory,
enableUnauthRekey: new(atomic.Bool),
}
c.certCountManager = cert_count.InitCertificateCountManager(c.logger)
@ -1437,6 +1449,15 @@ func NewCore(conf *CoreConfig) (*Core, error) {
c.clusterAddrBridge = conf.ClusterAddrBridge
c.licenseReloadCh = conf.LicenseReload
// Check if "rekey" is in the EnableUnauthenticatedAccess list
for _, endpoint := range conf.EnableUnauthenticatedAccess {
if endpoint == "rekey" {
c.enableUnauthRekey.Store(true)
break
}
}
return c, nil
}
@ -4434,6 +4455,24 @@ func (c *Core) ReloadIntrospectionEndpointEnabled() {
c.introspectionEnabled = conf.(*server.Config).EnableIntrospectionEndpoint
}
func (c *Core) ReloadEnableUnauthenticatedAccess() {
conf := c.rawConfig.Load()
if conf == nil {
return
}
// Check if "rekey" is in the EnableUnauthenticatedAccess list
enableRekey := false
for _, endpoint := range conf.(*server.Config).EnableUnauthenticatedAccess {
if endpoint == "rekey" {
enableRekey = true
break
}
}
c.enableUnauthRekey.Store(enableRekey)
}
type PeerNode struct {
Hostname string `json:"hostname"`
APIAddress string `json:"api_address"`
@ -4918,3 +4957,11 @@ var errRemovedHANode = errors.New("node has been removed from the HA cluster")
func (c *Core) CoreNumber() int {
return c.coreNumber
}
func (c *Core) GetEnableUnauthRekey() bool {
return c.enableUnauthRekey.Load()
}
func (c *Core) SetEnableUnauthRekey(val bool) {
c.enableUnauthRekey.Store(val)
}

View file

@ -40,6 +40,7 @@ func TestSysRekey_Verification(t *testing.T) {
func testSysRekey_Verification(t *testing.T, recovery bool, legacyShamir bool) {
opts := &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
NumCores: 1,
}
switch {
case recovery:

View file

@ -0,0 +1,187 @@
// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1
package system_binary
import (
"os"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/testcluster"
"github.com/hashicorp/vault/sdk/helper/testcluster/docker"
"github.com/stretchr/testify/require"
)
// waitForRekeyInConfig polls sys/config/state/sanitized until the rekey endpoint
// appears or disappears from enable_unauthenticated_access based on shouldBePresent.
func waitForRekeyInConfig(t *testing.T, client *api.Client, rootToken string, shouldBePresent bool) {
clientWithAuth, err := client.Clone()
require.NoError(t, err, "failed to clone client")
clientWithAuth.SetToken(rootToken)
require.Eventually(t, func() bool {
resp, err := clientWithAuth.Logical().Read("sys/config/state/sanitized")
if err != nil {
t.Logf("error reading config state: %v", err)
return false
}
if resp == nil || resp.Data == nil {
t.Logf("nil response or data from config state")
return false
}
override, ok := resp.Data["enable_unauthenticated_access"]
if !ok {
// If the field is not present, rekey is not in the override list
return !shouldBePresent
}
// Check if override contains "rekey"
rekeyFound := false
if overrideSlice, ok := override.([]interface{}); ok {
for _, v := range overrideSlice {
if str, ok := v.(string); ok && str == "rekey" {
rekeyFound = true
break
}
}
}
if shouldBePresent {
return rekeyFound
}
return !rekeyFound
}, 10*time.Second, 100*time.Millisecond, "rekey presence in enable_unauthenticated_access did not match expected state")
}
// TestSysRekey_ConfigReload tests that the rekey status endpoint can be toggled
// between requiring authentication and not requiring authentication by using
// the enable_unauthenticated_access config option and reloading the config.
func TestSysRekey_ConfigReload(t *testing.T) {
binary := os.Getenv("VAULT_BINARY")
if binary == "" {
t.Skip("only running docker test when $VAULT_BINARY present")
}
nodeConfig := &testcluster.VaultNodeConfig{
LogLevel: "TRACE",
}
opts := &docker.DockerClusterOptions{
ImageRepo: "hashicorp/vault",
ImageTag: "latest",
VaultBinary: binary,
DisableMlock: true,
ClusterOptions: testcluster.ClusterOptions{
NumCores: 1,
VaultNodeConfig: nodeConfig,
},
}
cluster := docker.NewTestDockerCluster(t, opts)
defer cluster.Cleanup()
node := cluster.Nodes()[0].(*docker.DockerClusterNode)
client := node.APIClient()
rootToken := cluster.GetRootToken()
// Test 1: Without enable_unauthenticated_access, rekey status should require auth
t.Run("requires-auth-by-default", func(t *testing.T) {
// Try without token - should fail
clientNoAuth, err := client.Clone()
require.NoError(t, err, "failed to clone client")
clientNoAuth.SetToken("")
_, err = clientNoAuth.Logical().Read("sys/rekey/init")
require.Error(t, err, "expected error when accessing rekey status without token")
require.Contains(t, err.Error(), "permission denied", "error should indicate permission denied")
// Try with token - should succeed
clientWithAuth, err := client.Clone()
require.NoError(t, err, "failed to clone client")
clientWithAuth.SetToken(rootToken)
resp, err := clientWithAuth.Logical().Read("sys/rekey/init")
require.NoError(t, err, "should succeed with valid token")
require.NotNil(t, resp, "response should not be nil")
require.NotNil(t, resp.Data, "response data should not be nil")
require.False(t, resp.Data["started"].(bool), "rekey should not be started")
})
// Test 2: Update config to enable unauthenticated rekey and reload
t.Run("enable-unauthenticated-via-config-reload", func(t *testing.T) {
// Create updated config with enable_unauthenticated_access
nodeConfig.EnableUnauthenticatedAccess = []string{"rekey"}
// Update the config and copy it to the container
err := node.UpdateConfig(t.Context(), nodeConfig)
require.NoError(t, err, "failed to update config")
// Send SIGHUP to reload the configuration
err = node.Signal(t.Context(), "SIGHUP")
require.NoError(t, err, "failed to send SIGHUP")
// Wait for rekey to appear in enable_unauthenticated_access
waitForRekeyInConfig(t, client, rootToken, true)
// Now test that rekey status works without auth
clientNoAuth, err := client.Clone()
require.NoError(t, err, "failed to clone client")
clientNoAuth.SetToken("")
resp, err := clientNoAuth.Logical().Read("sys/rekey/init")
require.NoError(t, err, "should succeed without token after config reload")
require.NotNil(t, resp, "response should not be nil")
require.NotNil(t, resp.Data, "response data should not be nil")
require.False(t, resp.Data["started"].(bool), "rekey should not be started")
// Verify it still works with auth
clientWithAuth2, err := client.Clone()
require.NoError(t, err, "failed to clone client")
clientWithAuth2.SetToken(rootToken)
resp2, err := clientWithAuth2.Logical().Read("sys/rekey/init")
require.NoError(t, err, "should still succeed with valid token")
require.NotNil(t, resp2, "response should not be nil")
require.NotNil(t, resp2.Data, "response data should not be nil")
require.False(t, resp2.Data["started"].(bool), "rekey should not be started")
})
// Test 3: Remove the override and reload to restore auth requirement
t.Run("restore-auth-requirement-via-config-reload", func(t *testing.T) {
// Create config without enable_unauthenticated_access
nodeConfig.EnableUnauthenticatedAccess = nil
// Update the config and copy it to the container
err := node.UpdateConfig(t.Context(), nodeConfig)
require.NoError(t, err, "failed to update config")
// Send SIGHUP to reload the configuration
err = node.Signal(t.Context(), "SIGHUP")
require.NoError(t, err, "failed to send SIGHUP")
// Wait for rekey to be removed from enable_unauthenticated_access
waitForRekeyInConfig(t, client, rootToken, false)
// Now test that rekey status requires auth again
clientNoAuth, err := client.Clone()
require.NoError(t, err, "failed to clone client")
clientNoAuth.SetToken("")
_, err = clientNoAuth.Logical().Read("sys/rekey/init")
require.Error(t, err, "should fail without token after restoring auth requirement")
require.Contains(t, err.Error(), "permission denied", "error should indicate permission denied")
// Verify it still works with auth
clientWithAuth2, err := client.Clone()
require.NoError(t, err, "failed to clone client")
clientWithAuth2.SetToken(rootToken)
resp, err := clientWithAuth2.Logical().Read("sys/rekey/init")
require.NoError(t, err, "should succeed with valid token")
require.NotNil(t, resp, "response should not be nil")
require.NotNil(t, resp.Data, "response data should not be nil")
require.False(t, resp.Data["started"].(bool), "rekey should not be started")
})
}

View file

@ -4,6 +4,7 @@
package vault
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/sha256"
@ -14,6 +15,7 @@ import (
"errors"
"fmt"
"hash"
"io"
"math/rand"
"net/http"
"net/url"
@ -43,6 +45,7 @@ import (
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/monitor"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/random"
"github.com/hashicorp/vault/helper/versions"
"github.com/hashicorp/vault/sdk/framework"
@ -106,6 +109,57 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
raftChallengeLimiter: rate.NewLimiter(rate.Limit(RaftChallengesPerSecond), RaftInitialChallengeLimit),
}
// Build the unauthenticated paths list. Rekey paths are conditionally added based on
// the enableUnauthRekey configuration (retrieved from Core).
unauthenticatedPaths := []string{
"wrapping/lookup",
"wrapping/pubkey",
"replication/status",
"internal/specs/openapi",
"internal/ui/authenticated-messages",
"internal/ui/unauthenticated-messages",
"internal/ui/mounts",
"internal/ui/mounts/*",
"internal/ui/namespaces",
"replication/performance/status",
"replication/dr/status",
"replication/dr/secondary/promote",
"replication/dr/secondary/disable",
"replication/dr/secondary/recover",
"replication/dr/secondary/update-primary",
"replication/dr/secondary/operation-token/delete",
"replication/dr/secondary/license",
"replication/dr/secondary/license/signed",
"replication/dr/secondary/license/status",
"replication/dr/secondary/sys/config/reload/license",
"replication/dr/secondary/reindex",
"storage/raft/bootstrap/challenge",
"storage/raft/bootstrap/answer",
"init",
"seal-status",
"unseal",
"leader",
"health",
"generate-root/attempt",
"generate-root/update",
"decode-token",
"mfa/validate",
}
// Note that while rekeyPaths are not part of unauthenticatedPaths, that's
// because they are defined both here and in http.handler. The latter ones
// are unauthenticated and don't use the logical framework. They are enabled
// only when Core.enableUnauthRekey is true, and being more specific paths
// than the v1/sys mux path they take precedence when enabled.
rekeyPaths := []string{
"rekey/init",
"rekey/update",
"rekey/verify",
"rekey-recovery-key/init",
"rekey-recovery-key/update",
"rekey-recovery-key/verify",
}
b.Backend = &framework.Backend{
RunningVersion: versions.DefaultBuiltinVersion,
Help: strings.TrimSpace(sysHelpRoot),
@ -147,46 +201,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
"step-down",
},
Unauthenticated: []string{
"wrapping/lookup",
"wrapping/pubkey",
"replication/status",
"internal/specs/openapi",
"internal/ui/authenticated-messages",
"internal/ui/unauthenticated-messages",
"internal/ui/mounts",
"internal/ui/mounts/*",
"internal/ui/namespaces",
"replication/performance/status",
"replication/dr/status",
"replication/dr/secondary/promote",
"replication/dr/secondary/disable",
"replication/dr/secondary/recover",
"replication/dr/secondary/update-primary",
"replication/dr/secondary/operation-token/delete",
"replication/dr/secondary/license",
"replication/dr/secondary/license/signed",
"replication/dr/secondary/license/status",
"replication/dr/secondary/sys/config/reload/license",
"replication/dr/secondary/reindex",
"storage/raft/bootstrap/challenge",
"storage/raft/bootstrap/answer",
"init",
"seal-status",
"unseal",
"leader",
"health",
"generate-root/attempt",
"generate-root/update",
"decode-token",
"rekey/init",
"rekey/update",
"rekey/verify",
"rekey-recovery-key/init",
"rekey-recovery-key/update",
"rekey-recovery-key/verify",
"mfa/validate",
},
Unauthenticated: unauthenticatedPaths,
LocalStorage: []string{
expirationSubPath,
@ -199,6 +214,8 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
SealWrapStorage: []string{
managedKeyRegistrySubPath,
},
Binary: rekeyPaths,
},
}
b.Backend.PathsSpecial.Unauthenticated = append(b.Backend.PathsSpecial.Unauthenticated, entUnauthenticatedPaths()...)
@ -1373,6 +1390,463 @@ func (b *SystemBackend) handleRekeyDeleteRecovery(ctx context.Context, req *logi
return b.handleRekeyDelete(ctx, req, data, true)
}
type RekeyStatusResponse struct {
Nonce string `json:"nonce"`
Started bool `json:"started"`
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
Required int `json:"required"`
PGPFingerprints []string `json:"pgp_fingerprints"`
Backup bool `json:"backup"`
VerificationRequired bool `json:"verification_required"`
VerificationNonce string `json:"verification_nonce,omitempty"`
}
func HandleSysRekeyInitGet(ctx context.Context, core *Core, recovery bool, grabLock bool) (*RekeyStatusResponse, int, error) {
barrierConfig, barrierConfErr := core.SealAccess().BarrierConfig(ctx)
if barrierConfErr != nil {
return nil, http.StatusInternalServerError, barrierConfErr
}
if barrierConfig == nil {
return nil, http.StatusBadRequest, fmt.Errorf("server is not yet initialized")
}
// Get the rekey configuration
rekeyConf, err := core.RekeyConfig(recovery, grabLock)
if err != nil {
return nil, err.Code(), err
}
sealThreshold, err := core.RekeyThreshold(ctx, recovery, grabLock)
if err != nil {
return nil, err.Code(), err
}
// Format the status
status := &RekeyStatusResponse{
Started: false,
T: 0,
N: 0,
Required: sealThreshold,
}
if rekeyConf != nil {
// Get the progress
started, progress, err := core.RekeyProgress(recovery, false, grabLock)
if err != nil {
return nil, err.Code(), err
}
status.Nonce = rekeyConf.Nonce
status.Started = started
status.T = rekeyConf.SecretThreshold
status.N = rekeyConf.SecretShares
status.Progress = progress
status.VerificationRequired = rekeyConf.VerificationRequired
status.VerificationNonce = rekeyConf.VerificationNonce
if rekeyConf.PGPKeys != nil && len(rekeyConf.PGPKeys) != 0 {
pgpFingerprints, err := pgpkeys.GetFingerprints(rekeyConf.PGPKeys, nil)
if err != nil {
return nil, http.StatusInternalServerError, err
}
status.PGPFingerprints = pgpFingerprints
status.Backup = rekeyConf.Backup
}
}
return status, 0, nil
}
type RekeyRequest struct {
SecretShares int `json:"secret_shares"`
SecretThreshold int `json:"secret_threshold"`
StoredShares int `json:"stored_shares"`
PGPKeys []string `json:"pgp_keys"`
Backup bool `json:"backup"`
RequireVerification bool `json:"require_verification"`
}
func HandleSysRekeyInitPut(core *Core, recovery bool, req *RekeyRequest, grabLock bool) (int, error) {
if req.Backup && len(req.PGPKeys) == 0 {
return http.StatusBadRequest, fmt.Errorf("cannot request a backup of the new keys without providing PGP keys for encryption")
}
if len(req.PGPKeys) > 0 && len(req.PGPKeys) != req.SecretShares {
return http.StatusBadRequest, fmt.Errorf("incorrect number of PGP keys for rekey")
}
// Initialize the rekey
err := core.RekeyInit(&SealConfig{
SecretShares: req.SecretShares,
SecretThreshold: req.SecretThreshold,
StoredShares: req.StoredShares,
PGPKeys: req.PGPKeys,
Backup: req.Backup,
VerificationRequired: req.RequireVerification,
Created: time.Now().UTC(),
}, recovery, grabLock)
if err != nil {
return err.Code(), err
}
return http.StatusOK, nil
}
type RekeyUpdateRequest struct {
Nonce string
Key string
}
type RekeyUpdateResponse struct {
Nonce string `json:"nonce"`
Complete bool `json:"complete"`
Keys []string `json:"keys"`
KeysB64 []string `json:"keys_base64"`
PGPFingerprints []string `json:"pgp_fingerprints"`
Backup bool `json:"backup"`
VerificationRequired bool `json:"verification_required"`
VerificationNonce string `json:"verification_nonce,omitempty"`
}
func HandleSysRekeyUpdatePut(ctx context.Context, core *Core, recovery bool, req *RekeyUpdateRequest, grabLock bool) (*RekeyUpdateResponse, int, error) {
if req.Key == "" {
return nil, http.StatusBadRequest, errors.New("'key' must be specified in request body as JSON")
}
// Decode the key, which is base64 or hex encoded
min, max := core.BarrierKeyLength()
key, err := hex.DecodeString(req.Key)
// We check min and max here to ensure that a string that is base64
// encoded but also valid hex will not be valid and we instead base64
// decode it
if err != nil || len(key) < min || len(key) > max {
key, err = base64.StdEncoding.DecodeString(req.Key)
if err != nil {
return nil, http.StatusBadRequest, errors.New("'key' must be a valid hex or base64 string")
}
}
// Use the key to make progress on rekey
result, rekeyErr := core.RekeyUpdate(ctx, key, req.Nonce, recovery, grabLock)
if rekeyErr != nil {
return nil, rekeyErr.Code(), rekeyErr
}
// Format the response
resp := &RekeyUpdateResponse{}
if result != nil {
resp.Complete = true
resp.Nonce = req.Nonce
resp.Backup = result.Backup
resp.PGPFingerprints = result.PGPFingerprints
resp.VerificationRequired = result.VerificationRequired
resp.VerificationNonce = result.VerificationNonce
// Encode the keys
keys := make([]string, 0, len(result.SecretShares))
keysB64 := make([]string, 0, len(result.SecretShares))
for _, k := range result.SecretShares {
keys = append(keys, hex.EncodeToString(k))
keysB64 = append(keysB64, base64.StdEncoding.EncodeToString(k))
}
resp.Keys = keys
resp.KeysB64 = keysB64
return resp, 0, nil
}
return nil, 0, nil
}
type RekeyVerifyStatusResponse struct {
Nonce string `json:"nonce"`
Started bool `json:"started"`
T int `json:"t"`
N int `json:"n"`
Progress int `json:"progress"`
}
func HandleSysRekeyVerifyGet(ctx context.Context, core *Core, recovery bool, grabLock bool) (*RekeyVerifyStatusResponse, int, error) {
barrierConfig, err := core.SealAccess().BarrierConfig(ctx)
if err != nil {
return nil, http.StatusInternalServerError, err
}
if barrierConfig == nil {
return nil, http.StatusBadRequest, fmt.Errorf("server is not yet initialized")
}
// Get the rekey configuration
rekeyConf, rekeyErr := core.RekeyConfig(recovery, grabLock)
if rekeyErr != nil {
return nil, rekeyErr.Code(), rekeyErr
}
if rekeyConf == nil {
return nil, http.StatusBadRequest, fmt.Errorf("no rekey configuration found")
}
// Get the progress
started, progress, rekeyErr := core.RekeyProgress(recovery, true, grabLock)
if rekeyErr != nil {
return nil, rekeyErr.Code(), rekeyErr
}
// Format the status
status := &RekeyVerifyStatusResponse{
Started: started,
Nonce: rekeyConf.VerificationNonce,
T: rekeyConf.SecretThreshold,
N: rekeyConf.SecretShares,
Progress: progress,
}
return status, 0, nil
}
type RekeyVerificationUpdateRequest struct {
Nonce string `json:"nonce"`
Key string `json:"key"`
}
type RekeyVerificationUpdateResponse struct {
Nonce string `json:"nonce"`
Complete bool `json:"complete"`
}
func HandleSysRekeyVerifyPut(ctx context.Context, core *Core, recovery bool, grabLock bool, req *RekeyVerificationUpdateRequest) (*RekeyVerificationUpdateResponse, int, error) {
if req.Key == "" {
return nil, http.StatusBadRequest, errors.New("'key' must be specified in request body as JSON")
}
// Decode the key, which is base64 or hex encoded
min, max := core.BarrierKeyLength()
key, err := hex.DecodeString(req.Key)
// We check min and max here to ensure that a string that is base64
// encoded but also valid hex will not be valid and we instead base64
// decode it
if err != nil || len(key) < min || len(key) > max {
key, err = base64.StdEncoding.DecodeString(req.Key)
if err != nil {
return nil, http.StatusBadRequest, errors.New("'key' must be a valid hex or base64 string")
}
}
// Use the key to make progress on rekey
result, rekeyErr := core.RekeyVerify(ctx, key, req.Nonce, recovery, grabLock)
if rekeyErr != nil {
return nil, rekeyErr.Code(), rekeyErr
}
if result != nil {
return &RekeyVerificationUpdateResponse{
Nonce: result.Nonce,
Complete: result.Complete,
}, http.StatusOK, nil
}
return nil, 0, nil
}
// handleRekeyInit handles the rekey/init endpoint for both barrier and recovery keys
func (b *SystemBackend) handleRekeyInit(
ctx context.Context,
req *logical.Request,
recovery bool,
) (*logical.Response, error) {
// Check replication state
repState := b.Core.ReplicationState()
if repState.HasState(consts.ReplicationPerformanceSecondary) {
return logical.ErrorResponse("rekeying can only be performed on the primary cluster when replication is activated"), nil
}
// Check if recovery key is supported
if recovery && !b.Core.SealAccess().RecoveryKeySupported() {
return logical.ErrorResponse("recovery rekeying not supported"), nil
}
switch req.Operation {
case logical.ReadOperation:
return b.handleRekeyInitGet(ctx, recovery)
case logical.UpdateOperation:
return b.handleRekeyInitPut(ctx, recovery)
case logical.DeleteOperation:
return b.handleRekeyInitDelete(ctx, recovery)
default:
return nil, logical.ErrUnsupportedOperation
}
}
// getJSONBody populates the out struct with the contents of the HTTP request body
// and returns (nil, nil), or on error returns values that a handler can return
// for failure. This is intended for older APIs that don't use the framework.
func getJSONBody(ctx context.Context, out any) (*logical.Response, error) {
body, ok := logical.ContextOriginalBodyValue(ctx)
if !ok {
return nonLogicalError(http.StatusInternalServerError, fmt.Errorf("failed to retrieve request body"))
}
err := jsonutil.DecodeJSONFromReader(body, out)
if err != nil && err != io.EOF {
return nonLogicalError(http.StatusBadRequest, fmt.Errorf("failed to parse JSON input: %w", err))
}
return nil, nil
}
// nonLogicalError creates an error response for older handlers that don't follow
// current vault response conventions.
func nonLogicalError(code int, err error) (*logical.Response, error) {
logical.AdjustErrorStatusCode(&code, err)
defer logical.IncrementResponseStatusCodeMetric(code)
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(logical.GenerateNonLogicalErrorResponse(code, err))
resp, _ := logical.RespondWithStatusCode(nil, nil, code)
resp.Data[logical.HTTPRawBodyError] = buf.String()
return resp, err
}
// nonLogicalResponse takes the result of a request handler, and either returns
// an error response if err is non nil, or serializes the val into the response.
// It uses the HTTP raw body field in response data, since this is for older
// APIs that don't follow our usual response format.
func nonLogicalResponse(val any, code int, err error) (*logical.Response, error) {
if err != nil {
return nonLogicalError(code, err)
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(val)
resp, _ := logical.RespondWithStatusCode(nil, nil, http.StatusOK)
resp.Data[logical.HTTPRawBody] = buf.String()
return resp, nil
}
func (b *SystemBackend) handleRekeyInitBarrier(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
return b.handleRekeyInit(ctx, req, false)
}
func (b *SystemBackend) handleRekeyInitRecovery(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
return b.handleRekeyInit(ctx, req, true)
}
func (b *SystemBackend) handleRekeyInitGet(ctx context.Context, recovery bool) (*logical.Response, error) {
status, code, err := HandleSysRekeyInitGet(ctx, b.Core, recovery, false)
return nonLogicalResponse(status, code, err)
}
func (b *SystemBackend) handleRekeyInitPut(ctx context.Context, recovery bool) (*logical.Response, error) {
var req RekeyRequest
resp, err := getJSONBody(ctx, &req)
if err != nil {
return resp, err
}
code, err := HandleSysRekeyInitPut(b.Core, recovery, &req, false)
if err != nil {
return nonLogicalError(code, err)
}
return b.handleRekeyInitGet(ctx, recovery)
}
type RekeyDeleteRequest struct {
Nonce string `json:"nonce"`
Key string `json:"key"`
}
func (b *SystemBackend) handleRekeyInitDelete(ctx context.Context, recovery bool) (*logical.Response, error) {
var req RekeyDeleteRequest
resp, err := getJSONBody(ctx, &req)
if err != nil {
return resp, err
}
if err := b.Core.RekeyCancel(recovery, req.Nonce, 10*time.Minute, false); err != nil {
return nil, fmt.Errorf("failed to cancel rekey: %w", err)
}
return nil, nil
}
// handleRekeyUpdate handles the rekey/update endpoint for both barrier and recovery keys
func (b *SystemBackend) handleRekeyUpdate(ctx context.Context, recovery bool) (*logical.Response, error) {
var req RekeyUpdateRequest
resp, err := getJSONBody(ctx, &req)
if err != nil {
return resp, err
}
// Use the key to make progress on rekey
result, code, err := HandleSysRekeyUpdatePut(ctx, b.Core, recovery, &req, false)
if err == nil && result == nil {
return b.handleRekeyInitGet(ctx, recovery)
}
return nonLogicalResponse(result, code, err)
}
func (b *SystemBackend) handleRekeyUpdateBarrier(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return b.handleRekeyUpdate(ctx, false)
}
func (b *SystemBackend) handleRekeyUpdateRecovery(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return b.handleRekeyUpdate(ctx, true)
}
// handleRekeyVerify handles the rekey/verify endpoint for both barrier and recovery keys
func (b *SystemBackend) handleRekeyVerify(ctx context.Context, req *logical.Request, _ *framework.FieldData, recovery bool) (*logical.Response, error) {
repState := b.Core.ReplicationState()
if repState.HasState(consts.ReplicationPerformanceSecondary) {
return logical.ErrorResponse("rekeying can only be performed on the primary cluster when replication is activated"), nil
}
// Check if recovery key is supported
if recovery && !b.Core.SealAccess().RecoveryKeySupported() {
return logical.ErrorResponse("recovery rekeying not supported"), nil
}
switch req.Operation {
case logical.ReadOperation:
return b.handleRekeyVerifyGet(ctx, recovery)
case logical.UpdateOperation:
return b.handleRekeyVerifyPut(ctx, recovery)
case logical.DeleteOperation:
return b.handleRekeyVerifyDelete(ctx, recovery)
default:
return nil, logical.ErrUnsupportedOperation
}
}
func (b *SystemBackend) handleRekeyVerifyBarrier(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return b.handleRekeyVerify(ctx, req, data, false)
}
func (b *SystemBackend) handleRekeyVerifyRecovery(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return b.handleRekeyVerify(ctx, req, data, true)
}
func (b *SystemBackend) handleRekeyVerifyGet(ctx context.Context, recovery bool) (*logical.Response, error) {
status, code, err := HandleSysRekeyVerifyGet(ctx, b.Core, recovery, false)
return nonLogicalResponse(status, code, err)
}
func (b *SystemBackend) handleRekeyVerifyDelete(ctx context.Context, recovery bool) (*logical.Response, error) {
if err := b.Core.RekeyVerifyRestart(recovery, false); err != nil {
return nil, fmt.Errorf("failed to restart rekey verification: %w", err)
}
return b.handleRekeyVerifyGet(ctx, recovery)
}
func (b *SystemBackend) handleRekeyVerifyPut(ctx context.Context, recovery bool) (*logical.Response, error) {
var req RekeyVerificationUpdateRequest
resp, err := getJSONBody(ctx, &req)
if err != nil {
return resp, err
}
result, code, err := HandleSysRekeyVerifyPut(ctx, b.Core, recovery, false, &RekeyVerificationUpdateRequest{
Nonce: req.Nonce,
Key: req.Key,
})
if err == nil && result == nil {
return b.handleRekeyVerifyGet(ctx, recovery)
}
return nonLogicalResponse(result, code, err)
}
func (b *SystemBackend) handleGenerateRootDecodeTokenUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
encodedToken := data.Get("encoded_token").(string)
otp := data.Get("otp").(string)

View file

@ -769,7 +769,7 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
respFields := map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Required: true,
Required: false,
},
"started": {
Type: framework.TypeBool,
@ -785,7 +785,7 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
},
"progress": {
Type: framework.TypeInt,
Required: true,
Required: false,
},
"required": {
Type: framework.TypeInt,
@ -793,11 +793,11 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
},
"verification_required": {
Type: framework.TypeBool,
Required: true,
Required: false,
},
"verification_nonce": {
Type: framework.TypeString,
Required: true,
Required: false,
},
"backup": {
Type: framework.TypeBool,
@ -815,6 +815,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
OperationPrefix: "rekey-attempt",
},
// Note that since we're a binary path we don't actually use these fields, they exist solely for the sake
// of populating the openapi schema.
Fields: map[string]*framework.FieldSchema{
"secret_shares": {
Type: framework.TypeInt,
@ -824,6 +826,10 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Type: framework.TypeInt,
Description: "Specifies the number of shares required to reconstruct the unseal key. This must be less than or equal secret_shares. If using Vault HSM with auto-unsealing, this value must be the same as secret_shares.",
},
"stored_shares": {
Type: framework.TypeInt,
Description: "Specifies the number of shares that should be encrypted by the HSM and stored for auto-unsealing. Currently must be the same as secret_shares.",
},
"pgp_keys": {
Type: framework.TypeCommaStringSlice,
Description: "Specifies an array of PGP public keys used to encrypt the output unseal keys. Ordering is preserved. The keys must be base64-encoded from their original binary representation. The size of this array must be the same as secret_shares.",
@ -836,10 +842,16 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Type: framework.TypeBool,
Description: "Turns on verification functionality",
},
"nonce": {
Type: framework.TypeString,
Description: "Specifies the nonce of the rekey operation. If the rekey was initialized within the last 10 minutes, you must provide the nonce to cancel the operation.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyInitBarrier,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "progress",
@ -853,6 +865,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Summary: "Reads the configuration and progress of the current rekey attempt.",
},
logical.UpdateOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyInitBarrier,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "initialize",
},
@ -866,6 +880,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Description: "Only a single rekey attempt can take place at a time, and changing the parameters of a rekey requires canceling and starting a new rekey, which will also provide a new nonce.",
},
logical.DeleteOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyInitBarrier,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "cancel",
},
@ -991,6 +1007,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
{
Pattern: "rekey/update",
// Note that since we're a binary path we don't actually use these fields, they exist solely for the sake
// of populating the openapi schema.
Fields: map[string]*framework.FieldSchema{
"key": {
Type: framework.TypeString,
@ -1004,6 +1022,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyUpdateBarrier,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "rekey-attempt",
OperationVerb: "update",
@ -1068,6 +1088,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
OperationPrefix: "rekey-verification",
},
// Note that since we're a binary path we don't actually use these fields, they exist solely for the sake
// of populating the openapi schema.
Fields: map[string]*framework.FieldSchema{
"key": {
Type: framework.TypeString,
@ -1081,6 +1103,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyVerifyBarrier,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "progress",
@ -1115,6 +1139,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Summary: "Read the configuration and progress of the current rekey verification attempt.",
},
logical.DeleteOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyVerifyBarrier,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "cancel",
},
@ -1149,6 +1175,8 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
Description: "This clears any progress made and resets the nonce. Unlike a `DELETE` against `sys/rekey/init`, this only resets the current verification operation, not the entire rekey atttempt.",
},
logical.UpdateOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyVerifyBarrier,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "update",
},
@ -1170,6 +1198,246 @@ func (b *SystemBackend) rekeyPaths() []*framework.Path {
},
},
},
{
Pattern: "rekey-recovery-key/init",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "rekey-recovery-key-attempt",
},
// Note that since we're a binary path we don't actually use these fields, they exist solely for the sake
// of populating the openapi schema.
Fields: map[string]*framework.FieldSchema{
"secret_shares": {
Type: framework.TypeInt,
Description: "Specifies the number of shares to split the recovery key into.",
},
"stored_shares": {
Type: framework.TypeInt,
Description: "Specifies the number of shares that should be encrypted by the HSM and stored for auto-unsealing. Currently must be the same as `secret_shares`.",
},
"secret_threshold": {
Type: framework.TypeInt,
Description: "Specifies the number of shares required to reconstruct the recovery key.",
},
"pgp_keys": {
Type: framework.TypeCommaStringSlice,
Description: "Specifies an array of PGP public keys used to encrypt the output recovery keys.",
},
"backup": {
Type: framework.TypeBool,
Description: "Specifies if using PGP-encrypted keys, whether Vault should also store a plaintext backup of the PGP-encrypted keys.",
},
"require_verification": {
Type: framework.TypeBool,
Description: "Turns on verification functionality",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyInitRecovery,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "progress",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: respFields,
}},
},
Summary: "Reads the configuration and progress of the current recovery key rekey attempt.",
},
logical.UpdateOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyInitRecovery,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "initialize",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: respFields,
}},
},
Summary: "Initializes a new recovery key rekey attempt.",
Description: "Only a single recovery key rekey attempt can take place at a time.",
},
logical.DeleteOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyInitRecovery,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "cancel",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
}},
},
Summary: "Cancels any in-progress recovery key rekey.",
Description: "This clears the recovery key rekey settings as well as any progress made.",
},
},
},
{
Pattern: "rekey-recovery-key/update",
// Note that since we're a binary path we don't actually use these fields, they exist solely for the sake
// of populating the openapi schema.
Fields: map[string]*framework.FieldSchema{
"key": {
Type: framework.TypeString,
Description: "Specifies a single recovery key share.",
},
"nonce": {
Type: framework.TypeString,
Description: "Specifies the nonce of the rekey attempt.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyUpdateRecovery,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "rekey-recovery-key-attempt",
OperationVerb: "update",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Required: true,
},
"complete": {
Type: framework.TypeBool,
},
"keys": {
Type: framework.TypeCommaStringSlice,
},
"keys_base64": {
Type: framework.TypeCommaStringSlice,
},
"verification_required": {
Type: framework.TypeBool,
Required: true,
},
"verification_nonce": {
Type: framework.TypeString,
Required: true,
},
"backup": {
Type: framework.TypeBool,
},
"pgp_fingerprints": {
Type: framework.TypeCommaStringSlice,
},
},
}},
},
Summary: "Enter a single recovery key share to progress the rekey of the Vault.",
},
},
},
{
Pattern: "rekey-recovery-key/verify",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "rekey-recovery-key-verification",
},
// Note that since we're a binary path we don't actually use these fields, they exist solely for the sake
// of populating the openapi schema.
Fields: map[string]*framework.FieldSchema{
"key": {
Type: framework.TypeString,
Description: "Specifies a single recovery key share from the new set of shares.",
},
"nonce": {
Type: framework.TypeString,
Description: "Specifies the nonce of the rekey verification operation.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyVerifyRecovery,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "progress",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Required: true,
},
"started": {
Type: framework.TypeBool,
Required: true,
},
"t": {
Type: framework.TypeInt,
Required: true,
},
"n": {
Type: framework.TypeInt,
Required: true,
},
"progress": {
Type: framework.TypeInt,
Required: true,
},
},
}},
},
Summary: "Read the configuration and progress of the current recovery key rekey verification attempt.",
},
logical.DeleteOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyVerifyRecovery,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "cancel",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
}},
},
Summary: "Cancel any in-progress recovery key rekey verification operation.",
Description: "This clears any progress made and resets the nonce.",
},
logical.UpdateOperation: &framework.PathOperation{
ForwardPerformanceStandby: true,
Callback: b.handleRekeyVerifyRecovery,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "update",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Required: true,
},
"complete": {
Type: framework.TypeBool,
},
},
}},
},
Summary: "Enter a single new recovery key share to progress the rekey verification operation.",
},
},
},
{
Pattern: "seal$",

View file

@ -271,17 +271,17 @@ func (c *Core) reloadBackendCommon(ctx context.Context, entry *MountEntry, isAut
paths := backend.SpecialPaths()
if paths != nil {
re.rootPaths.Store(pathsToRadix(paths.Root))
loginPathsEntry, err := parseUnauthenticatedPaths(paths.Unauthenticated)
loginPathsEntry, err := parseSpecialPaths(paths.Unauthenticated)
if err != nil {
return err
}
re.loginPaths.Store(loginPathsEntry)
binaryPathsEntry, err := parseUnauthenticatedPaths(paths.Binary)
binaryPathsEntry, err := parseSpecialPaths(paths.Binary)
if err != nil {
return err
}
re.binaryPaths.Store(binaryPathsEntry)
allowSnapshotReadPathsEntry, err := parseUnauthenticatedPaths(paths.AllowSnapshotRead)
allowSnapshotReadPathsEntry, err := parseSpecialPaths(paths.AllowSnapshotRead)
if err != nil {
return err
}

View file

@ -63,9 +63,11 @@ type RekeyBackup struct {
// the recovery key threshold, depending on whether rekey is being
// performed on the recovery key, or whether the seal supports
// recovery keys.
func (c *Core) RekeyThreshold(ctx context.Context, recovery bool) (int, logical.HTTPCodedError) {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
func (c *Core) RekeyThreshold(ctx context.Context, recovery bool, grabLock bool) (int, logical.HTTPCodedError) {
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return 0, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -97,9 +99,11 @@ func (c *Core) RekeyThreshold(ctx context.Context, recovery bool) (int, logical.
}
// RekeyProgress is used to return the rekey progress (num shares).
func (c *Core) RekeyProgress(recovery, verification bool) (bool, int, logical.HTTPCodedError) {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
func (c *Core) RekeyProgress(recovery, verification, grabLock bool) (bool, int, logical.HTTPCodedError) {
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return false, 0, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -128,9 +132,11 @@ func (c *Core) RekeyProgress(recovery, verification bool) (bool, int, logical.HT
}
// RekeyConfig is used to read the rekey configuration
func (c *Core) RekeyConfig(recovery bool) (*SealConfig, logical.HTTPCodedError) {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
func (c *Core) RekeyConfig(recovery bool, grabLock bool) (*SealConfig, logical.HTTPCodedError) {
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -158,19 +164,19 @@ func (c *Core) RekeyConfig(recovery bool) (*SealConfig, logical.HTTPCodedError)
// RekeyInit will either initialize the rekey of barrier or recovery key.
// recovery determines whether this is a rekey on the barrier or recovery key.
func (c *Core) RekeyInit(config *SealConfig, recovery bool) logical.HTTPCodedError {
func (c *Core) RekeyInit(config *SealConfig, recovery bool, grabLock bool) logical.HTTPCodedError {
if config.SecretThreshold > config.SecretShares {
return logical.CodedError(http.StatusBadRequest, "provided threshold greater than the total shares")
}
if recovery {
return c.RecoveryRekeyInit(config)
return c.RecoveryRekeyInit(config, grabLock)
}
return c.BarrierRekeyInit(config)
return c.BarrierRekeyInit(config, grabLock)
}
// BarrierRekeyInit is used to initialize the rekey settings for the barrier key
func (c *Core) BarrierRekeyInit(config *SealConfig) logical.HTTPCodedError {
func (c *Core) BarrierRekeyInit(config *SealConfig, grabLock bool) logical.HTTPCodedError {
switch c.seal.BarrierSealConfigType() {
case SealConfigTypeShamir:
// As of Vault 1.3 all seals use StoredShares==1. The one exception is
@ -210,8 +216,10 @@ func (c *Core) BarrierRekeyInit(config *SealConfig) logical.HTTPCodedError {
return logical.CodedError(http.StatusInternalServerError, fmt.Errorf("invalid rekey seal configuration: %w", err).Error())
}
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -239,14 +247,14 @@ func (c *Core) BarrierRekeyInit(config *SealConfig) logical.HTTPCodedError {
c.barrierRekeyConfig.Nonce = nonce
c.barrierRekeyConfig.Created = time.Now().UTC()
if c.logger.IsInfo() {
c.logger.Info("rekey initialized", "nonce", c.barrierRekeyConfig.Nonce, "shares", c.barrierRekeyConfig.SecretShares, "threshold", c.barrierRekeyConfig.SecretThreshold, "validation_required", c.barrierRekeyConfig.VerificationRequired)
}
c.logger.Info("rekey initialized", "nonce", c.barrierRekeyConfig.Nonce,
"shares", c.barrierRekeyConfig.SecretShares, "threshold", c.barrierRekeyConfig.SecretThreshold,
"validation_required", c.barrierRekeyConfig.VerificationRequired, "backup", c.barrierRekeyConfig.Backup)
return nil
}
// RecoveryRekeyInit is used to initialize the rekey settings for the recovery key
func (c *Core) RecoveryRekeyInit(config *SealConfig) logical.HTTPCodedError {
func (c *Core) RecoveryRekeyInit(config *SealConfig, grabLock bool) logical.HTTPCodedError {
if config.StoredShares > 0 {
return logical.CodedError(http.StatusBadRequest, "stored shares not supported by recovery key")
}
@ -261,8 +269,10 @@ func (c *Core) RecoveryRekeyInit(config *SealConfig) logical.HTTPCodedError {
return logical.CodedError(http.StatusBadRequest, "recovery keys not supported")
}
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -297,11 +307,11 @@ func (c *Core) RecoveryRekeyInit(config *SealConfig) logical.HTTPCodedError {
}
// RekeyUpdate is used to provide a new key part for the barrier or recovery key.
func (c *Core) RekeyUpdate(ctx context.Context, key []byte, nonce string, recovery bool) (*RekeyResult, logical.HTTPCodedError) {
func (c *Core) RekeyUpdate(ctx context.Context, key []byte, nonce string, recovery bool, grabLock bool) (*RekeyResult, logical.HTTPCodedError) {
if recovery {
return c.RecoveryRekeyUpdate(ctx, key, nonce)
return c.RecoveryRekeyUpdate(ctx, key, nonce, grabLock)
}
return c.BarrierRekeyUpdate(ctx, key, nonce)
return c.BarrierRekeyUpdate(ctx, key, nonce, grabLock)
}
// BarrierRekeyUpdate is used to provide a new key part. Barrier rekey can be done
@ -309,10 +319,12 @@ func (c *Core) RekeyUpdate(ctx context.Context, key []byte, nonce string, recove
// key.
//
// N.B.: If recovery keys are used to rekey, the new barrier key shares are not returned.
func (c *Core) BarrierRekeyUpdate(ctx context.Context, key []byte, nonce string) (*RekeyResult, logical.HTTPCodedError) {
func (c *Core) BarrierRekeyUpdate(ctx context.Context, key []byte, nonce string, grabLock bool) (*RekeyResult, logical.HTTPCodedError) {
// Ensure we are already unsealed
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -597,10 +609,12 @@ func (c *Core) performBarrierRekey(ctx context.Context, newSealKey []byte) logic
}
// RecoveryRekeyUpdate is used to provide a new key part
func (c *Core) RecoveryRekeyUpdate(ctx context.Context, key []byte, nonce string) (*RekeyResult, logical.HTTPCodedError) {
func (c *Core) RecoveryRekeyUpdate(ctx context.Context, key []byte, nonce string, grabLock bool) (*RekeyResult, logical.HTTPCodedError) {
// Ensure we are already unsealed
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -798,10 +812,12 @@ func (c *Core) performRecoveryRekey(ctx context.Context, newRootKey []byte) logi
return nil
}
func (c *Core) RekeyVerify(ctx context.Context, key []byte, nonce string, recovery bool) (ret *RekeyVerifyResult, retErr logical.HTTPCodedError) {
func (c *Core) RekeyVerify(ctx context.Context, key []byte, nonce string, recovery bool, grabLock bool) (ret *RekeyVerifyResult, retErr logical.HTTPCodedError) {
// Ensure we are already unsealed
c.stateLock.RLock()
defer c.stateLock.RUnlock()
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return nil, logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -913,9 +929,11 @@ func (c *Core) RekeyVerify(ctx context.Context, key []byte, nonce string, recove
}
// RekeyCancel is used to cancel an in-progress rekey
func (c *Core) RekeyCancel(recovery bool, nonce string, requiresNonceDeadline time.Duration) logical.HTTPCodedError {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
func (c *Core) RekeyCancel(recovery bool, nonce string, requiresNonceDeadline time.Duration, grabLock bool) logical.HTTPCodedError {
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}
@ -952,9 +970,11 @@ func (c *Core) RekeyCancel(recovery bool, nonce string, requiresNonceDeadline ti
}
// RekeyVerifyRestart is used to start the verification process over
func (c *Core) RekeyVerifyRestart(recovery bool) logical.HTTPCodedError {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
func (c *Core) RekeyVerifyRestart(recovery bool, grabLock bool) logical.HTTPCodedError {
if grabLock {
c.stateLock.RLock()
defer c.stateLock.RUnlock()
}
if c.Sealed() {
return logical.CodedError(http.StatusServiceUnavailable, consts.ErrSealed.Error())
}

View file

@ -36,7 +36,7 @@ func TestCore_Rekey_Lifecycle(t *testing.T) {
func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) {
min, _ := c.barrier.KeyLength()
// Verify update not allowed
_, err := c.RekeyUpdate(context.Background(), make([]byte, min), "", recovery)
_, err := c.RekeyUpdate(context.Background(), make([]byte, min), "", recovery, true)
expected := "no barrier rekey in progress"
if recovery {
expected = "no recovery rekey in progress"
@ -46,12 +46,12 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) {
}
// Should be no progress
if _, _, err := c.RekeyProgress(recovery, false); err == nil {
if _, _, err := c.RekeyProgress(recovery, false, true); err == nil {
t.Fatal("expected error from RekeyProgress")
}
// Should be no config
conf, err := c.RekeyConfig(recovery)
conf, err := c.RekeyConfig(recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -60,7 +60,7 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) {
}
// Cancel should be idempotent
err = c.RekeyCancel(false, "", 10*time.Minute)
err = c.RekeyCancel(false, "", 10*time.Minute, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -70,13 +70,13 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) {
SecretThreshold: 3,
SecretShares: 5,
}
err = c.RekeyInit(newConf, recovery)
err = c.RekeyInit(newConf, recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Should get config
conf, err = c.RekeyConfig(recovery)
conf, err = c.RekeyConfig(recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -86,13 +86,13 @@ func testCore_Rekey_Lifecycle_Common(t *testing.T, c *Core, recovery bool) {
}
// Cancel should be clear
err = c.RekeyCancel(recovery, conf.Nonce, 10*time.Minute)
err = c.RekeyCancel(recovery, conf.Nonce, 10*time.Minute, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Should be no config
conf, err = c.RekeyConfig(recovery)
conf, err = c.RekeyConfig(recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -114,7 +114,7 @@ func testCore_Rekey_Init_Common(t *testing.T, c *Core, recovery bool) {
SecretThreshold: 5,
SecretShares: 1,
}
err := c.RekeyInit(badConf, recovery)
err := c.RekeyInit(badConf, recovery, true)
if err == nil {
t.Fatalf("should fail")
}
@ -131,13 +131,13 @@ func testCore_Rekey_Init_Common(t *testing.T, c *Core, recovery bool) {
newConf.Type = c.seal.RecoverySealConfigType().String()
}
err = c.RekeyInit(newConf, recovery)
err = c.RekeyInit(newConf, recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Second should fail
err = c.RekeyInit(newConf, recovery)
err = c.RekeyInit(newConf, recovery, true)
if err == nil {
t.Fatalf("should fail")
}
@ -171,13 +171,13 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro
SecretThreshold: 3,
SecretShares: 5,
}
hErr := c.RekeyInit(newConf, recovery)
hErr := c.RekeyInit(newConf, recovery, true)
if hErr != nil {
t.Fatalf("err: %v", hErr)
}
// Fetch new config with generated nonce
rkconf, hErr := c.RekeyConfig(recovery)
rkconf, hErr := c.RekeyConfig(recovery, true)
if hErr != nil {
t.Fatalf("err: %v", hErr)
}
@ -188,7 +188,7 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro
// Provide the master/recovery keys
var result *RekeyResult
for _, key := range keys {
result, err = c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery)
result, err = c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery, true)
if err != nil {
if !wantRekeyUpdateError {
t.Fatalf("err: %v", err)
@ -208,12 +208,12 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro
}
// Should be no progress
if _, _, err := c.RekeyProgress(recovery, false); err == nil {
if _, _, err := c.RekeyProgress(recovery, false, true); err == nil {
t.Fatal("expected error from RekeyProgress")
}
// Should be no config
conf, hErr := c.RekeyConfig(recovery)
conf, hErr := c.RekeyConfig(recovery, true)
if hErr != nil {
t.Fatalf("rekey config error: %v", hErr)
}
@ -271,13 +271,13 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro
SecretThreshold: 1,
SecretShares: 1,
}
err = c.RekeyInit(newConf, recovery)
err = c.RekeyInit(newConf, recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Fetch new config with generated nonce
rkconf, err = c.RekeyConfig(recovery)
rkconf, err = c.RekeyConfig(recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -288,14 +288,14 @@ func testCore_Rekey_Update_Common_Error(t *testing.T, c *Core, keys [][]byte, ro
// Provide the parts master
oldResult := result
for i := 0; i < 3; i++ {
result, err = c.RekeyUpdate(context.Background(), TestKeyCopy(oldResult.SecretShares[i]), rkconf.Nonce, recovery)
result, err = c.RekeyUpdate(context.Background(), TestKeyCopy(oldResult.SecretShares[i]), rkconf.Nonce, recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Should be progress
if i < 2 {
_, num, err := c.RekeyProgress(recovery, false)
_, num, err := c.RekeyProgress(recovery, false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -367,13 +367,13 @@ func testCore_Rekey_Invalid_Common(t *testing.T, c *Core, keys [][]byte, recover
SecretThreshold: 3,
SecretShares: 5,
}
err := c.RekeyInit(newConf, recovery)
err := c.RekeyInit(newConf, recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Fetch new config with generated nonce
rkconf, err := c.RekeyConfig(recovery)
rkconf, err := c.RekeyConfig(recovery, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -382,7 +382,7 @@ func testCore_Rekey_Invalid_Common(t *testing.T, c *Core, keys [][]byte, recover
}
// Provide the nonce (invalid)
_, err = c.RekeyUpdate(context.Background(), keys[0], "abcd", recovery)
_, err = c.RekeyUpdate(context.Background(), keys[0], "abcd", recovery, true)
if err == nil {
t.Fatalf("expected error")
}
@ -392,13 +392,13 @@ func testCore_Rekey_Invalid_Common(t *testing.T, c *Core, keys [][]byte, recover
oldkeystr := fmt.Sprintf("%#v", key)
key[0]++
newkeystr := fmt.Sprintf("%#v", key)
ret, err := c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery)
ret, err := c.RekeyUpdate(context.Background(), key, rkconf.Nonce, recovery, true)
if err == nil {
t.Fatalf("expected error, ret is %#v\noldkeystr: %s\nnewkeystr: %s", *ret, oldkeystr, newkeystr)
}
// Check progress has been reset
_, num, err := c.RekeyProgress(recovery, false)
_, num, err := c.RekeyProgress(recovery, false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -466,12 +466,12 @@ func TestCore_Rekey_Standby(t *testing.T) {
SecretShares: 1,
SecretThreshold: 1,
}
err = core.RekeyInit(newConf, false)
err = core.RekeyInit(newConf, false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Fetch new config with generated nonce
rkconf, err := core.RekeyConfig(false)
rkconf, err := core.RekeyConfig(false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -480,7 +480,7 @@ func TestCore_Rekey_Standby(t *testing.T) {
}
var rekeyResult *RekeyResult
for _, key := range keys {
rekeyResult, err = core.RekeyUpdate(context.Background(), key, rkconf.Nonce, false)
rekeyResult, err = core.RekeyUpdate(context.Background(), key, rkconf.Nonce, false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -499,12 +499,12 @@ func TestCore_Rekey_Standby(t *testing.T) {
TestWaitActive(t, core2)
// Rekey the master key again
err = core2.RekeyInit(newConf, false)
err = core2.RekeyInit(newConf, false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
// Fetch new config with generated nonce
rkconf, err = core2.RekeyConfig(false)
rkconf, err = core2.RekeyConfig(false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -513,7 +513,7 @@ func TestCore_Rekey_Standby(t *testing.T) {
}
var rekeyResult2 *RekeyResult
for _, key := range rekeyResult.SecretShares {
rekeyResult2, err = core2.RekeyUpdate(context.Background(), key, rkconf.Nonce, false)
rekeyResult2, err = core2.RekeyUpdate(context.Background(), key, rkconf.Nonce, false, true)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -542,7 +542,7 @@ func TestSysRekey_Verification_Invalid(t *testing.T) {
err := core.BarrierRekeyInit(&SealConfig{
VerificationRequired: true,
StoredShares: 1,
})
}, true)
if err == nil {
t.Fatal("expected error")
@ -599,11 +599,11 @@ func TestCancelRekey_Nonce(t *testing.T) {
t.Skip(t, "recovery rekey not supported")
}
err := c.RekeyInit(tc.config, tc.recovery)
err := c.RekeyInit(tc.config, tc.recovery, true)
require.NoError(t, err, "rekey init failed")
// try to cancel without the nonce
err = c.RekeyCancel(tc.recovery, "", 10*time.Minute)
err = c.RekeyCancel(tc.recovery, "", 10*time.Minute, true)
require.Error(t, err, "cancel should have errored")
// retrieve the nonce
@ -621,7 +621,7 @@ func TestCancelRekey_Nonce(t *testing.T) {
require.NotEmpty(t, nonce, "nonce missing")
// cancel successfully
err = c.RekeyCancel(tc.recovery, nonce, 10*time.Minute)
err = c.RekeyCancel(tc.recovery, nonce, 10*time.Minute, true)
require.NoError(t, err, "error on rekey cancel")
})
}
@ -675,14 +675,14 @@ func TestCancelRekey_Regression(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
c.RekeyCancel(tc.recovery, "", 10*time.Minute)
c.RekeyCancel(tc.recovery, "", 10*time.Minute, true)
}()
}
err := c.RekeyInit(tc.config, tc.recovery)
err := c.RekeyInit(tc.config, tc.recovery, true)
require.NoError(t, err)
wg.Wait()
happening, keys, err := c.RekeyProgress(tc.recovery, false)
happening, keys, err := c.RekeyProgress(tc.recovery, false, true)
require.NoError(t, err)
require.True(t, happening)
require.Equal(t, 0, keys)
@ -731,14 +731,14 @@ func TestCancelRekey_AfterDeadline(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.config.Type, func(t *testing.T) {
c := tc.core(t)
err := c.RekeyInit(tc.config, tc.recovery)
err := c.RekeyInit(tc.config, tc.recovery, true)
require.NoError(t, err)
// ensure that that 10 ms have passed before we cancel
time.Sleep(10 * time.Millisecond)
// set the deadline to a microsecond, which means we won't need a
// nonce to cancel the rekey
err = c.RekeyCancel(tc.recovery, "", time.Microsecond)
err = c.RekeyCancel(tc.recovery, "", time.Microsecond, true)
require.NoError(t, err)
})
}

View file

@ -209,25 +209,25 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount
}
re.tainted.Store(mountEntry.Tainted)
re.rootPaths.Store(pathsToRadix(paths.Root))
loginPathsEntry, err := parseUnauthenticatedPaths(paths.Unauthenticated)
loginPathsEntry, err := parseSpecialPaths(paths.Unauthenticated)
if err != nil {
return err
}
re.loginPaths.Store(loginPathsEntry)
binaryPathsEntry, err := parseUnauthenticatedPaths(paths.Binary)
binaryPathsEntry, err := parseSpecialPaths(paths.Binary)
if err != nil {
return err
}
re.binaryPaths.Store(binaryPathsEntry)
limitedPathsEntry, err := parseUnauthenticatedPaths(paths.Limited)
limitedPathsEntry, err := parseSpecialPaths(paths.Limited)
if err != nil {
return err
}
re.limitedPaths.Store(limitedPathsEntry)
allowSnapshotReadPathsEntry, err := parseUnauthenticatedPaths(paths.AllowSnapshotRead)
allowSnapshotReadPathsEntry, err := parseSpecialPaths(paths.AllowSnapshotRead)
if err != nil {
return err
}
@ -1024,7 +1024,7 @@ func wildcardError(path, msg string) error {
return fmt.Errorf("path %q: invalid use of wildcards %s", path, msg)
}
func isValidUnauthenticatedPath(path string) (bool, error) {
func isValidSpecialPath(path string) (bool, error) {
switch {
case strings.Count(path, "*") > 1:
return false, wildcardError(path, "(multiple '*' is forbidden)")
@ -1038,13 +1038,13 @@ func isValidUnauthenticatedPath(path string) (bool, error) {
return true, nil
}
// parseUnauthenticatedPaths converts a list of special paths to a
// parseSpecialPaths converts a list of special paths to a
// specialPathsEntry
func parseUnauthenticatedPaths(paths []string) (*specialPathsEntry, error) {
func parseSpecialPaths(paths []string) (*specialPathsEntry, error) {
var tempPaths []string
tempWildcardPaths := make([]wildcardPath, 0)
for _, path := range paths {
if ok, err := isValidUnauthenticatedPath(path); !ok {
if ok, err := isValidSpecialPath(path); !ok {
return nil, err
}

View file

@ -571,7 +571,7 @@ func TestParseUnauthenticatedPaths(t *testing.T) {
}
allPaths := append(paths, wildcardPaths...)
p, err := parseUnauthenticatedPaths(allPaths)
p, err := parseSpecialPaths(allPaths)
if err != nil {
t.Fatal(err)
}
@ -629,7 +629,7 @@ func TestParseUnauthenticatedPaths_Error(t *testing.T) {
}
for _, tc := range tcases {
_, err := parseUnauthenticatedPaths(tc.paths)
_, err := parseSpecialPaths(tc.paths)
if err == nil || err != nil && !strings.Contains(err.Error(), tc.err) {
t.Fatalf("bad: path: %s expect: %v got %v", tc.paths, tc.err, err)
}

View file

@ -1173,6 +1173,7 @@ type TestClusterOptions struct {
// ABCDLoggerNames names the loggers according to our ABCD convention when generating 4 clusters
ABCDLoggerNames bool
DisableTLS bool
}
type TestPluginConfig struct {
@ -1374,6 +1375,10 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T
})
}
scheme := "https"
if opts.DisableTLS {
scheme = "http"
}
//
// Listener setup
//
@ -1430,10 +1435,14 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T
tlsConfigs = append(tlsConfigs, tlsConfig)
lns := []*TestListener{
{
Listener: tls.NewListener(ln, tlsConfig),
Address: ln.Addr().(*net.TCPAddr),
Address: ln.Addr().(*net.TCPAddr),
},
}
if opts.DisableTLS {
lns[0].Listener = ln
} else {
lns[0].Listener = tls.NewListener(ln, tlsConfig)
}
listeners = append(listeners, lns)
var handler http.Handler = http.NewServeMux()
handlers = append(handlers, handler)
@ -1461,8 +1470,8 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T
audit.TypeSocket: audit.NewSocketBackend,
audit.TypeSyslog: audit.NewSyslogBackend,
},
RedirectAddr: fmt.Sprintf("https://127.0.0.1:%d", listeners[0][0].Address.Port),
ClusterAddr: "https://127.0.0.1:0",
RedirectAddr: fmt.Sprintf(scheme+"://127.0.0.1:%d", listeners[0][0].Address.Port),
ClusterAddr: scheme + "://127.0.0.1:0",
DisableMlock: true,
EnableUI: true,
EnableRaw: true,
@ -1559,6 +1568,7 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T
coreConfig.PeriodicLeaderRefreshInterval = base.PeriodicLeaderRefreshInterval
coreConfig.ClusterAddrBridge = base.ClusterAddrBridge
coreConfig.ObservationSystemConfig = base.ObservationSystemConfig
coreConfig.EnableUnauthenticatedAccess = base.EnableUnauthenticatedAccess
testApplyEntBaseConfig(coreConfig, base)
}
@ -1856,7 +1866,11 @@ func (testCluster *TestCluster) newCore(t testing.TB, idx int, coreConfig *CoreC
firstCoreNumber = opts.FirstCoreNumber
}
localConfig.RedirectAddr = fmt.Sprintf("https://127.0.0.1:%d", listeners[0].Address.Port)
scheme := "https"
if opts != nil && opts.DisableTLS {
scheme = "http"
}
localConfig.RedirectAddr = fmt.Sprintf(scheme+"://127.0.0.1:%d", listeners[0].Address.Port)
// if opts.SealFunc is provided, use that to generate a seal for the config instead
if opts != nil && opts.SealFunc != nil {
@ -1920,10 +1934,10 @@ func (testCluster *TestCluster) newCore(t testing.TB, idx int, coreConfig *CoreC
if opts != nil && opts.ClusterLayers != nil {
localConfig.ClusterNetworkLayer = opts.ClusterLayers.Layers()[idx]
localConfig.ClusterAddr = "https://" + localConfig.ClusterNetworkLayer.Listeners()[0].Addr().String()
localConfig.ClusterAddr = scheme + "://" + localConfig.ClusterNetworkLayer.Listeners()[0].Addr().String()
}
if opts != nil && opts.BaseClusterListenPort != 0 {
localConfig.ClusterAddr = fmt.Sprintf("https://127.0.0.1:%d", opts.BaseClusterListenPort+idx)
localConfig.ClusterAddr = fmt.Sprintf(scheme+"://127.0.0.1:%d", opts.BaseClusterListenPort+idx)
}
switch {
@ -2144,7 +2158,11 @@ func (testCluster *TestCluster) getAPIClient(
port int, tlsConfig *tls.Config,
) *api.Client {
transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = tlsConfig.Clone()
scheme := "http"
if opts != nil && !opts.DisableTLS {
scheme = "https"
transport.TLSClientConfig = tlsConfig.Clone()
}
if err := http2.ConfigureTransport(transport); err != nil {
t.Fatal(err)
}
@ -2159,7 +2177,7 @@ func (testCluster *TestCluster) getAPIClient(
if config.Error != nil {
t.Fatal(config.Error)
}
config.Address = fmt.Sprintf("https://127.0.0.1:%d", port)
config.Address = fmt.Sprintf(scheme+"://127.0.0.1:%d", port)
config.HttpClient = client
config.MaxRetries = 0
apiClient, err := api.NewClient(config)