Backport Make generate-root and generate-operation-token endpoints authenticated by default into release/2.x.x+ent into ce/release/2.x.x (#13705)

* Make generate-root and generate-operation-token endpoints authenticated by default (#12856)

Also allow root tokens to be used in DR requests.
This commit is contained in:
Vault Automation 2026-04-07 13:02:53 -06:00 committed by GitHub
parent 0b1bd68968
commit 2a34df24b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 910 additions and 543 deletions

6
changelog/_12856.txt Normal file
View File

@ -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.
```

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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")

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -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",

View File

@ -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) {

View File

@ -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