Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fields 'ttl' and 'num_uses' to SecretID generation. #14474

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3e0e7a8
Add fields 'ttl' and 'num_uses' to SecretID generation.
RemcoBuddelmeijer Mar 12, 2022
8aeca35
Add secret_id_num_uses response field to generating SecretID
RemcoBuddelmeijer Mar 12, 2022
491e879
Add tests for new ttl and num_uses SecretID generation fields
RemcoBuddelmeijer Mar 12, 2022
44cfaab
Patch up test for ttl and num_uses fields
RemcoBuddelmeijer Mar 12, 2022
0f1c941
Add changelog entry for auth/approle 'ttl' and 'num_uses' fields
RemcoBuddelmeijer Mar 12, 2022
689e8c6
Add fields to API Docs and AppRole Auth Docs example
RemcoBuddelmeijer Mar 14, 2022
6b33aeb
Correct error message for failing test on missing field.
RemcoBuddelmeijer Apr 8, 2022
bd138f9
Remove unnecessary int cast to int "secret_id_num_uses" field.
RemcoBuddelmeijer Apr 8, 2022
00d8c0d
Move numUses field check to after assignment.
RemcoBuddelmeijer Apr 8, 2022
f72834e
Remove metadata entry in sample payload to limit change to changes made.
RemcoBuddelmeijer Apr 8, 2022
645e480
Bind fields 'ttl' and 'num_uses' to role's configuration.
RemcoBuddelmeijer May 31, 2022
0e6902a
Merge branch 'hashicorp:main' into feature/numuses-and-ttl-appsecret
RemcoBuddelmeijer May 31, 2022
772adde
Update changelog 14474 with a more detailed description.
RemcoBuddelmeijer May 31, 2022
59a7fed
Elaborate more on the bounds of the 'ttl' and 'num_uses' field.
RemcoBuddelmeijer May 31, 2022
5c2f6b5
Upper bound ttl with role secret id ttl
RemcoBuddelmeijer Jun 3, 2022
cba46ac
Formatting issues. Removed unnecessary newline
Jun 3, 2022
e4c9bfe
Update documentation for AppRole Secret ID and Role
RemcoBuddelmeijer Jun 3, 2022
d01dc76
Cleanup approle secret ID test and impl
RemcoBuddelmeijer Jun 3, 2022
ad0b5a5
Define ttl and num_uses in every test
RemcoBuddelmeijer Jun 3, 2022
364f714
Rename test RoleSecretID -> RoleSecretIDWithoutFields
RemcoBuddelmeijer Jun 3, 2022
376040b
Test secret id generation defaults to Role's config
RemcoBuddelmeijer Jun 23, 2022
dfc5b45
Change finit -> finite
RemcoBuddelmeijer Jun 23, 2022
ee219ec
Rephrase comments to the correct validation check
RemcoBuddelmeijer Jun 23, 2022
700e77d
Rephrase role-secret-id option description
RemcoBuddelmeijer Jun 30, 2022
5f34b69
Remove "default" incorrect statement about ttl
RemcoBuddelmeijer Jun 30, 2022
a9b4fac
Remove "default" incorrect statement about ttl for custom secret id
RemcoBuddelmeijer Jun 30, 2022
0f32504
Touch up approle.mdx to align more with path_role documentation
RemcoBuddelmeijer Jun 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions builtin/credential/approle/path_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,16 @@ the role.`,
Type: framework.TypeCommaStringSlice,
Description: defTokenFields["token_bound_cidrs"].Description,
},
"num_uses": {
Type: framework.TypeInt,
Description: `Number of times this SecretID can be used, after which the SecretID expires.
Overrides secret_id_num_uses role option when supplied.`,
},
"ttl": {
Type: framework.TypeDurationSecond,
Description: `Duration in seconds after which this SecretID expires. Defaults to 0, meaning no expiration.
Overrides secret_id_ttl role option when supplied.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathRoleSecretIDUpdate,
Expand Down Expand Up @@ -591,6 +601,16 @@ the role.`,
Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of
IP addresses which can use the returned token. Should be a subset of the token CIDR blocks listed on the role, if any.`,
},
"num_uses": {
Type: framework.TypeInt,
Description: `Number of times this SecretID can be used, after which the SecretID expires.
Overrides secret_id_num_uses role option when supplied.`,
},
"ttl": {
Type: framework.TypeDurationSecond,
Description: `Duration in seconds after which this SecretID expires. Defaults to 0, meaning no expiration.
Overrides secret_id_ttl role option when supplied.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathRoleCustomSecretIDUpdate,
Expand Down Expand Up @@ -1497,7 +1517,7 @@ func (b *backend) pathRoleFieldRead(ctx context.Context, req *logical.Request, d
"bound_cidr_list": role.BoundCIDRList,
},
}
resp.AddWarning(`The "bound_cidr_list" parameter is deprecated and will be removed. Please use "secret_id_bound_cidrs" instead.`)
resp.AddWarning(`The "bound_cidr_list" field is deprecated and will be removed. Please use "secret_id_bound_cidrs" instead.`)
return resp, nil
default:
// shouldn't occur IRL
Expand Down Expand Up @@ -2355,9 +2375,28 @@ func (b *backend) handleRoleSecretIDCommon(ctx context.Context, req *logical.Req
return nil, err
}

var numUses int
// Check whether or not num_uses is defined, otherwise fallback to secret_id_num_uses
if numUsesRaw, ok := data.GetOk("num_uses"); ok {
if numUses < 0 {
raskchanky marked this conversation as resolved.
Show resolved Hide resolved
return logical.ErrorResponse("num_uses cannot be negative"), nil
}
numUses = numUsesRaw.(int)
} else {
numUses = role.SecretIDNumUses
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
}

var ttl time.Duration
// Check whether or not ttl is defined, otherwise fallback to secret_id_ttl
if ttlRaw, ok := data.GetOk("ttl"); ok {
ttl = time.Second * time.Duration(ttlRaw.(int))
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
} else {
ttl = role.SecretIDTTL
}

secretIDStorage := &secretIDStorageEntry{
SecretIDNumUses: role.SecretIDNumUses,
SecretIDTTL: role.SecretIDTTL,
SecretIDNumUses: numUses,
SecretIDTTL: ttl,
Metadata: make(map[string]string),
CIDRList: secretIDCIDRs,
TokenBoundCIDRs: secretIDTokenCIDRs,
Expand All @@ -2376,6 +2415,7 @@ func (b *backend) handleRoleSecretIDCommon(ctx context.Context, req *logical.Req
"secret_id": secretID,
"secret_id_accessor": secretIDStorage.SecretIDAccessor,
"secret_id_ttl": int64(b.deriveSecretIDTTL(secretIDStorage.SecretIDTTL).Seconds()),
"secret_id_num_uses": int(secretIDStorage.SecretIDNumUses),
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
},
}

Expand Down Expand Up @@ -2476,11 +2516,11 @@ to be generated against only this specific role, it can be done via
'role/<role_name>/secret-id' and 'role/<role_name>/custom-secret-id' endpoints.
The properties of the SecretID created against the role and the properties
of the token issued with the SecretID generated against the role, can be
configured using the parameters of this endpoint.`,
configured using the fields of this endpoint.`,
},
"role-bind-secret-id": {
"Impose secret_id to be presented during login using this role.",
`By setting this to 'true', during login the parameter 'secret_id' becomes a mandatory argument.
`By setting this to 'true', during login the field 'secret_id' becomes a mandatory argument.
The value of 'secret_id' can be retrieved using 'role/<role_name>/secret-id' endpoint.`,
},
"role-bound-cidr-list": {
Expand Down Expand Up @@ -2514,14 +2554,15 @@ defined on the role, can access the role.`,
"Use limit of the SecretID generated against the role.",
`If the SecretIDs are generated/assigned against the role using the
'role/<role_name>/secret-id' or 'role/<role_name>/custom-secret-id' endpoints,
then the number of times that SecretID can access the role is defined by
this option.`,
then the number of times that SecretID can access the role is by default defined by this option.
Can be overriden by the 'num_uses' field on either of the two endpoints.`,
},
"role-secret-id-ttl": {
`Duration in seconds, representing the lifetime of the SecretIDs
that are generated against the role using 'role/<role_name>/secret-id' or
'role/<role_name>/custom-secret-id' endpoints.`,
``,
"Duration in seconds of the SecretID generated against the role.",
`If the SecretIDs are generated/assigned against the role using the
'role/<role_name>/secret-id' or 'role/<role_name>/custom-secret-id' endpoints,
then the lifetime of the SecretID is by default defined by this option.
Can be overriden by the 'ttl' field on either of the two endpoints.`,
},
"role-secret-id-lookup": {
"Read the properties of an issued secret_id",
Expand Down Expand Up @@ -2584,17 +2625,17 @@ this endpoint.`,
`The SecretID generated using this endpoint will be scoped to access
just this role and none else. The properties of this SecretID will be
based on the options set on the role. It will expire after a period
defined by the 'secret_id_ttl' option on the role and/or the backend
mount's maximum TTL value.`,
defined by the 'ttl' field or 'secret_id_ttl' option on the role,
and/or the backend mount's maximum TTL value.`,
},
"role-custom-secret-id": {
"Assign a SecretID of choice against the role.",
`This option is not recommended unless there is a specific need
to do so. This will assign a client supplied SecretID to be used to access
the role. This SecretID will behave similarly to the SecretIDs generated by
the backend. The properties of this SecretID will be based on the options
set on the role. It will expire after a period defined by the 'secret_id_ttl'
option on the role and/or the backend mount's maximum TTL value.`,
set on the role. It will expire after a period defined by the 'ttl' field
or 'secret_id_ttl' option on the role, and/or the backend mount's maximum TTL value.`,
},
"role-period": {
"Updates the value of 'period' on the role",
Expand Down
71 changes: 71 additions & 0 deletions builtin/credential/approle/path_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,77 @@ func TestAppRole_RoleSecretID(t *testing.T) {
}
}

func TestAppRole_RoleSecretIDWithFields(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)

roleData := map[string]interface{}{
"policies": "p,q,r,s",
"secret_id_num_uses": 0,
"secret_id_ttl": 0,
"token_ttl": 400,
"token_max_ttl": 500,
}
roleReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/role1",
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)
}

roleSecretIDReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/role1/secret-id",
Storage: storage,
}
roleCustomSecretIDData := map[string]interface{}{
"ttl": 5,
"num_uses": 5,
}
roleSecretIDReq.Data = roleCustomSecretIDData

resp, err = b.HandleRequest(context.Background(), roleSecretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}

if resp.Data["secret_id"].(string) == "" {
t.Fatalf("failed to generate secret_id")
}
if resp.Data["secret_id_ttl"].(int64) != 5 {
t.Fatalf("secret_id_ttl has not been set by the 'ttl' field")
}
if resp.Data["secret_id_num_uses"].(int) != 5 {
t.Fatalf("secret_id_ttl has not been set by the 'num_uses' field")
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
}

roleSecretIDReq.Path = "role/role1/custom-secret-id"
roleCustomSecretIDData["secret_id"] = "abcd123"

roleSecretIDReq.Data = roleCustomSecretIDData
roleSecretIDReq.Operation = logical.UpdateOperation
resp, err = b.HandleRequest(context.Background(), roleSecretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}

if resp.Data["secret_id"] != "abcd123" {
t.Fatalf("failed to set specific secret_id to role")
}
if resp.Data["secret_id_ttl"].(int64) == 0 {
t.Fatalf("secret_id_ttl has not been set by the 'ttl' field")
}
if resp.Data["secret_id_num_uses"].(int) != 5 {
t.Fatalf("secret_id_ttl has not been set by the 'num_uses' field")
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
}
}

func TestAppRole_RoleCRUD(t *testing.T) {
var resp *logical.Response
var err error
Expand Down
3 changes: 3 additions & 0 deletions changelog/14474.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
auth/approle: add 'ttl' and 'num_uses' fields when generating SecretIDs
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
```
36 changes: 29 additions & 7 deletions website/content/api-docs/auth/approle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ enabled while creating or updating a role.
blocks; if set, specifies blocks of IP addresses which can perform the login
operation.
- `secret_id_num_uses` `(integer: 0)` - Number of times any particular SecretID
can be used to fetch a token from this AppRole, after which the SecretID will
expire. A value of zero will allow unlimited uses.
can be used to fetch a token from this AppRole, after which the SecretID by default
will expire. A value of zero will allow unlimited uses.
Can be overriden by `num_uses` request field when generating a SecretID.
- `secret_id_ttl` `(string: "")` - Duration in either an integer number of
seconds (`3600`) or an integer time unit (`60m`) after which any SecretID
seconds (`3600`) or an integer time unit (`60m`) after which by default any SecretID
expires.
Can be overriden by `ttl` request field when generating a SecretID.
- `local_secret_ids` `(bool: false)` - If set, the secret IDs generated
using this role will be cluster local. This can only be set during role
creation and once set, it can't be reset later.
Expand Down Expand Up @@ -272,12 +274,20 @@ itself, and also to delete the SecretID from the AppRole.
- `token_bound_cidrs` `(array: [])` - Comma-separated string or list of CIDR
blocks; if set, specifies blocks of IP addresses which can use the auth tokens
generated by this SecretID. Overrides any role-set value but must be a subset.
- `num_uses` `(integer: 0)` - Number of times this SecretID can be used, after which
the SecretID expires. A value of zero will allow unlimited uses.
Overrides secret_id_num_uses role option when supplied.
- `ttl` `(string: "")` - Duration in seconds (`3600`) or an integer time unit (`60m`)
after which this SecretID expires. Defaults to 0, meaning no expiration.
Overrides secret_id_ttl role option when supplied.

### Sample Payload

```json
{
"metadata": "{ \"tag1\": \"production\" }"
"metadata": "{ \"tag1\": \"production\" }",
"ttl": 600,
"num_uses": 50
}
```

Expand All @@ -301,7 +311,8 @@ $ curl \
"data": {
"secret_id_accessor": "84896a0c-1347-aa90-a4f6-aca8b7558780",
"secret_id": "841771dc-11c9-bbc7-bcac-6a3945a69cd9",
"secret_id_ttl": 600
"secret_id_ttl": 600,
"secret_id_num_uses": 50
},
"lease_duration": 0,
"renewable": false,
Expand Down Expand Up @@ -501,12 +512,21 @@ Assigns a "custom" SecretID against an existing AppRole. This is used in the
- `token_bound_cidrs` `(array: [])` - Comma-separated string or list of CIDR
blocks; if set, specifies blocks of IP addresses which can use the auth tokens
generated by this SecretID. Overrides any role-set value but must be a subset.
- `num_uses` `(integer: 0)` - Number of times this SecretID can be used, after which
the SecretID expires. A value of zero will allow unlimited uses.
Overrides secret_id_num_uses role option when supplied.
- `ttl` `(string: "")` - Duration in seconds (`3600`) or an integer time unit (`60m`)
after which this SecretID expires. Defaults to 0, meaning no expiration.
Overrides secret_id_ttl role option when supplied.

### Sample Payload

```json
{
"secret_id": "testsecretid"
"secret_id": "testsecretid",
"metadata": "{ \"tag1\": \"production\" }",
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
RemcoBuddelmeijer marked this conversation as resolved.
Show resolved Hide resolved
"ttl": 600,
"num_uses": 50
}
```

Expand All @@ -528,8 +548,10 @@ $ curl \
"warnings": null,
"wrap_info": null,
"data": {
"secret_id": "testsecretid",
"secret_id_accessor": "84896a0c-1347-aa90-a4f6-aca8b7558780",
"secret_id": "testsecretid"
"secret_id_ttl": 600,
"secret_id_num_uses": 50
},
"lease_duration": 0,
"renewable": false,
Expand Down
18 changes: 10 additions & 8 deletions website/content/docs/auth/approle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ documentation.
secret_id 6a174c20-f6de-a53c-74d2-6018fcceff64
secret_id_accessor c454f7e5-996e-7230-6074-6ef26b7bcf86
secret_id_ttl 10m
secret_id_num_uses 40
```

### Via the API
Expand Down Expand Up @@ -175,7 +176,8 @@ documentation.
"data": {
"secret_id_accessor": "45946873-1d96-a9d4-678c-9229f74386a5",
"secret_id": "37b74931-c4cd-d49a-9246-ccc62d682a25",
"secret_id_ttl": 600
"secret_id_ttl": 600,
"secret_id_num_uses": 40
}
}
```
Expand Down Expand Up @@ -341,12 +343,12 @@ using VaultSharp.V1.AuthMethods.AppRole;
using VaultSharp.V1.AuthMethods.Token;
using VaultSharp.V1.Commons;

namespace Examples
namespace Examples
{
public class ApproleAuthExample
{
const string DefaultTokenPath = "../../../path/to/wrapping-token";

/// <summary>
/// Fetches a key-value secret (kv-v2) after authenticating to Vault via AppRole authentication
/// </summary>
Expand All @@ -356,7 +358,7 @@ namespace Examples
// The Secret ID is a value that needs to be protected, so instead of the app having knowledge of the secret ID directly,
// we have a trusted orchestrator (https://learn.hashicorp.com/tutorials/vault/secure-introduction?in=vault/app-integration#trusted-orchestrator)
// give the app access to a short-lived response-wrapping token (https://www.vaultproject.io/docs/concepts/response-wrapping).
// Read more at: https://learn.hashicorp.com/tutorials/vault/approle-best-practices?in=vault/auth-methods#secretid-delivery-best-practices
// Read more at: https://learn.hashicorp.com/tutorials/vault/approle-best-practices?in=vault/auth-methods#secretid-delivery-best-practices
var vaultAddr = Environment.GetEnvironmentVariable("VAULT_ADDR");
if(String.IsNullOrEmpty(vaultAddr))
{
Expand All @@ -382,9 +384,9 @@ namespace Examples
// We pass null here instead of the wrapping token to avoid depleting its single usage
// given that we already initialized our client with the wrapping token
Secret<Dictionary<string, object>> secretIdData = vaultClientForUnwrapping.V1.System
.UnwrapWrappedResponseDataAsync<Dictionary<string, object>>(null).Result;
.UnwrapWrappedResponseDataAsync<Dictionary<string, object>>(null).Result;

var secretId = secretIdData.Data["secret_id"]; // Grab the secret_id
var secretId = secretIdData.Data["secret_id"]; // Grab the secret_id

// We create a second VaultClient and initialize it with the AppRole auth method and our new credentials.
IAuthMethodInfo authMethod = new AppRoleAuthMethodInfo(roleId, secretId.ToString());
Expand All @@ -395,9 +397,9 @@ namespace Examples
// We can retrieve the secret from VaultClient
Secret<SecretData> kv2Secret = null;
kv2Secret = vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(path: "/creds").Result;

var password = kv2Secret.Data.Data["password"];

return password.ToString();
}
}
Expand Down