diff --git a/changelog/_12712.txt b/changelog/_12712.txt new file mode 100644 index 0000000000..d53727d36d --- /dev/null +++ b/changelog/_12712.txt @@ -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". +``` diff --git a/command/server.go b/command/server.go index 031fe98937..e6e4db308a 100644 --- a/command/server.go +++ b/command/server.go @@ -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 { diff --git a/command/server/config.go b/command/server/config.go index f960f404ca..9888da47c5 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -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, diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index 3974232bd9..a1ac8f1ab9 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -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"}) diff --git a/http/handler.go b/http/handler.go index 59230666c3..aefa8e7957 100644 --- a/http/handler.go +++ b/http/handler.go @@ -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 diff --git a/http/sys_config_state_test.go b/http/sys_config_state_test.go index a837911851..6921e626b8 100644 --- a/http/sys_config_state_test.go +++ b/http/sys_config_state_test.go @@ -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 { diff --git a/http/sys_rekey.go b/http/sys_rekey.go index a9d9618fa5..a9a526604e 100644 --- a/http/sys_rekey.go +++ b/http/sys_rekey.go @@ -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"` -} diff --git a/http/sys_rekey_test.go b/http/sys_rekey_test.go index babe79c179..d5d8d06ce6 100644 --- a/http/sys_rekey_test.go +++ b/http/sys_rekey_test.go @@ -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) { diff --git a/sdk/helper/docker/testhelpers.go b/sdk/helper/docker/testhelpers.go index 60ee1fcd45..a1ba1c712f 100644 --- a/sdk/helper/docker/testhelpers.go +++ b/sdk/helper/docker/testhelpers.go @@ -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) diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go index 2adc9f30a8..b2985c9ec4 100644 --- a/sdk/helper/testcluster/docker/environment.go +++ b/sdk/helper/testcluster/docker/environment.go @@ -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 { diff --git a/sdk/helper/testcluster/types.go b/sdk/helper/testcluster/types.go index d61a55d2e0..23835062aa 100644 --- a/sdk/helper/testcluster/types.go +++ b/sdk/helper/testcluster/types.go @@ -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 { diff --git a/sdk/logical/response.go b/sdk/logical/response.go index f80cb83093..a673ceb0ca 100644 --- a/sdk/logical/response.go +++ b/sdk/logical/response.go @@ -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. diff --git a/sdk/logical/response_util.go b/sdk/logical/response_util.go index 3408524aca..231dc08ca9 100644 --- a/sdk/logical/response_util.go +++ b/sdk/logical/response_util.go @@ -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) { diff --git a/vault/core.go b/vault/core.go index 5c79682c7c..2da158e6ae 100644 --- a/vault/core.go +++ b/vault/core.go @@ -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) +} diff --git a/vault/external_tests/api/sys_rekey_ext_test.go b/vault/external_tests/api/sys_rekey_ext_test.go index 0a584ed247..960a206f10 100644 --- a/vault/external_tests/api/sys_rekey_ext_test.go +++ b/vault/external_tests/api/sys_rekey_ext_test.go @@ -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: diff --git a/vault/external_tests/system/system_binary/sys_rekey_config_reload_test.go b/vault/external_tests/system/system_binary/sys_rekey_config_reload_test.go new file mode 100644 index 0000000000..0f40a8bcf8 --- /dev/null +++ b/vault/external_tests/system/system_binary/sys_rekey_config_reload_test.go @@ -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") + }) +} diff --git a/vault/logical_system.go b/vault/logical_system.go index d018091d14..e830a62d78 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -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) diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 32f4405d65..c4180883cc 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -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$", diff --git a/vault/plugin_reload.go b/vault/plugin_reload.go index 8ea7ccd925..42d6ef6797 100644 --- a/vault/plugin_reload.go +++ b/vault/plugin_reload.go @@ -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 } diff --git a/vault/rekey.go b/vault/rekey.go index e808e6c891..b6bc23ab31 100644 --- a/vault/rekey.go +++ b/vault/rekey.go @@ -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()) } diff --git a/vault/rekey_test.go b/vault/rekey_test.go index c99b25374a..08ca240ff8 100644 --- a/vault/rekey_test.go +++ b/vault/rekey_test.go @@ -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) }) } diff --git a/vault/router.go b/vault/router.go index 3db7fb6caa..e92810224f 100644 --- a/vault/router.go +++ b/vault/router.go @@ -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 } diff --git a/vault/router_test.go b/vault/router_test.go index 1eba959b70..c845e5c0c1 100644 --- a/vault/router_test.go +++ b/vault/router_test.go @@ -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) } diff --git a/vault/testing.go b/vault/testing.go index d2a0fc9625..d2735282e9 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -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)