diff --git a/changelog/_12856.txt b/changelog/_12856.txt new file mode 100644 index 0000000000..2c49473868 --- /dev/null +++ b/changelog/_12856.txt @@ -0,0 +1,6 @@ +```release-note:change +core: sys/generate-root and sys/replication/dr/secondary/generate-operation-token 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 "generate-root" or "generate-operation-token" respectively. +``` +```release-note:change +core: secondary DR requests can now be authenticated using a root token generated on the primary. +``` diff --git a/http/custom_header_test.go b/http/custom_header_test.go index d0f90387e1..ca295a8910 100644 --- a/http/custom_header_test.go +++ b/http/custom_header_test.go @@ -4,6 +4,7 @@ package http import ( + "net/http" "testing" "github.com/hashicorp/vault/vault" @@ -83,8 +84,8 @@ func TestCustomResponseHeaders(t *testing.T) { testResponseStatus(t, resp, 200) testResponseHeader(t, resp, customHeader200) - resp = testHttpGet(t, token, addr+"/v1/sys/generate-root/update") - testResponseStatus(t, resp, 400) + resp = testHttpGet(t, token, addr+"/v1/sys/rekey/verify") + testResponseStatus(t, resp, http.StatusBadRequest) testResponseHeader(t, resp, defaultCustomHeaders) testResponseHeader(t, resp, customHeader4xx) testResponseHeader(t, resp, customHeader400) diff --git a/http/handler.go b/http/handler.go index 4cc83d9de5..80f04c3d9a 100644 --- a/http/handler.go +++ b/http/handler.go @@ -24,6 +24,7 @@ import ( "path" "regexp" "strings" + "sync" "time" "github.com/hashicorp/errwrap" @@ -234,22 +235,52 @@ func (h HandlerFunc) Handler(props *vault.HandlerProperties) http.Handler { var _ vault.HandlerHandler = HandlerFunc(func(props *vault.HandlerProperties) http.Handler { return nil }) +type handlerSettings struct { + unauthRekey bool + unauthGenerateRoot bool + unauthDROperationToken bool +} + +func getHandlerSettings(core *vault.Core) handlerSettings { + return handlerSettings{ + unauthRekey: core.GetEnableUnauthRekey(), + unauthGenerateRoot: core.GetEnableUnauthGenerateRoot(), + unauthDROperationToken: core.GetEnableUnauthDROperationToken(), + } +} + // 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) + var l sync.RWMutex + settings := getHandlerSettings(props.Core) + hws := handlerWithSettings(props, settings) return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { - if props.Core.GetEnableUnauthRekey() { - handlerUnauth.ServeHTTP(writer, req) - } else { - handlerAuth.ServeHTTP(writer, req) + newSettings := getHandlerSettings(props.Core) + + l.RLock() + changed := settings != newSettings + // Create a local copy so that we don't have to hold the lock while + // running the handler + myhws := hws + l.RUnlock() + + if changed { + l.Lock() + if settings != newSettings { + settings = newSettings + hws = handlerWithSettings(props, settings) + myhws = hws + } + l.Unlock() } + + myhws.ServeHTTP(writer, req) }) } -func handlerWithUnauthRekey(props *vault.HandlerProperties, unauthRekey bool) http.Handler { +func handlerWithSettings(props *vault.HandlerProperties, settings handlerSettings) http.Handler { core := props.Core // Create the muxer to handle the actual endpoints @@ -285,14 +316,19 @@ func handlerWithUnauthRekey(props *vault.HandlerProperties, unauthRekey bool) ht WithRedactClusterName(props.ListenerConfig.RedactClusterName), WithRedactVersion(props.ListenerConfig.RedactVersion))) mux.Handle("/v1/sys/monitor", handleLogicalNoForward(core, chrootNamespace)) - mux.Handle("/v1/sys/generate-root/attempt", handleRequestForwarding(core, - handleAuditNonLogical(core, handleSysGenerateRootAttempt(core, vault.GenerateStandardRootTokenStrategy)))) - mux.Handle("/v1/sys/generate-root/update", handleRequestForwarding(core, - handleAuditNonLogical(core, handleSysGenerateRootUpdate(core, vault.GenerateStandardRootTokenStrategy)))) + + // Register generate-root endpoints as unauthenticated handlers only if unauthGenerateRoot is true. + // When false, these endpoints will be handled by the sys backend as authenticated endpoints. + if settings.unauthGenerateRoot { + mux.Handle("/v1/sys/generate-root/attempt", handleRequestForwarding(core, + handleAuditNonLogical(core, handleSysGenerateRootAttempt(core, vault.GenerateStandardRootTokenStrategy)))) + mux.Handle("/v1/sys/generate-root/update", handleRequestForwarding(core, + handleAuditNonLogical(core, handleSysGenerateRootUpdate(core, vault.GenerateStandardRootTokenStrategy)))) + } // 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 { + if settings.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))) @@ -347,7 +383,9 @@ func handlerWithUnauthRekey(props *vault.HandlerProperties, unauthRekey bool) ht } else { mux.Handle("/v1/sys/in-flight-req", handleLogicalNoForward(core, chrootNamespace)) } - entAdditionalRoutes(mux, core) + if settings.unauthDROperationToken { + entDROperationRoutes(mux, core) + } } // Build up a chain of wrapping handlers. @@ -421,10 +459,7 @@ func (w *copyResponseWriter) WriteHeader(code int) { func handleAuditNonLogical(core *vault.Core, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origBody := new(bytes.Buffer) - reader := io.NopCloser(io.TeeReader(r.Body, origBody)) - r.Body = reader - req, _, status, err := buildLogicalRequestNoAuth(core.PerfStandby(), core.RouterAccess(), w, r) + req, origBody, status, err := buildLogicalRequestNoAuth(core.PerfStandby(), core.RouterAccess(), w, r) if err != nil || status != 0 { respondError(w, status, err) return diff --git a/http/sys_generate_root.go b/http/sys_generate_root.go index 37bbdf0cd4..394e4086bf 100644 --- a/http/sys_generate_root.go +++ b/http/sys_generate_root.go @@ -4,14 +4,9 @@ package http import ( - "encoding/base64" - "encoding/hex" - "errors" - "fmt" "io" "net/http" - "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/vault/vault" ) @@ -34,108 +29,36 @@ func handleSysGenerateRootAttemptGet(core *vault.Core, w http.ResponseWriter, r ctx, cancel := core.GetContext() defer cancel() - // Get the current seal configuration - barrierConfig, err := core.SealAccess().BarrierConfig(ctx) + status, code, err := vault.HandleSysGenerateRootAttemptGet(ctx, core, otp, true) if err != nil { - respondError(w, http.StatusInternalServerError, err) + respondError(w, code, err) return } - if barrierConfig == nil { - respondError(w, http.StatusBadRequest, fmt.Errorf("server is not yet initialized")) - return - } - - sealConfig := barrierConfig - if core.SealAccess().RecoveryKeySupported() { - sealConfig, err = core.SealAccess().RecoveryConfig(ctx) - if err != nil { - respondError(w, http.StatusInternalServerError, err) - return - } - } - - // Get the generation configuration - generationConfig, err := core.GenerateRootConfiguration() - if err != nil { - respondError(w, http.StatusInternalServerError, err) - return - } - - // Get the progress - progress, err := core.GenerateRootProgress() - if err != nil { - respondError(w, http.StatusInternalServerError, err) - return - } - var otpLength int - if core.DisableSSCTokens() { - otpLength = vault.TokenLength + vault.OldTokenPrefixLength - } else { - otpLength = vault.TokenLength + vault.TokenPrefixLength - } - - // Format the status - status := &GenerateRootStatusResponse{ - Started: false, - Progress: progress, - Required: sealConfig.SecretThreshold, - Complete: false, - OTPLength: otpLength, - OTP: otp, - } - if generationConfig != nil { - status.Nonce = generationConfig.Nonce - status.Started = true - status.PGPFingerprint = generationConfig.PGPFingerprint - } respondOk(w, status) } func handleSysGenerateRootAttemptPut(core *vault.Core, w http.ResponseWriter, r *http.Request, generateStrategy vault.GenerateRootStrategy) { // Parse the request - var req GenerateRootInitRequest + var req vault.GenerateRootInitRequest if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil && err != io.EOF { respondError(w, http.StatusBadRequest, err) return } - var err error - var genned bool - - switch { - case len(req.PGPKey) > 0, len(req.OTP) > 0: - default: - genned = true - if core.DisableSSCTokens() { - req.OTP, err = base62.Random(vault.TokenLength + vault.OldTokenPrefixLength) - } else { - req.OTP, err = base62.Random(vault.TokenLength + vault.TokenPrefixLength) - } - if err != nil { - respondError(w, http.StatusInternalServerError, err) - return - } - } - - // Attemptialize the generation - if err := core.GenerateRootInit(req.OTP, req.PGPKey, generateStrategy); err != nil { - respondError(w, http.StatusBadRequest, err) + otp, code, err := vault.HandleSysGenerateRootAttemptPut(core, generateStrategy, &req, true) + if err != nil { + respondError(w, code, err) return } - if genned { - handleSysGenerateRootAttemptGet(core, w, r, req.OTP) - return - } - - handleSysGenerateRootAttemptGet(core, w, r, "") + handleSysGenerateRootAttemptGet(core, w, r, otp) } func handleSysGenerateRootAttemptDelete(core *vault.Core, w http.ResponseWriter, r *http.Request) { - err := core.GenerateRootCancel() + code, err := vault.HandleSysGenerateRootAttemptDelete(core, true) if err != nil { - respondError(w, http.StatusInternalServerError, err) + respondError(w, code, err) return } respondOk(w, nil) @@ -144,81 +67,21 @@ func handleSysGenerateRootAttemptDelete(core *vault.Core, w http.ResponseWriter, func handleSysGenerateRootUpdate(core *vault.Core, generateStrategy vault.GenerateRootStrategy) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Parse the request - var req GenerateRootUpdateRequest + var req vault.GenerateRootUpdateRequest 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 root generation - result, err := core.GenerateRootUpdate(ctx, key, req.Nonce, generateStrategy) + resp, code, err := vault.HandleSysGenerateRootUpdate(ctx, core, generateStrategy, &req, true) if err != nil { - respondError(w, http.StatusBadRequest, err) + respondError(w, code, err) return } - resp := &GenerateRootStatusResponse{ - Complete: result.Progress == result.Required, - Nonce: req.Nonce, - Progress: result.Progress, - Required: result.Required, - Started: true, - EncodedToken: result.EncodedToken, - PGPFingerprint: result.PGPFingerprint, - } - - if generateStrategy == vault.GenerateStandardRootTokenStrategy { - resp.EncodedRootToken = result.EncodedToken - } - respondOk(w, resp) }) } - -type GenerateRootInitRequest struct { - OTP string `json:"otp"` - PGPKey string `json:"pgp_key"` -} - -type GenerateRootStatusResponse struct { - Nonce string `json:"nonce"` - Started bool `json:"started"` - Progress int `json:"progress"` - Required int `json:"required"` - Complete bool `json:"complete"` - EncodedToken string `json:"encoded_token"` - EncodedRootToken string `json:"encoded_root_token"` - PGPFingerprint string `json:"pgp_fingerprint"` - OTP string `json:"otp"` - OTPLength int `json:"otp_length"` -} - -type GenerateRootUpdateRequest struct { - Nonce string - Key string -} diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index 6dce3b361e..6217a0702b 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -26,90 +26,201 @@ import ( var tokenLength string = fmt.Sprintf("%d", vault.TokenLength+vault.TokenPrefixLength) -func TestSysGenerateRootAttempt_Status(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp, err := http.Get(addr + "/v1/sys/generate-root/attempt") - if err != nil { - t.Fatalf("err: %s", err) +// TestSysGenerateRoot_Status verifies the initial output of sys/generate-root/attempt, +// prior to actually starting a root token generation. +func TestSysGenerateRoot_Status(t *testing.T) { + testCases := []struct { + name string + enableUnauthGenerateRoot bool + useToken bool + expectError bool + }{ + { + name: "default-unauthenticated", + enableUnauthGenerateRoot: true, + useToken: false, + expectError: false, + }, + { + name: "default-authenticated", + enableUnauthGenerateRoot: true, + useToken: true, + expectError: false, + }, + { + name: "auth-required-without-token", + enableUnauthGenerateRoot: false, + useToken: false, + expectError: true, + }, + { + name: "auth-required-with-token", + enableUnauthGenerateRoot: false, + useToken: true, + expectError: false, + }, } - var actual map[string]interface{} - expected := map[string]interface{}{ - "started": false, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "nonce": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - expected["otp"] = actual["otp"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conf := &vault.CoreConfig{} + if tc.enableUnauthGenerateRoot { + conf.EnableUnauthenticatedAccess = []string{"generate-root"} + } + cluster := vault.NewTestCluster(t, conf, &vault.TestClusterOptions{ + HandlerFunc: Handler, + NumCores: 1, + }) + cl := cluster.Cores[0].Client + + if tc.useToken { + cl.SetToken(cluster.RootToken) + } else { + cl.SetToken("") + } + + resp, err := cl.Logical().Read("sys/generate-root/attempt") + + 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, + "progress": json.Number("0"), + "required": json.Number("3"), + "complete": false, + "nonce": "", + "otp": "", + "otp_length": json.Number("28"), + "pgp_fingerprint": "", + "encoded_token": "", + "encoded_root_token": "", + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) + } + }) } } func TestSysGenerateRootAttempt_Setup_OTP(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", nil) - testResponseStatus(t, resp, 200) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "started": true, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - expected["otp"] = actual["otp"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) + testCases := []struct { + name string + enableUnauthGenerateRoot bool + useToken bool + expectError bool + }{ + { + name: "unauthenticated", + enableUnauthGenerateRoot: true, + useToken: false, + expectError: false, + }, + { + name: "authenticated", + enableUnauthGenerateRoot: true, + useToken: true, + expectError: false, + }, + { + name: "auth-required", + enableUnauthGenerateRoot: false, + useToken: true, + expectError: false, + }, + { + name: "auth-required-no-token", + enableUnauthGenerateRoot: false, + useToken: false, + expectError: true, + }, } - resp = testHttpGet(t, token, addr+"/v1/sys/generate-root/attempt") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conf := &vault.CoreConfig{} + if tc.enableUnauthGenerateRoot { + conf.EnableUnauthenticatedAccess = []string{"generate-root"} + } + cluster := vault.NewTestCluster(t, conf, &vault.TestClusterOptions{ + DisableTLS: true, + HandlerFunc: Handler, + NumCores: 1, + }) + cl := cluster.Cores[0].Client - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "started": true, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "otp": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) + reqToken := "" + if tc.useToken { + reqToken = cl.Token() + } + addr := cl.Address() + + resp := testHttpPut(t, reqToken, addr+"/v1/sys/generate-root/attempt", nil) + + if tc.expectError { + testResponseStatus(t, resp, http.StatusForbidden) + return + } + testResponseStatus(t, resp, http.StatusOK) + + var actual map[string]interface{} + testResponseBody(t, resp, &actual) + + expected := map[string]interface{}{ + "started": true, + "progress": json.Number("0"), + "required": json.Number("3"), + "complete": false, + "encoded_token": "", + "encoded_root_token": "", + "pgp_fingerprint": "", + "otp_length": json.Number(tokenLength), + } + + if actual["nonce"].(string) == "" { + t.Fatalf("nonce was empty") + } + expected["nonce"] = actual["nonce"] + expected["otp"] = actual["otp"] + + if diff := deep.Equal(actual, expected); diff != nil { + t.Fatal(diff) + } + + resp = testHttpGet(t, reqToken, addr+"/v1/sys/generate-root/attempt") + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "started": true, + "progress": json.Number("0"), + "required": json.Number("3"), + "complete": false, + "encoded_token": "", + "encoded_root_token": "", + "pgp_fingerprint": "", + "otp": "", + "otp_length": json.Number(tokenLength), + } + testResponseStatus(t, resp, http.StatusOK) + testResponseBody(t, resp, &actual) + if actual["nonce"].(string) == "" { + t.Fatalf("nonce was empty") + } + expected["nonce"] = actual["nonce"] + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) + } + }) } } @@ -122,7 +233,7 @@ func TestSysGenerateRootAttempt_Setup_PGP(t *testing.T) { resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{ "pgp_key": pgpkeys.TestPubKey1, }) - testResponseStatus(t, resp, 200) + testResponseStatus(t, resp, http.StatusOK) resp = testHttpGet(t, token, addr+"/v1/sys/generate-root/attempt") @@ -138,7 +249,7 @@ func TestSysGenerateRootAttempt_Setup_PGP(t *testing.T) { "otp": "", "otp_length": json.Number(tokenLength), } - testResponseStatus(t, resp, 200) + testResponseStatus(t, resp, http.StatusOK) testResponseBody(t, resp, &actual) if actual["nonce"].(string) == "" { t.Fatalf("nonce was empty") @@ -149,64 +260,6 @@ func TestSysGenerateRootAttempt_Setup_PGP(t *testing.T) { } } -func TestSysGenerateRootAttempt_Cancel(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", nil) - - var actual map[string]interface{} - expected := map[string]interface{}{ - "started": true, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if actual["nonce"].(string) == "" { - t.Fatalf("nonce was empty") - } - expected["nonce"] = actual["nonce"] - expected["otp"] = actual["otp"] - if diff := deep.Equal(actual, expected); diff != nil { - t.Fatal(diff) - } - - resp = testHttpDelete(t, token, addr+"/v1/sys/generate-root/attempt") - testResponseStatus(t, resp, 204) - - resp, err := http.Get(addr + "/v1/sys/generate-root/attempt") - if err != nil { - t.Fatalf("err: %s", err) - } - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "started": false, - "progress": json.Number("0"), - "required": json.Number("3"), - "complete": false, - "encoded_token": "", - "encoded_root_token": "", - "pgp_fingerprint": "", - "nonce": "", - "otp": "", - "otp_length": json.Number(tokenLength), - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } -} - func enableNoopAudit(t *testing.T, token string, core *vault.Core) { t.Helper() auditReq := &logical.Request{ @@ -270,7 +323,7 @@ func TestSysGenerateRoot_ReAttemptUpdate(t *testing.T) { TestServerAuth(t, addr, token) resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", nil) - testResponseStatus(t, resp, 200) + testResponseStatus(t, resp, http.StatusOK) resp = testHttpDelete(t, token, addr+"/v1/sys/generate-root/attempt") testResponseStatus(t, resp, 204) @@ -279,199 +332,295 @@ func TestSysGenerateRoot_ReAttemptUpdate(t *testing.T) { "pgp_key": pgpkeys.TestPubKey1, }) - testResponseStatus(t, resp, 200) + testResponseStatus(t, resp, http.StatusOK) } func TestSysGenerateRoot_Update_OTP(t *testing.T) { - var records *[][]byte - ln, addr, token, keys := testServerWithAudit(t, &records) - defer ln.Close() + testCases := []struct { + name string + enableUnauthGenerateRoot bool + useToken bool + expectError bool + }{ + { + name: "unauthenticated", + enableUnauthGenerateRoot: true, + useToken: false, + expectError: false, + }, + { + name: "authenticated", + enableUnauthGenerateRoot: true, + useToken: true, + expectError: false, + }, + { + name: "auth-required", + enableUnauthGenerateRoot: false, + useToken: true, + expectError: false, + }, + { + name: "auth-required-no-token", + enableUnauthGenerateRoot: false, + useToken: false, + expectError: true, + }, + } - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{}) - var rootGenerationStatus map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &rootGenerationStatus) - otp := rootGenerationStatus["otp"].(string) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conf := &vault.CoreConfig{} + if tc.enableUnauthGenerateRoot { + conf.EnableUnauthenticatedAccess = []string{"generate-root"} + } + cluster := vault.NewTestCluster(t, conf, &vault.TestClusterOptions{ + DisableTLS: true, + HandlerFunc: Handler, + NumCores: 1, + }) + cl := cluster.Cores[0].Client + reqToken := "" + if tc.useToken { + reqToken = cl.Token() + } + addr := cl.Address() - var actual map[string]interface{} - var expected map[string]interface{} - for i, key := range keys { - resp = testHttpPut(t, token, addr+"/v1/sys/generate-root/update", map[string]interface{}{ - "nonce": rootGenerationStatus["nonce"].(string), - "key": hex.EncodeToString(key), + resp := testHttpPut(t, reqToken, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{}) + var rootGenerationStatus map[string]interface{} + if tc.expectError { + testResponseStatus(t, resp, http.StatusForbidden) + return + } + testResponseStatus(t, resp, http.StatusOK) + testResponseBody(t, resp, &rootGenerationStatus) + otp := rootGenerationStatus["otp"].(string) + + var actual map[string]interface{} + var expected map[string]interface{} + for i, key := range cluster.BarrierKeys { + resp = testHttpPut(t, reqToken, addr+"/v1/sys/generate-root/update", map[string]interface{}{ + "nonce": rootGenerationStatus["nonce"].(string), + "key": hex.EncodeToString(key), + }) + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "complete": false, + "nonce": rootGenerationStatus["nonce"].(string), + "progress": json.Number(fmt.Sprintf("%d", i+1)), + "required": json.Number(fmt.Sprintf("%d", len(cluster.BarrierKeys))), + "started": true, + "pgp_fingerprint": "", + "otp": "", + "otp_length": json.Number("0"), + } + if i+1 == len(cluster.BarrierKeys) { + expected["complete"] = true + } + testResponseStatus(t, resp, http.StatusOK) + testResponseBody(t, resp, &actual) + } + + if actual["encoded_token"] == nil || actual["encoded_token"] == "" { + t.Fatalf("no encoded token found in response") + } + if actual["encoded_root_token"] == nil || actual["encoded_root-token"] == "" { + t.Fatalf("no encoded root token found in response") + } + expected["encoded_token"] = actual["encoded_token"] + expected["encoded_root_token"] = actual["encoded_root_token"] + expected["encoded_token"] = actual["encoded_token"] + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) + } + + tokenBytes, err := base64.RawStdEncoding.DecodeString(expected["encoded_token"].(string)) + if err != nil { + t.Fatal(err) + } + + tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp)) + if err != nil { + t.Fatal(err) + } + newRootToken := string(tokenBytes) + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "id": newRootToken, + "display_name": "root", + "meta": interface{}(nil), + "num_uses": json.Number("0"), + "policies": []interface{}{"root"}, + "orphan": true, + "creation_ttl": json.Number("0"), + "ttl": json.Number("0"), + "path": "auth/token/root", + "explicit_max_ttl": json.Number("0"), + "expire_time": nil, + "entity_id": "", + "type": "service", + } + + resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") + testResponseStatus(t, resp, http.StatusOK) + testResponseBody(t, resp, &actual) + + expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] + expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] + + if !reflect.DeepEqual(actual["data"], expected) { + t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"]) + } }) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "complete": false, - "nonce": rootGenerationStatus["nonce"].(string), - "progress": json.Number(fmt.Sprintf("%d", i+1)), - "required": json.Number(fmt.Sprintf("%d", len(keys))), - "started": true, - "pgp_fingerprint": "", - "otp": "", - "otp_length": json.Number("0"), - } - if i+1 == len(keys) { - expected["complete"] = true - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - } - - if actual["encoded_token"] == nil || actual["encoded_token"] == "" { - t.Fatalf("no encoded token found in response") - } - if actual["encoded_root_token"] == nil || actual["encoded_root-token"] == "" { - t.Fatalf("no encoded root token found in response") - } - expected["encoded_token"] = actual["encoded_token"] - expected["encoded_root_token"] = actual["encoded_root_token"] - expected["encoded_token"] = actual["encoded_token"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } - - tokenBytes, err := base64.RawStdEncoding.DecodeString(expected["encoded_token"].(string)) - if err != nil { - t.Fatal(err) - } - - tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp)) - if err != nil { - t.Fatal(err) - } - newRootToken := string(tokenBytes) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "id": newRootToken, - "display_name": "root", - "meta": interface{}(nil), - "num_uses": json.Number("0"), - "policies": []interface{}{"root"}, - "orphan": true, - "creation_ttl": json.Number("0"), - "ttl": json.Number("0"), - "path": "auth/token/root", - "explicit_max_ttl": json.Number("0"), - "expire_time": nil, - "entity_id": "", - "type": "service", - } - - resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] - expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] - - if !reflect.DeepEqual(actual["data"], expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"]) - } - - for _, r := range *records { - t.Log(string(r)) } } func TestSysGenerateRoot_Update_PGP(t *testing.T) { - core, keys, token := vault.TestCoreUnsealed(t) - ln, addr := TestServer(t, core) - defer ln.Close() - TestServerAuth(t, addr, token) - - resp := testHttpPut(t, token, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{ - "pgp_key": pgpkeys.TestPubKey1, - }) - testResponseStatus(t, resp, 200) - - // We need to get the nonce first before we update - resp, err := http.Get(addr + "/v1/sys/generate-root/attempt") - if err != nil { - t.Fatalf("err: %s", err) + testCases := []struct { + name string + enableUnauthGenerateRoot bool + useToken bool + expectError bool + }{ + { + name: "unauthenticated", + enableUnauthGenerateRoot: true, + useToken: false, + expectError: false, + }, + { + name: "authenticated", + enableUnauthGenerateRoot: true, + useToken: true, + expectError: false, + }, + { + name: "auth-required", + enableUnauthGenerateRoot: false, + useToken: true, + expectError: false, + }, + { + name: "auth-required-no-token", + enableUnauthGenerateRoot: false, + useToken: false, + expectError: true, + }, } - var rootGenerationStatus map[string]interface{} - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &rootGenerationStatus) - var actual map[string]interface{} - var expected map[string]interface{} - for i, key := range keys { - resp = testHttpPut(t, token, addr+"/v1/sys/generate-root/update", map[string]interface{}{ - "nonce": rootGenerationStatus["nonce"].(string), - "key": hex.EncodeToString(key), + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conf := &vault.CoreConfig{} + if tc.enableUnauthGenerateRoot { + conf.EnableUnauthenticatedAccess = []string{"generate-root"} + } + cluster := vault.NewTestCluster(t, conf, &vault.TestClusterOptions{ + DisableTLS: true, + HandlerFunc: Handler, + NumCores: 1, + }) + cl := cluster.Cores[0].Client + reqToken := "" + if tc.useToken { + reqToken = cl.Token() + } + addr := cl.Address() + + resp := testHttpPut(t, reqToken, addr+"/v1/sys/generate-root/attempt", map[string]interface{}{ + "pgp_key": pgpkeys.TestPubKey1, + }) + if tc.expectError { + testResponseStatus(t, resp, http.StatusForbidden) + return + } + testResponseStatus(t, resp, http.StatusOK) + + // We need to get the nonce first before we update + resp = testHttpGet(t, reqToken, addr+"/v1/sys/generate-root/attempt") + var rootGenerationStatus map[string]interface{} + testResponseStatus(t, resp, http.StatusOK) + testResponseBody(t, resp, &rootGenerationStatus) + + var actual map[string]interface{} + var expected map[string]interface{} + for i, key := range cluster.BarrierKeys { + resp = testHttpPut(t, reqToken, addr+"/v1/sys/generate-root/update", map[string]interface{}{ + "nonce": rootGenerationStatus["nonce"].(string), + "key": hex.EncodeToString(key), + }) + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "complete": false, + "nonce": rootGenerationStatus["nonce"].(string), + "progress": json.Number(fmt.Sprintf("%d", i+1)), + "required": json.Number(fmt.Sprintf("%d", len(cluster.BarrierKeys))), + "started": true, + "pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793", + "otp": "", + "otp_length": json.Number("0"), + } + if i+1 == len(cluster.BarrierKeys) { + expected["complete"] = true + } + testResponseStatus(t, resp, http.StatusOK) + testResponseBody(t, resp, &actual) + } + + if actual["encoded_token"] == nil || actual["encoded_token"] == "" { + t.Fatalf("no encoded token found in response") + } + if actual["encoded_root_token"] == nil || actual["encoded_root-token"] == "" { + t.Fatalf("no encoded root token found in response") + } + expected["encoded_token"] = actual["encoded_token"] + expected["encoded_root_token"] = actual["encoded_root_token"] + expected["encoded_token"] = actual["encoded_token"] + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) + } + + decodedTokenBuf, err := pgpkeys.DecryptBytes(actual["encoded_token"].(string), pgpkeys.TestPrivKey1) + if err != nil { + t.Fatal(err) + } + if decodedTokenBuf == nil { + t.Fatal("decoded root token buffer is nil") + } + + newRootToken := decodedTokenBuf.String() + + actual = map[string]interface{}{} + expected = map[string]interface{}{ + "id": newRootToken, + "display_name": "root", + "meta": interface{}(nil), + "num_uses": json.Number("0"), + "policies": []interface{}{"root"}, + "orphan": true, + "creation_ttl": json.Number("0"), + "ttl": json.Number("0"), + "path": "auth/token/root", + "explicit_max_ttl": json.Number("0"), + "expire_time": nil, + "entity_id": "", + "type": "service", + } + + resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") + testResponseStatus(t, resp, http.StatusOK) + testResponseBody(t, resp, &actual) + + expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] + expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] + + if diff := deep.Equal(actual["data"], expected); diff != nil { + t.Fatal(diff) + } }) - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "complete": false, - "nonce": rootGenerationStatus["nonce"].(string), - "progress": json.Number(fmt.Sprintf("%d", i+1)), - "required": json.Number(fmt.Sprintf("%d", len(keys))), - "started": true, - "pgp_fingerprint": "816938b8a29146fbe245dd29e7cbaf8e011db793", - "otp": "", - "otp_length": json.Number("0"), - } - if i+1 == len(keys) { - expected["complete"] = true - } - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - } - - if actual["encoded_token"] == nil || actual["encoded_token"] == "" { - t.Fatalf("no encoded token found in response") - } - if actual["encoded_root_token"] == nil || actual["encoded_root-token"] == "" { - t.Fatalf("no encoded root token found in response") - } - expected["encoded_token"] = actual["encoded_token"] - expected["encoded_root_token"] = actual["encoded_root_token"] - expected["encoded_token"] = actual["encoded_token"] - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual) - } - - decodedTokenBuf, err := pgpkeys.DecryptBytes(actual["encoded_token"].(string), pgpkeys.TestPrivKey1) - if err != nil { - t.Fatal(err) - } - if decodedTokenBuf == nil { - t.Fatal("decoded root token buffer is nil") - } - - newRootToken := decodedTokenBuf.String() - - actual = map[string]interface{}{} - expected = map[string]interface{}{ - "id": newRootToken, - "display_name": "root", - "meta": interface{}(nil), - "num_uses": json.Number("0"), - "policies": []interface{}{"root"}, - "orphan": true, - "creation_ttl": json.Number("0"), - "ttl": json.Number("0"), - "path": "auth/token/root", - "explicit_max_ttl": json.Number("0"), - "expire_time": nil, - "entity_id": "", - "type": "service", - } - - resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self") - testResponseStatus(t, resp, 200) - testResponseBody(t, resp, &actual) - - expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"] - expected["accessor"] = actual["data"].(map[string]interface{})["accessor"] - - if diff := deep.Equal(actual["data"], expected); diff != nil { - t.Fatal(diff) } } diff --git a/http/util_oss.go b/http/util_oss.go index d275e1aef2..77232f5815 100644 --- a/http/util_oss.go +++ b/http/util_oss.go @@ -19,7 +19,7 @@ func entWrapGenericHandler(core *vault.Core, in http.Handler, props *vault.Handl return wrapGenericHandler(core, in, props) } -func entAdditionalRoutes(mux *http.ServeMux, core *vault.Core) {} +func entDROperationRoutes(mux *http.ServeMux, core *vault.Core) {} func entAdjustResponse(core *vault.Core, w http.ResponseWriter, req *logical.Request) { } diff --git a/sdk/helper/ldaputil/config.go b/sdk/helper/ldaputil/config.go index 02fc486742..fcb0b90500 100644 --- a/sdk/helper/ldaputil/config.go +++ b/sdk/helper/ldaputil/config.go @@ -22,10 +22,10 @@ import ( var ( ldapDerefAliasMap = map[string]int{ - "never": ldap.NeverDerefAliases, - "finding": ldap.DerefFindingBaseObj, - "searching": ldap.DerefInSearching, - "always": ldap.DerefAlways, + "never": ldap.NeverDerefAliases, + "finding": ldap.DerefFindingBaseObj, + "searching": ldap.DerefInSearching, + "always": ldap.DerefAlways, } cannotDeriveUserBindDNError = errors.New("cannot derive UserBindDN") diff --git a/vault/core.go b/vault/core.go index 2da158e6ae..ef48275b6c 100644 --- a/vault/core.go +++ b/vault/core.go @@ -279,6 +279,16 @@ type Core struct { // endpoints (false, default). enableUnauthRekey *atomic.Bool + // enableUnauthGenerateRoot controls whether generate-root endpoints are registered as + // unauthenticated endpoints (true) or as authenticated sys backend + // endpoints (false, default). + enableUnauthGenerateRoot *atomic.Bool + + // enableUnauthDROperationToken controls whether DR operation token endpoints are registered as + // unauthenticated endpoints (true) or as authenticated sys backend + // endpoints (false, default). + enableUnauthDROperationToken *atomic.Bool + // HABackend may be available depending on the physical backend ha physical.HABackend @@ -1167,6 +1177,8 @@ func CreateCore(conf *CoreConfig) (*Core, error) { rpcLastSuccessfulHeartbeat: new(atomic.Value), reportingScanDirectory: conf.ReportingScanDirectory, enableUnauthRekey: new(atomic.Bool), + enableUnauthGenerateRoot: new(atomic.Bool), + enableUnauthDROperationToken: new(atomic.Bool), } c.certCountManager = cert_count.InitCertificateCountManager(c.logger) @@ -1450,11 +1462,15 @@ func NewCore(conf *CoreConfig) (*Core, error) { c.clusterAddrBridge = conf.ClusterAddrBridge c.licenseReloadCh = conf.LicenseReload - // Check if "rekey" is in the EnableUnauthenticatedAccess list + // Check if endpoints are in the EnableUnauthenticatedAccess list for _, endpoint := range conf.EnableUnauthenticatedAccess { - if endpoint == "rekey" { + switch endpoint { + case "rekey": c.enableUnauthRekey.Store(true) - break + case "generate-root": + c.enableUnauthGenerateRoot.Store(true) + case "generate-operation-token": + c.enableUnauthDROperationToken.Store(true) } } @@ -4461,16 +4477,24 @@ func (c *Core) ReloadEnableUnauthenticatedAccess() { return } - // Check if "rekey" is in the EnableUnauthenticatedAccess list + // Check which endpoints are in the EnableUnauthenticatedAccess list enableRekey := false + enableGenerateRoot := false + enableDROperationToken := false for _, endpoint := range conf.(*server.Config).EnableUnauthenticatedAccess { - if endpoint == "rekey" { + switch endpoint { + case "rekey": enableRekey = true - break + case "generate-root": + enableGenerateRoot = true + case "generate-operation-token": + enableDROperationToken = true } } c.enableUnauthRekey.Store(enableRekey) + c.enableUnauthGenerateRoot.Store(enableGenerateRoot) + c.enableUnauthDROperationToken.Store(enableDROperationToken) } type PeerNode struct { @@ -4965,3 +4989,19 @@ func (c *Core) GetEnableUnauthRekey() bool { func (c *Core) SetEnableUnauthRekey(val bool) { c.enableUnauthRekey.Store(val) } + +func (c *Core) GetEnableUnauthGenerateRoot() bool { + return c.enableUnauthGenerateRoot.Load() +} + +func (c *Core) SetEnableUnauthGenerateRoot(val bool) { + c.enableUnauthGenerateRoot.Store(val) +} + +func (c *Core) GetEnableUnauthDROperationToken() bool { + return c.enableUnauthDROperationToken.Load() +} + +func (c *Core) SetEnableUnauthDROperationToken(val bool) { + c.enableUnauthDROperationToken.Store(val) +} diff --git a/vault/external_tests/raft/raft_binary/raft_test.go b/vault/external_tests/raft/raft_binary/raft_test.go index f278df6a4c..8c1f516ac3 100644 --- a/vault/external_tests/raft/raft_binary/raft_test.go +++ b/vault/external_tests/raft/raft_binary/raft_test.go @@ -405,6 +405,7 @@ func TestRaft_LogStore_Migration_Snapshot(t *testing.T) { // caching the old cluster's barrier keys oldBarrierKeys := cluster.GetBarrierKeys() + oldRootToken := cluster.GetRootToken() // clean up the old cluster as there is no further use to it cluster.Cleanup() @@ -432,6 +433,7 @@ func TestRaft_LogStore_Migration_Snapshot(t *testing.T) { testcluster.WaitForActiveNode(ctx, newCluster) // generate a root token as the unseal keys have changed + newCluster.SetRootToken(oldRootToken) rootToken, err := testcluster.GenerateRoot(newCluster, testcluster.GenerateRootRegular) if err != nil { t.Fatal(err) diff --git a/vault/generate_root.go b/vault/generate_root.go index fb4da4380d..df99ae386b 100644 --- a/vault/generate_root.go +++ b/vault/generate_root.go @@ -7,9 +7,12 @@ import ( "bytes" "context" "encoding/base64" + "encoding/hex" "errors" "fmt" + "net/http" + "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/sdk/helper/consts" @@ -90,9 +93,11 @@ type GenerateRootResult struct { } // GenerateRootProgress is used to return the root generation progress (num shares) -func (c *Core) GenerateRootProgress() (int, error) { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) GenerateRootProgress(grabLock bool) (int, error) { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() && !c.recoveryMode { return 0, consts.ErrSealed } @@ -108,9 +113,11 @@ func (c *Core) GenerateRootProgress() (int, error) { // GenerateRootConfiguration is used to read the root generation configuration // It stubbornly refuses to return the OTP if one is there. -func (c *Core) GenerateRootConfiguration() (*GenerateRootConfig, error) { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) GenerateRootConfiguration(grabLock bool) (*GenerateRootConfig, error) { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() && !c.recoveryMode { return nil, consts.ErrSealed } @@ -133,7 +140,7 @@ func (c *Core) GenerateRootConfiguration() (*GenerateRootConfig, error) { } // GenerateRootInit is used to initialize the root generation settings -func (c *Core) GenerateRootInit(otp, pgpKey string, strategy GenerateRootStrategy) error { +func (c *Core) GenerateRootInit(otp, pgpKey string, strategy GenerateRootStrategy, grabLock bool) error { var fingerprint string switch { case len(otp) > 0: @@ -156,8 +163,10 @@ func (c *Core) GenerateRootInit(otp, pgpKey string, strategy GenerateRootStrateg return fmt.Errorf("otp or pgp_key parameter must be provided") } - c.stateLock.RLock() - defer c.stateLock.RUnlock() + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() && !c.recoveryMode { return consts.ErrSealed } @@ -209,7 +218,7 @@ func (c *Core) GenerateRootInit(otp, pgpKey string, strategy GenerateRootStrateg } // GenerateRootUpdate is used to provide a new key part -func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string, strategy GenerateRootStrategy) (*GenerateRootResult, error) { +func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string, strategy GenerateRootStrategy, grabLock bool) (*GenerateRootResult, error) { // Verify the key length min, max := c.barrier.KeyLength() max += shamir.ShareOverhead @@ -241,8 +250,10 @@ func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string, } // Ensure we are already unsealed - c.stateLock.RLock() - defer c.stateLock.RUnlock() + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() && !c.recoveryMode { return nil, consts.ErrSealed } @@ -362,9 +373,11 @@ func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string, } // GenerateRootCancel is used to cancel an in-progress root generation -func (c *Core) GenerateRootCancel() error { - c.stateLock.RLock() - defer c.stateLock.RUnlock() +func (c *Core) GenerateRootCancel(grabLock bool) error { + if grabLock { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + } if c.Sealed() && !c.recoveryMode { return consts.ErrSealed } @@ -380,3 +393,176 @@ func (c *Core) GenerateRootCancel() error { c.generateRootProgress = nil return nil } + +// GenerateRootStatusResponse is the response structure for generate root status +type GenerateRootStatusResponse struct { + Nonce string `json:"nonce"` + Started bool `json:"started"` + Progress int `json:"progress"` + Required int `json:"required"` + Complete bool `json:"complete"` + EncodedToken string `json:"encoded_token"` + EncodedRootToken string `json:"encoded_root_token"` + PGPFingerprint string `json:"pgp_fingerprint"` + OTP string `json:"otp"` + OTPLength int `json:"otp_length"` +} + +// GenerateRootInitRequest is the request structure for initializing generate root +type GenerateRootInitRequest struct { + OTP string `json:"otp"` + PGPKey string `json:"pgp_key"` +} + +// GenerateRootUpdateRequest is the request structure for updating generate root +type GenerateRootUpdateRequest struct { + Nonce string `json:"nonce"` + Key string `json:"key"` +} + +// HandleSysGenerateRootAttemptGet returns the status of a generate root attempt. +// In the event of an error, we return both the error and the HTTP status code to +// return that error with. +func HandleSysGenerateRootAttemptGet(ctx context.Context, core *Core, otp string, grabLock bool) (*GenerateRootStatusResponse, int, error) { + // Get the current seal configuration + 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") + } + + sealConfig := barrierConfig + if core.SealAccess().RecoveryKeySupported() { + sealConfig, err = core.SealAccess().RecoveryConfig(ctx) + if err != nil { + return nil, http.StatusInternalServerError, err + } + } + + // Get the generation configuration + generationConfig, err := core.GenerateRootConfiguration(grabLock) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + // Get the progress + progress, err := core.GenerateRootProgress(grabLock) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + var otpLength int + if core.DisableSSCTokens() { + otpLength = TokenLength + OldTokenPrefixLength + } else { + otpLength = TokenLength + TokenPrefixLength + } + + // Format the status + status := &GenerateRootStatusResponse{ + Started: false, + Progress: progress, + Required: sealConfig.SecretThreshold, + Complete: false, + OTPLength: otpLength, + OTP: otp, + } + if generationConfig != nil { + status.Nonce = generationConfig.Nonce + status.Started = true + status.PGPFingerprint = generationConfig.PGPFingerprint + } + + return status, 0, nil +} + +// HandleSysGenerateRootAttemptPut initializes a new generate root attempt. +// In the event of an error, we return both the error and the HTTP status code to +// return that error with. +func HandleSysGenerateRootAttemptPut(core *Core, strategy GenerateRootStrategy, req *GenerateRootInitRequest, grabLock bool) (string, int, error) { + var err error + var genned bool + otp := req.OTP + + switch { + case len(req.PGPKey) > 0, len(req.OTP) > 0: + default: + genned = true + if core.DisableSSCTokens() { + otp, err = base62.Random(TokenLength + OldTokenPrefixLength) + } else { + otp, err = base62.Random(TokenLength + TokenPrefixLength) + } + if err != nil { + return "", http.StatusInternalServerError, err + } + } + + // Initialize the generation + if err := core.GenerateRootInit(otp, req.PGPKey, strategy, grabLock); err != nil { + return "", http.StatusBadRequest, err + } + + if genned { + return otp, 0, nil + } + + return "", 0, nil +} + +// HandleSysGenerateRootAttemptDelete cancels any in-progress generate root attempt. +// In the event of an error, we return both the error and the HTTP status code to +// return that error with. +func HandleSysGenerateRootAttemptDelete(core *Core, grabLock bool) (int, error) { + err := core.GenerateRootCancel(grabLock) + if err != nil { + return http.StatusInternalServerError, err + } + return 0, nil +} + +// HandleSysGenerateRootUpdate processes a key share for generate root. +// In the event of an error, we return both the error and the HTTP status code to +// return that error with. +func HandleSysGenerateRootUpdate(ctx context.Context, core *Core, strategy GenerateRootStrategy, req *GenerateRootUpdateRequest, grabLock bool) (*GenerateRootStatusResponse, int, error) { + if req.Key == "" { + return nil, http.StatusBadRequest, fmt.Errorf("'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, fmt.Errorf("'key' must be a valid hex or base64 string") + } + } + + // Use the key to make progress on root generation + result, err := core.GenerateRootUpdate(ctx, key, req.Nonce, strategy, grabLock) + if err != nil { + return nil, http.StatusBadRequest, err + } + + resp := &GenerateRootStatusResponse{ + Complete: result.Progress == result.Required, + Nonce: req.Nonce, + Progress: result.Progress, + Required: result.Required, + Started: true, + EncodedToken: result.EncodedToken, + PGPFingerprint: result.PGPFingerprint, + } + + if strategy == GenerateStandardRootTokenStrategy { + resp.EncodedRootToken = result.EncodedToken + } + + return resp, 0, nil +} diff --git a/vault/generate_root_test.go b/vault/generate_root_test.go index da1c75a1ba..bc15bbd156 100644 --- a/vault/generate_root_test.go +++ b/vault/generate_root_test.go @@ -20,12 +20,12 @@ func TestCore_GenerateRoot_Lifecycle(t *testing.T) { func testCore_GenerateRoot_Lifecycle_Common(t *testing.T, c *Core, keys [][]byte) { // Verify update not allowed - if _, err := c.GenerateRootUpdate(namespace.RootContext(nil), keys[0], "", GenerateStandardRootTokenStrategy); err == nil { + if _, err := c.GenerateRootUpdate(namespace.RootContext(nil), keys[0], "", GenerateStandardRootTokenStrategy, true); err == nil { t.Fatalf("no root generation in progress") } // Should be no progress - num, err := c.GenerateRootProgress() + num, err := c.GenerateRootProgress(true) if err != nil { t.Fatalf("err: %v", err) } @@ -34,7 +34,7 @@ func testCore_GenerateRoot_Lifecycle_Common(t *testing.T, c *Core, keys [][]byte } // Should be no config - conf, err := c.GenerateRootConfiguration() + conf, err := c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } @@ -43,7 +43,7 @@ func testCore_GenerateRoot_Lifecycle_Common(t *testing.T, c *Core, keys [][]byte } // Cancel should be idempotent - err = c.GenerateRootCancel() + err = c.GenerateRootCancel(true) if err != nil { t.Fatalf("err: %v", err) } @@ -54,25 +54,25 @@ func testCore_GenerateRoot_Lifecycle_Common(t *testing.T, c *Core, keys [][]byte } // Start a root generation - err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy) + err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy, true) if err != nil { t.Fatalf("err: %v", err) } // Should get config - conf, err = c.GenerateRootConfiguration() + conf, err = c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } // Cancel should be clear - err = c.GenerateRootCancel() + err = c.GenerateRootCancel(true) if err != nil { t.Fatalf("err: %v", err) } // Should be no config - conf, err = c.GenerateRootConfiguration() + conf, err = c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } @@ -97,13 +97,13 @@ func testCore_GenerateRoot_Init_Common(t *testing.T, c *Core) { t.Fatal(err) } - err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy) + err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy, true) if err != nil { t.Fatalf("err: %v", err) } // Second should fail - err = c.GenerateRootInit("", pgpkeys.TestPubKey1, GenerateStandardRootTokenStrategy) + err = c.GenerateRootInit("", pgpkeys.TestPubKey1, GenerateStandardRootTokenStrategy, true) if err == nil { t.Fatalf("should fail") } @@ -122,13 +122,13 @@ func testCore_GenerateRoot_InvalidMasterNonce_Common(t *testing.T, c *Core, keys t.Fatal(err) } - err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy) + err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy, true) if err != nil { t.Fatalf("err: %v", err) } // Fetch new config with generated nonce - rgconf, err := c.GenerateRootConfiguration() + rgconf, err := c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } @@ -137,14 +137,14 @@ func testCore_GenerateRoot_InvalidMasterNonce_Common(t *testing.T, c *Core, keys } // Provide the nonce (invalid) - _, err = c.GenerateRootUpdate(namespace.RootContext(nil), keys[0], "abcd", GenerateStandardRootTokenStrategy) + _, err = c.GenerateRootUpdate(namespace.RootContext(nil), keys[0], "abcd", GenerateStandardRootTokenStrategy, true) if err == nil { t.Fatalf("expected error") } // Provide the master (invalid) for _, key := range keys { - _, err = c.GenerateRootUpdate(namespace.RootContext(nil), key, rgconf.Nonce, GenerateStandardRootTokenStrategy) + _, err = c.GenerateRootUpdate(namespace.RootContext(nil), key, rgconf.Nonce, GenerateStandardRootTokenStrategy, true) } if err == nil { t.Fatalf("expected error") @@ -163,13 +163,13 @@ func testCore_GenerateRoot_Update_OTP_Common(t *testing.T, c *Core, keys [][]byt } // Start a root generation - err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy) + err = c.GenerateRootInit(otp, "", GenerateStandardRootTokenStrategy, true) if err != nil { t.Fatal(err) } // Fetch new config with generated nonce - rkconf, err := c.GenerateRootConfiguration() + rkconf, err := c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } @@ -180,7 +180,7 @@ func testCore_GenerateRoot_Update_OTP_Common(t *testing.T, c *Core, keys [][]byt // Provide the keys var result *GenerateRootResult for _, key := range keys { - result, err = c.GenerateRootUpdate(namespace.RootContext(nil), key, rkconf.Nonce, GenerateStandardRootTokenStrategy) + result, err = c.GenerateRootUpdate(namespace.RootContext(nil), key, rkconf.Nonce, GenerateStandardRootTokenStrategy, true) if err != nil { t.Fatalf("err: %v", err) } @@ -195,7 +195,7 @@ func testCore_GenerateRoot_Update_OTP_Common(t *testing.T, c *Core, keys [][]byt encodedToken := result.EncodedToken // Should be no progress - num, err := c.GenerateRootProgress() + num, err := c.GenerateRootProgress(true) if err != nil { t.Fatalf("err: %v", err) } @@ -204,7 +204,7 @@ func testCore_GenerateRoot_Update_OTP_Common(t *testing.T, c *Core, keys [][]byt } // Should be no config - conf, err := c.GenerateRootConfiguration() + conf, err := c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } @@ -245,13 +245,13 @@ func TestCore_GenerateRoot_Update_PGP(t *testing.T) { func testCore_GenerateRoot_Update_PGP_Common(t *testing.T, c *Core, keys [][]byte) { // Start a root generation - err := c.GenerateRootInit("", pgpkeys.TestPubKey1, GenerateStandardRootTokenStrategy) + err := c.GenerateRootInit("", pgpkeys.TestPubKey1, GenerateStandardRootTokenStrategy, true) if err != nil { t.Fatalf("err: %v", err) } // Fetch new config with generated nonce - rkconf, err := c.GenerateRootConfiguration() + rkconf, err := c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } @@ -262,7 +262,7 @@ func testCore_GenerateRoot_Update_PGP_Common(t *testing.T, c *Core, keys [][]byt // Provide the keys var result *GenerateRootResult for _, key := range keys { - result, err = c.GenerateRootUpdate(namespace.RootContext(nil), key, rkconf.Nonce, GenerateStandardRootTokenStrategy) + result, err = c.GenerateRootUpdate(namespace.RootContext(nil), key, rkconf.Nonce, GenerateStandardRootTokenStrategy, true) if err != nil { t.Fatalf("err: %v", err) } @@ -277,7 +277,7 @@ func testCore_GenerateRoot_Update_PGP_Common(t *testing.T, c *Core, keys [][]byt encodedToken := result.EncodedToken // Should be no progress - num, err := c.GenerateRootProgress() + num, err := c.GenerateRootProgress(true) if err != nil { t.Fatalf("err: %v", err) } @@ -286,7 +286,7 @@ func testCore_GenerateRoot_Update_PGP_Common(t *testing.T, c *Core, keys [][]byt } // Should be no config - conf, err := c.GenerateRootConfiguration() + conf, err := c.GenerateRootConfiguration(true) if err != nil { t.Fatalf("err: %v", err) } diff --git a/vault/logical_system.go b/vault/logical_system.go index b165dba6fb..006febf4d3 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -109,8 +109,7 @@ 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). + // Build the unauthenticated paths list. unauthenticatedPaths := []string{ "wrapping/lookup", "wrapping/pubkey", @@ -140,16 +139,14 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf "unseal", "leader", "health", - "generate-root/attempt", - "generate-root/update", "decode-token", "mfa/validate", } - // Note that while rekeyPaths are not part of unauthenticatedPaths, that's + // Note that while rekeyPaths and generateRootPaths 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 + // only when Core.enableUnauthRekey or Core.enableUnauthGenerateRoot is true, and being more specific paths // than the v1/sys mux path they take precedence when enabled. rekeyPaths := []string{ "rekey/init", @@ -160,6 +157,11 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf "rekey-recovery-key/verify", } + generateRootPaths := []string{ + "generate-root/attempt", + "generate-root/update", + } + b.Backend = &framework.Backend{ RunningVersion: versions.DefaultBuiltinVersion, Help: strings.TrimSpace(sysHelpRoot), @@ -218,7 +220,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf managedKeyRegistrySubPath, }, - Binary: rekeyPaths, + Binary: append(append(rekeyPaths, generateRootPaths...), entBinaryPaths()...), }, } b.Backend.PathsSpecial.Unauthenticated = append(b.Backend.PathsSpecial.Unauthenticated, entUnauthenticatedPaths()...) @@ -1788,6 +1790,62 @@ func (b *SystemBackend) handleRekeyUpdateRecovery(ctx context.Context, req *logi return b.handleRekeyUpdate(ctx, true) } +// handleGenerateRootAttempt handles the generate-root/attempt endpoint +func (b *SystemBackend) handleGenerateRootAttempt(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + switch req.Operation { + case logical.ReadOperation: + return b.handleGenerateRootAttemptGet(ctx) + case logical.UpdateOperation: + return b.handleGenerateRootAttemptPut(ctx) + case logical.DeleteOperation: + return b.handleGenerateRootAttemptDelete(ctx) + default: + return nil, logical.ErrUnsupportedOperation + } +} + +func (b *SystemBackend) handleGenerateRootAttemptGet(ctx context.Context) (*logical.Response, error) { + status, code, err := HandleSysGenerateRootAttemptGet(ctx, b.Core, "", false) + return nonLogicalResponse(status, code, err) +} + +func (b *SystemBackend) handleGenerateRootAttemptPut(ctx context.Context) (*logical.Response, error) { + var req GenerateRootInitRequest + resp, err := getJSONBody(ctx, &req) + if err != nil { + return resp, err + } + + otp, code, err := HandleSysGenerateRootAttemptPut(b.Core, GenerateStandardRootTokenStrategy, &req, false) + if err != nil { + return nonLogicalError(code, err) + } + + // Return status with OTP if generated + status, code, err := HandleSysGenerateRootAttemptGet(ctx, b.Core, otp, false) + return nonLogicalResponse(status, code, err) +} + +func (b *SystemBackend) handleGenerateRootAttemptDelete(ctx context.Context) (*logical.Response, error) { + code, err := HandleSysGenerateRootAttemptDelete(b.Core, false) + if err != nil { + return nonLogicalError(code, err) + } + return nil, nil +} + +// handleGenerateRootUpdate handles the generate-root/update endpoint +func (b *SystemBackend) handleGenerateRootUpdate(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { + var updateReq GenerateRootUpdateRequest + resp, err := getJSONBody(ctx, &updateReq) + if err != nil { + return resp, err + } + + result, code, err := HandleSysGenerateRootUpdate(ctx, b.Core, GenerateStandardRootTokenStrategy, &updateReq, false) + return nonLogicalResponse(result, code, err) +} + // 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() diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index c4180883cc..193166c31c 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -295,6 +295,8 @@ func (b *SystemBackend) configPaths() []*framework.Path { Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleGenerateRootAttempt, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "read", OperationSuffix: "progress2|progress", @@ -349,7 +351,9 @@ func (b *SystemBackend) configPaths() []*framework.Path { }, }, logical.UpdateOperation: &framework.PathOperation{ - Summary: "Initializes a new root generation attempt.", + ForwardPerformanceStandby: true, + Callback: b.handleGenerateRootAttempt, + Summary: "Initializes a new root generation attempt.", DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "initialize", OperationSuffix: "2|", @@ -404,6 +408,8 @@ func (b *SystemBackend) configPaths() []*framework.Path { }, }, logical.DeleteOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleGenerateRootAttempt, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "cancel", OperationSuffix: "2|", @@ -434,6 +440,8 @@ func (b *SystemBackend) configPaths() []*framework.Path { }, Operations: map[logical.Operation]framework.OperationHandler{ logical.UpdateOperation: &framework.PathOperation{ + ForwardPerformanceStandby: true, + Callback: b.handleGenerateRootUpdate, DisplayAttrs: &framework.DisplayAttributes{ OperationPrefix: "root-token-generation", OperationVerb: "update", diff --git a/vault/logical_system_stubs_oss.go b/vault/logical_system_stubs_oss.go index 900816c092..04e2d7cba7 100644 --- a/vault/logical_system_stubs_oss.go +++ b/vault/logical_system_stubs_oss.go @@ -22,6 +22,10 @@ func entUnauthenticatedPaths() []string { return []string{} } +func entBinaryPaths() []string { + return []string{} +} + func (s *SystemBackend) entInit() {} func (s *SystemBackend) makeSnapshotSource(ctx context.Context, _ *framework.FieldData) (snapshots.Source, error) { diff --git a/vault/token_store.go b/vault/token_store.go index 9822cba54d..fbb1106f97 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -758,6 +758,16 @@ type TokenStore struct { func NewTokenStore(ctx context.Context, logger log.Logger, core *Core, config *logical.BackendConfig) (*TokenStore, error) { // Create a sub-view view := core.systemBarrierView.SubView(tokenSubPath) + if core.IsDRSecondary() { + // Generally speaking, DR secondaries do not handle requests using tokens + // from the TokenStore. Requests to DR secondaries should either be + // unauthenticated, or use a batch token, or use a DR operation token + // created using the generate-operation-token api. With the change to + // make generate-operation-token authenticated by default, we're now also + // allowing regular root tokens from the primary cluster to be used. + // + view.setReadOnlyErr(logical.ErrReadOnly) + } // Initialize the store t := &TokenStore{ @@ -1745,6 +1755,11 @@ func (ts *TokenStore) lookupInternal(ctx context.Context, id string, salted, tai return entry, nil } + if entry.IsRoot() { + // We don't need to check for persistence or expiration for root tokens. + return entry, nil + } + // Perform these checks on upgraded fields, but before persisting // If we are still restoring the expiration manager, we want to ensure the