Skip to content
This repository was archived by the owner on Mar 15, 2024. It is now read-only.

Commit 22f6aa8

Browse files
authored
Merge pull request #20 from splunk/mw/ephemeral-creds
ephemeral credentials for any node type
2 parents 90dfaf8 + b51a75c commit 22f6aa8

File tree

8 files changed

+111
-23
lines changed

8 files changed

+111
-23
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ after the configured lease ends, in 5 minutes. We can use `vault
102102
lease [renew|revoke]` to manually alter the length of the lease, up to
103103
the configured maximum time.
104104

105+
For clustered stacks, we create ephemeral credentials for specific nodes:
106+
107+
$ vault read splunk/creds/local-admin/idx.example.com
108+
Key Value
109+
--- -----
110+
lease_id splunk/creds/local-admin/idx.example.com/u2N97uUVVDw3YVaETB1yRK74
111+
lease_duration 30s
112+
lease_renewable true
113+
connection local
114+
password &R1iX5W%$41QGcf^yN2i9%%#tUNf58h!
115+
roles [admin]
116+
url https://idx.example.com:8089
117+
username vault_29079642-4aa1-1979-f402-b3775f2713a7
118+
119+
105120
Rotate the Splunk admin password:
106121

107122
vault write -f splunk/rotate-root/local

backend_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,11 @@ func TestBackend_RoleCRUD(t *testing.T) {
8787
}
8888

8989
testRoleConfig := roleConfig{
90-
Connection: "testconn",
91-
Roles: []string{"admin"},
92-
UserPrefix: "my-custom-prefix",
90+
Connection: "testconn",
91+
Roles: []string{"admin"},
92+
AllowedNodeTypes: []string{"*"},
93+
PasswordSpec: DefaultPasswordSpec(),
94+
UserPrefix: "my-custom-prefix",
9395
}
9496

9597
logicaltest.Test(t, logicaltest.TestCase{

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect
1818
github.com/fatih/structs v1.1.0
1919
github.com/go-sql-driver/mysql v1.4.1 // indirect
20+
github.com/go-test/deep v1.0.5 // indirect
2021
github.com/golang/snappy v0.0.1 // indirect
2122
github.com/google/go-querystring v1.0.0
2223
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
@@ -48,6 +49,7 @@ require (
4849
github.com/pierrec/lz4 v2.2.6+incompatible // indirect
4950
github.com/prometheus/client_golang v1.1.0 // indirect
5051
github.com/ryanuber/go-glob v1.0.0 // indirect
52+
github.com/sethvargo/go-password v0.1.3
5153
github.com/sirupsen/logrus v1.4.2 // indirect
5254
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
5355
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
5151
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
5252
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
5353
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
54+
github.com/go-test/deep v1.0.5 h1:AKODKU3pDH1RzZzm6YZu77YWtEAq6uh1rLIAQlay2qc=
55+
github.com/go-test/deep v1.0.5/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
5456
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
5557
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
5658
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -185,6 +187,8 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
185187
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
186188
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
187189
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
190+
github.com/sethvargo/go-password v0.1.3 h1:18KkbGDkw8SuzeohAbWqBLNSfRQblVwEHOLbPa0PvWM=
191+
github.com/sethvargo/go-password v0.1.3/go.mod h1:2tyaaoHK/AlXwh5WWQDYjqQbHcq4cjPj5qb/ciYvu/Q=
188192
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
189193
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
190194
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=

password.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package splunk
2+
3+
import (
4+
"github.com/sethvargo/go-password/password"
5+
)
6+
7+
type PasswordSpec struct {
8+
Length int `json:"length" structs:"length"`
9+
NumDigits int `json:"num_digits" structs:"num_digits"`
10+
NumSymbols int `json:"num_symbols" structs:"num_symbols"`
11+
AllowUpper bool `json:"allow_upper" structs:"allow_upper"`
12+
AllowRepeat bool `json:"allow_repeat" structs:"allow_repeat"`
13+
}
14+
15+
func DefaultPasswordSpec() *PasswordSpec {
16+
return &PasswordSpec{
17+
Length: 32,
18+
NumDigits: 4,
19+
NumSymbols: 4,
20+
AllowUpper: true,
21+
AllowRepeat: true,
22+
}
23+
}
24+
25+
func GeneratePassword(spec *PasswordSpec) (string, error) {
26+
passwdgen, err := password.NewGenerator(&password.GeneratorInput{
27+
LowerLetters: password.LowerLetters,
28+
UpperLetters: password.UpperLetters,
29+
Digits: password.Digits,
30+
Symbols: "_&^%$#@!", // mostly shell-safe set, TE-101
31+
})
32+
if err != nil {
33+
return "", err
34+
}
35+
36+
if spec == nil {
37+
spec = DefaultPasswordSpec()
38+
}
39+
return passwdgen.Generate(spec.Length, spec.NumDigits, spec.NumSymbols, !spec.AllowUpper, spec.AllowRepeat)
40+
}

path_creds_create.go

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,15 @@ package splunk
33
import (
44
"context"
55
"fmt"
6+
67
"github.com/hashicorp/errwrap"
7-
"github.com/hashicorp/go-uuid"
8+
uuid "github.com/hashicorp/go-uuid"
89
"github.com/hashicorp/vault/helper/strutil"
910
"github.com/hashicorp/vault/logical"
1011
"github.com/hashicorp/vault/logical/framework"
1112
"github.com/splunk/vault-plugin-splunk/clients/splunk"
1213
)
1314

14-
const (
15-
SEARCHHEAD = "search_head"
16-
INDEXER = "indexer"
17-
)
18-
1915
func (b *backend) pathCredsCreate() *framework.Path {
2016
return &framework.Path{
2117
Pattern: "creds/" + framework.GenericNameRegex("name"),
@@ -84,7 +80,7 @@ func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.R
8480
}
8581

8682
// Generate credentials
87-
userUUID, err := uuid.GenerateUUID()
83+
userUUID, err := generateUserID(role)
8884
if err != nil {
8985
return nil, err
9086
}
@@ -93,7 +89,7 @@ func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.R
9389
userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName)
9490
}
9591
username := fmt.Sprintf("%s_%s", userPrefix, userUUID)
96-
passwd, err := uuid.GenerateUUID()
92+
passwd, err := generateUserPassword(role)
9793
if err != nil {
9894
return nil, errwrap.Wrapf("error generating new password {{err}}", err)
9995
}
@@ -128,20 +124,23 @@ func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.R
128124
return resp, nil
129125
}
130126

131-
func findNode(nodeFQDN string, hosts []splunk.ServerInfoEntry) (bool, error) {
127+
func findNode(nodeFQDN string, hosts []splunk.ServerInfoEntry, roleConfig *roleConfig) (bool, error) {
132128
for _, host := range hosts {
133129
// check if node_fqdn is in either of HostFQDN or Host. User might not always the FQDN on the cli input
134130
if host.Content.HostFQDN == nodeFQDN || host.Content.Host == nodeFQDN {
135-
// Return true if the requested node is a search head
131+
// Return true if the requested node type is allowed
132+
if strutil.StrListContains(roleConfig.AllowedNodeTypes, "*") {
133+
return true, nil
134+
}
136135
for _, role := range host.Content.Roles {
137-
if role == SEARCHHEAD {
136+
if strutil.StrListContainsGlob(roleConfig.AllowedNodeTypes, role) {
138137
return true, nil
139138
}
140139
}
141-
return false, fmt.Errorf("host: %s isn't search head; creating ephemeral creds is only supported for search heads", nodeFQDN)
140+
return false, fmt.Errorf("host %q does not have an allowed node type", nodeFQDN)
142141
}
143142
}
144-
return false, fmt.Errorf("host: %s not found", nodeFQDN)
143+
return false, fmt.Errorf("host %q not found", nodeFQDN)
145144
}
146145

147146
func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
@@ -180,7 +179,7 @@ func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Reques
180179
b.Logger().Error("Error while reading SearchPeers from cluster master", err)
181180
return nil, errwrap.Wrapf("unable to read searchpeers from cluster master: {{err}}", err)
182181
}
183-
_, err = findNode(nodeFQDN, nodes)
182+
_, err = findNode(nodeFQDN, nodes, role)
184183
if err != nil {
185184
return nil, err
186185
}
@@ -193,7 +192,7 @@ func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Reques
193192
return nil, err
194193
}
195194
// Generate credentials
196-
userUUID, err := uuid.GenerateUUID()
195+
userUUID, err := generateUserID(role)
197196
if err != nil {
198197
return nil, err
199198
}
@@ -202,11 +201,10 @@ func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Reques
202201
userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName)
203202
}
204203
username := fmt.Sprintf("%s_%s", userPrefix, userUUID)
205-
passwd, err := uuid.GenerateUUID()
204+
passwd, err := generateUserPassword(role)
206205
if err != nil {
207206
return nil, errwrap.Wrapf("error generating new password: {{err}}", err)
208207
}
209-
conn.Params().BaseURL = nodeFQDN
210208
opts := splunk.CreateUserOptions{
211209
Name: username,
212210
Password: passwd,
@@ -251,6 +249,19 @@ func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d
251249
return b.credsReadHandlerStandalone(ctx, req, d)
252250
}
253251

252+
func generateUserID(roleConfig *roleConfig) (string, error) {
253+
return uuid.GenerateUUID()
254+
}
255+
256+
func generateUserPassword(roleConfig *roleConfig) (string, error) {
257+
passwd, err := GeneratePassword(roleConfig.PasswordSpec)
258+
if err == nil {
259+
return passwd, nil
260+
}
261+
// fallback
262+
return uuid.GenerateUUID()
263+
}
264+
254265
const pathCredsCreateHelpSyn = `
255266
Request Splunk credentials for a certain role.
256267
`

path_roles.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ func (b *backend) pathRoles() *framework.Path {
3535
Type: framework.TypeCommaStringSlice,
3636
Description: "Comma-separated string or list of Splunk roles.",
3737
},
38+
"allowed_node_types": &framework.FieldSchema{
39+
Type: framework.TypeCommaStringSlice,
40+
Description: trimIndent(`
41+
Comma-separated string or array of node type (glob) patterns that are allowed
42+
to fetch credentials for. If empty, no nodes are allowed. If "*", all
43+
node types are allowed.`),
44+
Default: []string{"*"},
45+
},
3846
"default_app": &framework.FieldSchema{
3947
Type: framework.TypeString,
4048
Description: trimIndent(`
@@ -114,6 +122,10 @@ func (b *backend) rolesWriteHandler(ctx context.Context, req *logical.Request, d
114122
if maxTTLRaw, ok := getValue(data, req.Operation, "max_ttl"); ok {
115123
role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second
116124
}
125+
if allowed_node_types, ok := getValue(data, req.Operation, "allowed_node_types"); ok {
126+
role.AllowedNodeTypes = allowed_node_types.([]string)
127+
}
128+
role.PasswordSpec = DefaultPasswordSpec() // XXX make configurable
117129

118130
if roles, ok := getValue(data, req.Operation, "roles"); ok {
119131
role.Roles = roles.([]string)

role.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111
)
1212

1313
type roleConfig struct {
14-
Connection string `json:"connection" structs:"connection"`
15-
DefaultTTL time.Duration `json:"default_ttl" structs:"default_ttl"`
16-
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl"`
14+
Connection string `json:"connection" structs:"connection"`
15+
DefaultTTL time.Duration `json:"default_ttl" structs:"default_ttl"`
16+
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl"`
17+
AllowedNodeTypes []string `json:"allowed_node_types" structs:"allowed_node_types"`
18+
PasswordSpec *PasswordSpec `json:"password_spec" structs:"password_spec"`
1719

1820
// Splunk user attributes
1921
Roles []string `json:"roles" structs:"roles"`

0 commit comments

Comments
 (0)