diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index 5474f7cb5..8e72defe4 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -564,8 +564,7 @@ func TestBackend_role_lease(t *testing.T) { }) } -func testAccStepConfig( - t *testing.T, config map[string]interface{}) logicaltest.TestStep { +func testAccStepConfig(t *testing.T, config map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config/access", @@ -573,8 +572,7 @@ func testAccStepConfig( } } -func testAccStepReadToken( - t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { +func testAccStepReadToken(t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: "creds/" + name, @@ -610,8 +608,7 @@ func testAccStepReadToken( } } -func testAccStepReadManagementToken( - t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { +func testAccStepReadManagementToken(t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: "creds/" + name, @@ -1082,6 +1079,239 @@ func testBackendEntPartition(t *testing.T) { } } +func TestBackendRenewRevokeRolesAndIdentities(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cleanup, consulConfig := consul.PrepareTestContainer(t, "", false, true) + defer cleanup() + + connData := map[string]interface{}{ + "address": consulConfig.Address(), + "token": consulConfig.Token, + } + + req := &logical.Request{ + Storage: config.StorageView, + Operation: logical.UpdateOperation, + Path: "config/access", + Data: connData, + } + resp, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + cases := map[string]struct { + RoleName string + RoleData map[string]interface{} + }{ + "just role": { + "r", + map[string]interface{}{ + "consul_roles": []string{"role-test"}, + "lease": "6h", + }, + }, + "role and policies": { + "rp", + map[string]interface{}{ + "policies": []string{"test"}, + "consul_roles": []string{"role-test"}, + "lease": "6h", + }, + }, + "service identity": { + "si", + map[string]interface{}{ + "service_identities": "service1", + "lease": "6h", + }, + }, + "service identity and policies": { + "sip", + map[string]interface{}{ + "policies": []string{"test"}, + "service_identities": "service1", + "lease": "6h", + }, + }, + "service identity and role": { + "sir", + map[string]interface{}{ + "consul_roles": []string{"role-test"}, + "service_identities": "service1", + "lease": "6h", + }, + }, + "service identity and role and policies": { + "sirp", + map[string]interface{}{ + "policies": []string{"test"}, + "consul_roles": []string{"role-test"}, + "service_identities": "service1", + "lease": "6h", + }, + }, + "node identity": { + "ni", + map[string]interface{}{ + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + "node identity and policies": { + "nip", + map[string]interface{}{ + "policies": []string{"test"}, + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + "node identity and role": { + "nir", + map[string]interface{}{ + "consul_roles": []string{"role-test"}, + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + "node identity and role and policies": { + "nirp", + map[string]interface{}{ + "consul_roles": []string{"role-test"}, + "service_identities": "service1", + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + "node identity and service identity": { + "nisi", + map[string]interface{}{ + "service_identities": "service1", + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + "node identity and service identity and policies": { + "nisip", + map[string]interface{}{ + "policies": []string{"test"}, + "service_identities": "service1", + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + "node identity and service identity and role": { + "nisir", + map[string]interface{}{ + "consul_roles": []string{"role-test"}, + "service_identities": "service1", + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + "node identity and service identity and role and policies": { + "nisirp", + map[string]interface{}{ + "policies": []string{"test"}, + "consul_roles": []string{"role-test"}, + "service_identities": "service1", + "node_identities": []string{"node1:dc1"}, + "lease": "6h", + }, + }, + } + + for description, tc := range cases { + t.Logf("Testing: %s", description) + + req.Operation = logical.UpdateOperation + req.Path = fmt.Sprintf("roles/%s", tc.RoleName) + req.Data = tc.RoleData + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.ReadOperation + req.Path = fmt.Sprintf("creds/%s", tc.RoleName) + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("resp nil") + } + if resp.IsError() { + t.Fatalf("resp is error: %v", resp.Error()) + } + + generatedSecret := resp.Secret + generatedSecret.TTL = 6 * time.Hour + + var d struct { + Token string `mapstructure:"token"` + Accessor string `mapstructure:"accessor"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + t.Fatal(err) + } + t.Logf("Generated token: %s with accessor %s", d.Token, d.Accessor) + + // Build a client and verify that the credentials work + consulapiConfig := consulapi.DefaultNonPooledConfig() + consulapiConfig.Address = connData["address"].(string) + consulapiConfig.Token = d.Token + client, err := consulapi.NewClient(consulapiConfig) + if err != nil { + t.Fatal(err) + } + + t.Log("Verifying that the generated token works...") + _, err = client.Catalog(), nil + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.RenewOperation + req.Secret = generatedSecret + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response from renew") + } + + req.Operation = logical.RevokeOperation + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + // Build a management client and verify that the token does not exist anymore + consulmgmtConfig := consulapi.DefaultNonPooledConfig() + consulmgmtConfig.Address = connData["address"].(string) + consulmgmtConfig.Token = connData["token"].(string) + mgmtclient, err := consulapi.NewClient(consulmgmtConfig) + + q := &consulapi.QueryOptions{ + Datacenter: "DC1", + } + + t.Log("Verifying that the generated token does not exist...") + _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) + if err == nil { + t.Fatal("err: expected error") + } + } +} + const testPolicy = ` key "" { policy = "write" diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index a73f674e6..967dd646c 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -89,6 +89,18 @@ created within. Defaults to 'default'. Available in Consul 1.7 and above.`, Description: `Indicates which admin partition that the token will be created within. Defaults to 'default'. Available in Consul 1.11 and above.`, }, + + "service_identities": { + Type: framework.TypeStringSlice, + Description: `List of Service Identities to attach to the +token, separated by semicolons. Available in Consul 1.5 or above.`, + }, + + "node_identities": { + Type: framework.TypeStringSlice, + Description: `List of Node Identities to attach to the +token. Available in Consul 1.8.1 or above.`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -149,6 +161,13 @@ func (b *backend) pathRolesRead(ctx context.Context, req *logical.Request, d *fr if len(roleConfigData.ConsulRoles) > 0 { resp.Data["consul_roles"] = roleConfigData.ConsulRoles } + if len(roleConfigData.ServiceIdentities) > 0 { + resp.Data["service_identities"] = roleConfigData.ServiceIdentities + } + if len(roleConfigData.NodeIdentities) > 0 { + resp.Data["node_identities"] = roleConfigData.NodeIdentities + } + return resp, nil } @@ -157,17 +176,19 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f policy := d.Get("policy").(string) policies := d.Get("policies").([]string) roles := d.Get("consul_roles").([]string) + serviceIdentities := d.Get("service_identities").([]string) + nodeIdentities := d.Get("node_identities").([]string) switch tokenType { case "client": - if policy == "" && len(policies) == 0 && len(roles) == 0 { + if policy == "" && len(policies) == 0 && len(roles) == 0 && + len(serviceIdentities) == 0 && len(nodeIdentities) == 0 { return logical.ErrorResponse( - "Use either a policy document, a list of policies, or a list of roles, depending on your Consul version"), nil + "Use either a policy document, a list of policies or roles, or a set of service or node identities, depending on your Consul version"), nil } case "management": default: - return logical.ErrorResponse( - "token_type must be \"client\" or \"management\""), nil + return logical.ErrorResponse("token_type must be \"client\" or \"management\""), nil } policyRaw, err := base64.StdEncoding.DecodeString(policy) @@ -198,15 +219,17 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f namespace := d.Get("consul_namespace").(string) partition := d.Get("partition").(string) entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{ - Policy: string(policyRaw), - Policies: policies, - ConsulRoles: roles, - TokenType: tokenType, - TTL: ttl, - MaxTTL: maxTTL, - Local: local, - ConsulNamespace: namespace, - Partition: partition, + Policy: string(policyRaw), + Policies: policies, + ConsulRoles: roles, + ServiceIdentities: serviceIdentities, + NodeIdentities: nodeIdentities, + TokenType: tokenType, + TTL: ttl, + MaxTTL: maxTTL, + Local: local, + ConsulNamespace: namespace, + Partition: partition, }) if err != nil { return nil, err @@ -228,13 +251,15 @@ func (b *backend) pathRolesDelete(ctx context.Context, req *logical.Request, d * } type roleConfig struct { - Policy string `json:"policy"` - Policies []string `json:"policies"` - ConsulRoles []string `json:"consul_roles"` - TTL time.Duration `json:"lease"` - MaxTTL time.Duration `json:"max_ttl"` - TokenType string `json:"token_type"` - Local bool `json:"local"` - ConsulNamespace string `json:"consul_namespace"` - Partition string `json:"partition"` + Policy string `json:"policy"` + Policies []string `json:"policies"` + ConsulRoles []string `json:"consul_roles"` + ServiceIdentities []string `json:"service_identities"` + NodeIdentities []string `json:"node_identities"` + TTL time.Duration `json:"lease"` + MaxTTL time.Duration `json:"max_ttl"` + TokenType string `json:"token_type"` + Local bool `json:"local"` + ConsulNamespace string `json:"consul_namespace"` + Partition string `json:"partition"` } diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go index f309fa35f..7568774f3 100644 --- a/builtin/logical/consul/path_token.go +++ b/builtin/logical/consul/path_token.go @@ -3,6 +3,7 @@ package consul import ( "context" "fmt" + "strings" "time" "github.com/hashicorp/consul/api" @@ -103,13 +104,18 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr }) } + aclServiceIdentities := parseServiceIdentities(roleConfigData.ServiceIdentities) + aclNodeIdentities := parseNodeIdentities(roleConfigData.NodeIdentities) + token, _, err := c.ACL().TokenCreate(&api.ACLToken{ - Description: tokenName, - Policies: policyLinks, - Roles: roleLinks, - Local: roleConfigData.Local, - Namespace: roleConfigData.ConsulNamespace, - Partition: roleConfigData.Partition, + Description: tokenName, + Policies: policyLinks, + Roles: roleLinks, + ServiceIdentities: aclServiceIdentities, + NodeIdentities: aclNodeIdentities, + Local: roleConfigData.Local, + Namespace: roleConfigData.ConsulNamespace, + Partition: roleConfigData.Partition, }, writeOpts) if err != nil { return logical.ErrorResponse(err.Error()), nil @@ -132,3 +138,35 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr return s, nil } + +func parseServiceIdentities(data []string) []*api.ACLServiceIdentity { + aclServiceIdentities := []*api.ACLServiceIdentity{} + + for _, serviceIdentity := range data { + entry := &api.ACLServiceIdentity{} + components := strings.Split(serviceIdentity, ":") + entry.ServiceName = components[0] + if len(components) == 2 { + entry.Datacenters = strings.Split(components[1], ",") + } + aclServiceIdentities = append(aclServiceIdentities, entry) + } + + return aclServiceIdentities +} + +func parseNodeIdentities(data []string) []*api.ACLNodeIdentity { + aclNodeIdentities := []*api.ACLNodeIdentity{} + + for _, nodeIdentity := range data { + entry := &api.ACLNodeIdentity{} + components := strings.Split(nodeIdentity, ":") + entry.NodeName = components[0] + if len(components) > 1 { + entry.Datacenter = components[1] + } + aclNodeIdentities = append(aclNodeIdentities, entry) + } + + return aclNodeIdentities +} diff --git a/builtin/logical/consul/path_token_test.go b/builtin/logical/consul/path_token_test.go new file mode 100644 index 000000000..e83ceee2d --- /dev/null +++ b/builtin/logical/consul/path_token_test.go @@ -0,0 +1,104 @@ +package consul + +import ( + "reflect" + "testing" + + "github.com/hashicorp/consul/api" +) + +func Test_parseServiceIdentities(t *testing.T) { + tests := []struct { + name string + args []string + want []*api.ACLServiceIdentity + }{ + { + name: "No datacenters", + args: []string{"myservice-1"}, + want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: nil}}, + }, + { + name: "One datacenter", + args: []string{"myservice-1:dc1"}, + want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: []string{"dc1"}}}, + }, + { + name: "Multiple datacenters", + args: []string{"myservice-1:dc1,dc2,dc3"}, + want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: []string{"dc1", "dc2", "dc3"}}}, + }, + { + name: "Missing service name with datacenter", + args: []string{":dc1"}, + want: []*api.ACLServiceIdentity{{ServiceName: "", Datacenters: []string{"dc1"}}}, + }, + { + name: "Missing service name and missing datacenter", + args: []string{""}, + want: []*api.ACLServiceIdentity{{ServiceName: "", Datacenters: nil}}, + }, + { + name: "Multiple service identities", + args: []string{"myservice-1:dc1", "myservice-2:dc1", "myservice-3:dc1,dc2"}, + want: []*api.ACLServiceIdentity{ + {ServiceName: "myservice-1", Datacenters: []string{"dc1"}}, + {ServiceName: "myservice-2", Datacenters: []string{"dc1"}}, + {ServiceName: "myservice-3", Datacenters: []string{"dc1", "dc2"}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseServiceIdentities(tt.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseServiceIdentities() = {%s:%v}, want {%s:%v}", got[0].ServiceName, got[0].Datacenters, tt.want[0].ServiceName, tt.want[0].Datacenters) + } + }) + } +} + +func Test_parseNodeIdentities(t *testing.T) { + tests := []struct { + name string + args []string + want []*api.ACLNodeIdentity + }{ + { + name: "No datacenter", + args: []string{"server-1"}, + want: []*api.ACLNodeIdentity{{NodeName: "server-1", Datacenter: ""}}, + }, + { + name: "One datacenter", + args: []string{"server-1:dc1"}, + want: []*api.ACLNodeIdentity{{NodeName: "server-1", Datacenter: "dc1"}}, + }, + { + name: "Missing node name with datacenter", + args: []string{":dc1"}, + want: []*api.ACLNodeIdentity{{NodeName: "", Datacenter: "dc1"}}, + }, + { + name: "Missing node name and missing datacenter", + args: []string{""}, + want: []*api.ACLNodeIdentity{{NodeName: "", Datacenter: ""}}, + }, + { + name: "Multiple node identities", + args: []string{"server-1:dc1", "server-2:dc1", "server-3:dc1"}, + want: []*api.ACLNodeIdentity{ + {NodeName: "server-1", Datacenter: "dc1"}, + {NodeName: "server-2", Datacenter: "dc1"}, + {NodeName: "server-3", Datacenter: "dc1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseNodeIdentities(tt.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseNodeIdentities() = {%s:%s}, want {%s:%s}", got[0].NodeName, got[0].Datacenter, tt.want[0].NodeName, tt.want[0].Datacenter) + } + }) + } +} diff --git a/changelog/15295.txt b/changelog/15295.txt new file mode 100644 index 000000000..0fbf4b5ed --- /dev/null +++ b/changelog/15295.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/consul: Add support for Consul node-identities and service-identities +``` \ No newline at end of file diff --git a/helper/testhelpers/consul/consulhelper.go b/helper/testhelpers/consul/consulhelper.go index e88149079..3facba58f 100644 --- a/helper/testhelpers/consul/consulhelper.go +++ b/helper/testhelpers/consul/consulhelper.go @@ -148,7 +148,7 @@ func PrepareTestContainer(t *testing.T, version string, isEnterprise bool, boots currVersion, _ := goversion.NewVersion(version) roleVersion, _ := goversion.NewVersion("1.5") if currVersion.GreaterThanOrEqual(roleVersion) { - ACLList := []*consulapi.ACLLink{{Name: "test"}} + ACLList := []*consulapi.ACLTokenRoleLink{{Name: "test"}} role := &consulapi.ACLRole{ Name: "role-test", diff --git a/website/content/api-docs/secret/consul.mdx b/website/content/api-docs/secret/consul.mdx index eacc7e3f7..56a41926a 100644 --- a/website/content/api-docs/secret/consul.mdx +++ b/website/content/api-docs/secret/consul.mdx @@ -74,13 +74,6 @@ updated attributes. | :----- | :-------------------- | | `POST` | `/consul/roles/:name` | -### Parameters for Consul versions 1.7 and above - -- `consul_namespace` `(string: "")` - Specifies the Consul namespace the token - will be generated within. The namespace must exist, and the policies or roles assigned to the - Vault role must also exist inside the given Consul namespace. If not provided, the "default" - namespace is used. - ### Parameters for Consul versions 1.11 and above - `partition` `(string: "")` - Specifies the Consul admin partition the token @@ -88,14 +81,6 @@ updated attributes. Vault role must also exist inside the given partition. If not provided, the "default" partition is used. -To create a client token within a particular Consul namespace: - -```json -{ - "consul_namespace": "ns1" -} -``` - To create a client token within a particular Consul admin partition: ```json @@ -104,7 +89,34 @@ To create a client token within a particular Consul admin partition: } ``` -### Parameters for Consul versions 1.4 and above +### Parameters for Consul versions 1.8 and above + +- `consul_namespace` `(string: "")` - Specifies the Consul namespace the token + will be generated within. The namespace must exist, and the policies or roles assigned to the + Vault role must also exist inside the given Consul namespace. If not provided, the "default" + namespace is used. + +- `node_identities` `(list: )` - The list of node identities to +assign to the generated token. This may be a comma-separated list to attach multiple node identities +to a token. + +To create a client token within a particular Consul namespace: + +```json +{ + "consul_namespace": "ns1" +} +``` + +To create a client token with node identities attached: + +```json +{ + "node_identities": "client-1:dc1,client-2:dc1" +} +``` + +### Parameters for Consul versions 1.5 and above - `name` `(string: )` – Specifies the name of an existing role against which to create this Consul credential. This is part of the request URL. @@ -125,6 +137,10 @@ To create a client token within a particular Consul admin partition: - `consul_roles` `(list: )` – The list of Consul roles to assign to the generated token. Either `policies` or `consul_roles` are required for Consul 1.5 and above. +- `service_identities` `(list: )` - The list of + service identities to assign to the generated token. This may be a semicolon-separated list to + attach multiple service identities to a token. + - `local` `(bool: false)` - Indicates that the token should not be replicated globally and instead be local to the current datacenter. Only available in Consul 1.4 and greater. @@ -163,6 +179,14 @@ To create a client token with defined roles: } ``` +To create a client token with service identities attached: + +```json +{ + "service_identities": "myservice-1:dc1,dc2;myservice-2:dc1" +} +``` + ### Sample Request ```shell-session diff --git a/website/content/docs/secrets/consul.mdx b/website/content/docs/secrets/consul.mdx index 738b7ea27..f7e41cca1 100644 --- a/website/content/docs/secrets/consul.mdx +++ b/website/content/docs/secrets/consul.mdx @@ -38,39 +38,39 @@ management tool. If you have already bootstrapped the ACL system of your Consul cluster, you will need to give Vault a management token: - - In Consul versions below 1.4, acquire a [management token][consul-mgmt-token] from Consul, using the - `acl_master_token` from your Consul configuration file or another management - token: + In Consul versions below 1.4, acquire a [management token][consul-mgmt-token] from Consul, using the + `acl_master_token` from your Consul configuration file or another management + token: - ```sh - $ curl \ - --header "X-Consul-Token: my-management-token" \ - --request PUT \ - --data '{"Name": "sample", "Type": "management"}' \ - https://consul.rocks/v1/acl/create - ``` + ```sh + $ curl \ + --header "X-Consul-Token: my-management-token" \ + --request PUT \ + --data '{"Name": "sample", "Type": "management"}' \ + https://consul.rocks/v1/acl/create + ``` - Vault must have a management type token so that it can create and revoke ACL - tokens. The response will return a new token: + Vault must have a management type token so that it can create and revoke ACL + tokens. The response will return a new token: - ```json - { - "ID": "7652ba4c-0f6e-8e75-5724-5e083d72cfe4" - } - ``` + ```json + { + "ID": "7652ba4c-0f6e-8e75-5724-5e083d72cfe4" + } + ``` - - For Consul 1.4 and above, use the command line to generate a token with the appropriate policy: + For Consul 1.4 and above, use the command line to generate a token with the appropriate policy: - ```shell-session - $ CONSUL_HTTP_TOKEN="" consul acl token create -policy-name="global-management" - AccessorID: 865dc5e9-e585-3180-7b49-4ddc0fc45135 - SecretID: ef35f0f1-885b-0cab-573c-7c91b65a7a7e - Description: - Local: false - Create Time: 2018-10-22 17:40:24.128188 -0700 PDT - Policies: - 00000000-0000-0000-0000-000000000001 - global-management - ``` + ```shell-session + $ CONSUL_HTTP_TOKEN="" consul acl token create -policy-name="global-management" + AccessorID: 865dc5e9-e585-3180-7b49-4ddc0fc45135 + SecretID: ef35f0f1-885b-0cab-573c-7c91b65a7a7e + Description: + Local: false + Create Time: 2018-10-22 17:40:24.128188 -0700 PDT + Policies: + 00000000-0000-0000-0000-000000000001 - global-management + ``` 1. Configure Vault to connect and authenticate to Consul: @@ -82,7 +82,8 @@ management tool. ``` 1. Configure a role that maps a name in Vault to a Consul ACL policy. Depending on your Consul version, - you will either provide a policy document and a token_type, or a set of policies. + you will either provide a policy document and a token_type, a list of policies or roles, or a set of + service or node identities. When users generate credentials, they are generated against this role. For Consul versions below 1.4, the policy must be base64-encoded. The policy language is @@ -103,14 +104,26 @@ management tool. Success! Data written to: consul/roles/my-role ``` - For Consul versions 1.5 and above, [generate a role in Consul](https://www.consul.io/api/acl/roles), and - proceed to link it to the role: + For Consul versions 1.5 and above, [generate a role in Consul](https://www.consul.io/api/acl/roles) and + proceed to link it to the role, or [attach a Consul service identity](https://www.consul.io/commands/acl/token/create#service-identity) to the role: ```shell-session $ vault write consul/roles/my-role consul_roles="api-server" Success! Data written to: consul/roles/my-role ``` + ```shell-session + $ vault write consul/roles/my-role service_identities="myservice:dc1,dc2" + Success! Data written to: consul/roles/my-role + ``` + + For Consul versions 1.8 and above, [attach a Consul node identity](https://www.consul.io/commands/acl/token/create#node-identity) to the role. + + ```shell-session + $ vault write consul/roles/my-role node_identities="server-1:dc1" + Success! Data written to: consul/roles/my-role + ``` + -> **Token lease duration:** If you do not specify a value for `ttl` (or `lease` for Consul versions below 1.4) the tokens created using Vault's Consul secrets engine are created with a Time To Live (TTL) of 30 days. You can change the lease duration by passing `-ttl=` to the command above with "duration" being a string with a time