diff --git a/builtin/credential/approle/path_role.go b/builtin/credential/approle/path_role.go index f98f8b6adb..75238102be 100644 --- a/builtin/credential/approle/path_role.go +++ b/builtin/credential/approle/path_role.go @@ -2358,12 +2358,15 @@ func (b *backend) handleRoleSecretIDCommon(ctx context.Context, req *logical.Req return nil, errwrap.Wrapf("failed to store secret_id: {{err}}", err) } - return &logical.Response{ + resp := &logical.Response{ Data: map[string]interface{}{ "secret_id": secretID, "secret_id_accessor": secretIDStorage.SecretIDAccessor, + "secret_id_ttl": int64(b.deriveSecretIDTTL(secretIDStorage.SecretIDTTL).Seconds()), }, - }, nil + } + + return resp, nil } func (b *backend) roleIDLock(roleID string) *locksutil.LockEntry { diff --git a/builtin/credential/approle/path_role_test.go b/builtin/credential/approle/path_role_test.go index 2e70c2400c..5cc0bfb1fe 100644 --- a/builtin/credential/approle/path_role_test.go +++ b/builtin/credential/approle/path_role_test.go @@ -1931,3 +1931,92 @@ func TestAppRole_TokenutilUpgrade(t *testing.T) { }) } } + +func TestAppRole_SecretID_WithTTL(t *testing.T) { + tests := []struct { + name string + roleName string + ttl int64 + sysTTLCap bool + }{ + { + "zero ttl", + "role-zero-ttl", + 0, + false, + }, + { + "custom ttl", + "role-custom-ttl", + 60, + false, + }, + { + "system ttl capped", + "role-sys-ttl-cap", + 700000000, + true, + }, + } + + b, storage := createBackendWithStorage(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create role + roleData := map[string]interface{}{ + "policies": "default", + "secret_id_ttl": tt.ttl, + } + + roleReq := &logical.Request{ + Operation: logical.CreateOperation, + Path: "role/" + tt.roleName, + Storage: storage, + Data: roleData, + } + resp, err := b.HandleRequest(context.Background(), roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + // Generate secret ID + secretIDReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "role/" + tt.roleName + "/secret-id", + Storage: storage, + } + resp, err = b.HandleRequest(context.Background(), secretIDReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + // Extract the "ttl" value from the response data if it exists + ttlRaw, okTTL := resp.Data["secret_id_ttl"] + if !okTTL { + t.Fatalf("expected TTL value in response") + } + + var ( + respTTL int64 + ok bool + ) + respTTL, ok = ttlRaw.(int64) + if !ok { + t.Fatalf("expected ttl to be an integer, got: %s", err) + } + + // Verify secret ID response for different cases + switch { + case tt.sysTTLCap: + if respTTL != int64(b.System().MaxLeaseTTL().Seconds()) { + t.Fatalf("expected TTL value to be system's max lease TTL, got: %d", respTTL) + } + default: + if respTTL != tt.ttl { + t.Fatalf("expected TTL value to be %d, got: %d", tt.ttl, respTTL) + } + } + }) + } +} diff --git a/builtin/credential/approle/validation.go b/builtin/credential/approle/validation.go index 96357d62fd..8936d15493 100644 --- a/builtin/credential/approle/validation.go +++ b/builtin/credential/approle/validation.go @@ -238,15 +238,8 @@ func (b *backend) registerSecretIDEntry(ctx context.Context, s logical.Storage, secretEntry.CreationTime = currentTime secretEntry.LastUpdatedTime = currentTime - // If SecretIDTTL is not specified or if it crosses the backend mount's limit, - // cap the expiration to backend's max. Otherwise, use it to determine the - // expiration time. - if secretEntry.SecretIDTTL < time.Duration(0) || secretEntry.SecretIDTTL > b.System().MaxLeaseTTL() { - secretEntry.ExpirationTime = currentTime.Add(b.System().MaxLeaseTTL()) - } else if secretEntry.SecretIDTTL != time.Duration(0) { - // Set the ExpirationTime only if SecretIDTTL was set. SecretIDs should not - // expire by default. - secretEntry.ExpirationTime = currentTime.Add(secretEntry.SecretIDTTL) + if ttl := b.deriveSecretIDTTL(secretEntry.SecretIDTTL); ttl != time.Duration(0) { + secretEntry.ExpirationTime = currentTime.Add(ttl) } // Before storing the SecretID, store its accessor. @@ -261,6 +254,20 @@ func (b *backend) registerSecretIDEntry(ctx context.Context, s logical.Storage, return secretEntry, nil } +// deriveSecretIDTTL determines the secret ID TTL to use based on the system's +// max lease TTL. +// +// If SecretIDTTL is negative or if it crosses the backend mount's limit, +// return to backend's max lease TTL. Otherwise, return the provided secretIDTTL +// value. +func (b *backend) deriveSecretIDTTL(secretIDTTL time.Duration) time.Duration { + if secretIDTTL < time.Duration(0) || secretIDTTL > b.System().MaxLeaseTTL() { + return b.System().MaxLeaseTTL() + } + + return secretIDTTL +} + // secretIDAccessorEntry is used to read the storage entry that maps an // accessor to a secret_id. func (b *backend) secretIDAccessorEntry(ctx context.Context, s logical.Storage, secretIDAccessor, roleSecretIDPrefix string) (*secretIDAccessorStorageEntry, error) { diff --git a/changelog/10826.txt b/changelog/10826.txt new file mode 100644 index 0000000000..8fb719fd2c --- /dev/null +++ b/changelog/10826.txt @@ -0,0 +1,3 @@ +```changelog:changes +auth/approle: Secrets ID generation endpoint now returns `secret_id_ttl` as part of its response. +``` diff --git a/website/content/api-docs/auth/approle.mdx b/website/content/api-docs/auth/approle.mdx index 4810a15652..24cf8fd465 100644 --- a/website/content/api-docs/auth/approle.mdx +++ b/website/content/api-docs/auth/approle.mdx @@ -301,7 +301,8 @@ $ curl \ "wrap_info": null, "data": { "secret_id_accessor": "84896a0c-1347-aa90-a4f6-aca8b7558780", - "secret_id": "841771dc-11c9-bbc7-bcac-6a3945a69cd9" + "secret_id": "841771dc-11c9-bbc7-bcac-6a3945a69cd9", + "secret_id_ttl": 600 }, "lease_duration": 0, "renewable": false, diff --git a/website/content/docs/auth/approle.mdx b/website/content/docs/auth/approle.mdx index 0fc17e2092..9ccc5af56a 100644 --- a/website/content/docs/auth/approle.mdx +++ b/website/content/docs/auth/approle.mdx @@ -113,6 +113,7 @@ documentation. $ vault write -f auth/approle/role/my-role/secret-id secret_id 6a174c20-f6de-a53c-74d2-6018fcceff64 secret_id_accessor c454f7e5-996e-7230-6074-6ef26b7bcf86 + secret_id_ttl 10m ``` ### Via the API @@ -170,7 +171,8 @@ documentation. { "data": { "secret_id_accessor": "45946873-1d96-a9d4-678c-9229f74386a5", - "secret_id": "37b74931-c4cd-d49a-9246-ccc62d682a25" + "secret_id": "37b74931-c4cd-d49a-9246-ccc62d682a25", + "secret_id_ttl": 600 } } ```