Skip to content
This repository was archived by the owner on Mar 15, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ after the configured lease ends, in 5 minutes. We can use `vault
lease [renew|revoke]` to manually alter the length of the lease, up to
the configured maximum time.

For clustered stacks, we create ephemeral credentials for specific nodes:

$ vault read splunk/creds/local-admin/idx.example.com
Key Value
--- -----
lease_id splunk/creds/local-admin/idx.example.com/u2N97uUVVDw3YVaETB1yRK74
lease_duration 30s
lease_renewable true
connection local
password &R1iX5W%$41QGcf^yN2i9%%#tUNf58h!
roles [admin]
url https://idx.example.com:8089
username vault_29079642-4aa1-1979-f402-b3775f2713a7


Rotate the Splunk admin password:

vault write -f splunk/rotate-root/local
Expand Down
8 changes: 5 additions & 3 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ func TestBackend_RoleCRUD(t *testing.T) {
}

testRoleConfig := roleConfig{
Connection: "testconn",
Roles: []string{"admin"},
UserPrefix: "my-custom-prefix",
Connection: "testconn",
Roles: []string{"admin"},
AllowedNodeTypes: []string{"*"},
PasswordSpec: DefaultPasswordSpec(),
UserPrefix: "my-custom-prefix",
}

logicaltest.Test(t, logicaltest.TestCase{
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect
github.com/fatih/structs v1.1.0
github.com/go-sql-driver/mysql v1.4.1 // indirect
github.com/go-test/deep v1.0.5 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-querystring v1.0.0
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
Expand Down Expand Up @@ -48,6 +49,7 @@ require (
github.com/pierrec/lz4 v2.2.6+incompatible // indirect
github.com/prometheus/client_golang v1.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sethvargo/go-password v0.1.3
github.com/sirupsen/logrus v1.4.2 // indirect
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.5 h1:AKODKU3pDH1RzZzm6YZu77YWtEAq6uh1rLIAQlay2qc=
github.com/go-test/deep v1.0.5/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
Expand Down Expand Up @@ -185,6 +187,8 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sethvargo/go-password v0.1.3 h1:18KkbGDkw8SuzeohAbWqBLNSfRQblVwEHOLbPa0PvWM=
github.com/sethvargo/go-password v0.1.3/go.mod h1:2tyaaoHK/AlXwh5WWQDYjqQbHcq4cjPj5qb/ciYvu/Q=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
Expand Down
40 changes: 40 additions & 0 deletions password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package splunk

import (
"github.com/sethvargo/go-password/password"
)

type PasswordSpec struct {
Length int `json:"length" structs:"length"`
NumDigits int `json:"num_digits" structs:"num_digits"`
NumSymbols int `json:"num_symbols" structs:"num_symbols"`
AllowUpper bool `json:"allow_upper" structs:"allow_upper"`
AllowRepeat bool `json:"allow_repeat" structs:"allow_repeat"`
}

func DefaultPasswordSpec() *PasswordSpec {
return &PasswordSpec{
Length: 32,
NumDigits: 4,
NumSymbols: 4,
AllowUpper: true,
AllowRepeat: true,
}
}

func GeneratePassword(spec *PasswordSpec) (string, error) {
passwdgen, err := password.NewGenerator(&password.GeneratorInput{
LowerLetters: password.LowerLetters,
UpperLetters: password.UpperLetters,
Digits: password.Digits,
Symbols: "_&^%$#@!", // mostly shell-safe set, TE-101
})
if err != nil {
return "", err
}

if spec == nil {
spec = DefaultPasswordSpec()
}
return passwdgen.Generate(spec.Length, spec.NumDigits, spec.NumSymbols, !spec.AllowUpper, spec.AllowRepeat)
}
45 changes: 28 additions & 17 deletions path_creds_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ package splunk
import (
"context"
"fmt"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-uuid"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
"github.com/splunk/vault-plugin-splunk/clients/splunk"
)

const (
SEARCHHEAD = "search_head"
INDEXER = "indexer"
)

func (b *backend) pathCredsCreate() *framework.Path {
return &framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name"),
Expand Down Expand Up @@ -84,7 +80,7 @@ func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.R
}

// Generate credentials
userUUID, err := uuid.GenerateUUID()
userUUID, err := generateUserID(role)
if err != nil {
return nil, err
}
Expand All @@ -93,7 +89,7 @@ func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.R
userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName)
}
username := fmt.Sprintf("%s_%s", userPrefix, userUUID)
passwd, err := uuid.GenerateUUID()
passwd, err := generateUserPassword(role)
if err != nil {
return nil, errwrap.Wrapf("error generating new password {{err}}", err)
}
Expand Down Expand Up @@ -128,20 +124,23 @@ func (b *backend) credsReadHandlerStandalone(ctx context.Context, req *logical.R
return resp, nil
}

func findNode(nodeFQDN string, hosts []splunk.ServerInfoEntry) (bool, error) {
func findNode(nodeFQDN string, hosts []splunk.ServerInfoEntry, roleConfig *roleConfig) (bool, error) {
for _, host := range hosts {
// check if node_fqdn is in either of HostFQDN or Host. User might not always the FQDN on the cli input
if host.Content.HostFQDN == nodeFQDN || host.Content.Host == nodeFQDN {
// Return true if the requested node is a search head
// Return true if the requested node type is allowed
if strutil.StrListContains(roleConfig.AllowedNodeTypes, "*") {
return true, nil
}
for _, role := range host.Content.Roles {
if role == SEARCHHEAD {
if strutil.StrListContainsGlob(roleConfig.AllowedNodeTypes, role) {
return true, nil
}
}
return false, fmt.Errorf("host: %s isn't search head; creating ephemeral creds is only supported for search heads", nodeFQDN)
return false, fmt.Errorf("host %q does not have an allowed node type", nodeFQDN)
}
}
return false, fmt.Errorf("host: %s not found", nodeFQDN)
return false, fmt.Errorf("host %q not found", nodeFQDN)
}

func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
Expand Down Expand Up @@ -180,7 +179,7 @@ func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Reques
b.Logger().Error("Error while reading SearchPeers from cluster master", err)
return nil, errwrap.Wrapf("unable to read searchpeers from cluster master: {{err}}", err)
}
_, err = findNode(nodeFQDN, nodes)
_, err = findNode(nodeFQDN, nodes, role)
if err != nil {
return nil, err
}
Expand All @@ -193,7 +192,7 @@ func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Reques
return nil, err
}
// Generate credentials
userUUID, err := uuid.GenerateUUID()
userUUID, err := generateUserID(role)
if err != nil {
return nil, err
}
Expand All @@ -202,11 +201,10 @@ func (b *backend) credsReadHandlerMulti(ctx context.Context, req *logical.Reques
userPrefix = fmt.Sprintf("%s_%s", role.UserPrefix, req.DisplayName)
}
username := fmt.Sprintf("%s_%s", userPrefix, userUUID)
passwd, err := uuid.GenerateUUID()
passwd, err := generateUserPassword(role)
if err != nil {
return nil, errwrap.Wrapf("error generating new password: {{err}}", err)
}
conn.Params().BaseURL = nodeFQDN
opts := splunk.CreateUserOptions{
Name: username,
Password: passwd,
Expand Down Expand Up @@ -251,6 +249,19 @@ func (b *backend) credsReadHandler(ctx context.Context, req *logical.Request, d
return b.credsReadHandlerStandalone(ctx, req, d)
}

func generateUserID(roleConfig *roleConfig) (string, error) {
return uuid.GenerateUUID()
}

func generateUserPassword(roleConfig *roleConfig) (string, error) {
passwd, err := GeneratePassword(roleConfig.PasswordSpec)
if err == nil {
return passwd, nil
}
// fallback
return uuid.GenerateUUID()
}

const pathCredsCreateHelpSyn = `
Request Splunk credentials for a certain role.
`
Expand Down
12 changes: 12 additions & 0 deletions path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ func (b *backend) pathRoles() *framework.Path {
Type: framework.TypeCommaStringSlice,
Description: "Comma-separated string or list of Splunk roles.",
},
"allowed_node_types": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: trimIndent(`
Comma-separated string or array of node type (glob) patterns that are allowed
to fetch credentials for. If empty, no nodes are allowed. If "*", all
node types are allowed.`),
Default: []string{"*"},
},
"default_app": &framework.FieldSchema{
Type: framework.TypeString,
Description: trimIndent(`
Expand Down Expand Up @@ -114,6 +122,10 @@ func (b *backend) rolesWriteHandler(ctx context.Context, req *logical.Request, d
if maxTTLRaw, ok := getValue(data, req.Operation, "max_ttl"); ok {
role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second
}
if allowed_node_types, ok := getValue(data, req.Operation, "allowed_node_types"); ok {
role.AllowedNodeTypes = allowed_node_types.([]string)
}
role.PasswordSpec = DefaultPasswordSpec() // XXX make configurable

if roles, ok := getValue(data, req.Operation, "roles"); ok {
role.Roles = roles.([]string)
Expand Down
8 changes: 5 additions & 3 deletions role.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (
)

type roleConfig struct {
Connection string `json:"connection" structs:"connection"`
DefaultTTL time.Duration `json:"default_ttl" structs:"default_ttl"`
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl"`
Connection string `json:"connection" structs:"connection"`
DefaultTTL time.Duration `json:"default_ttl" structs:"default_ttl"`
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl"`
AllowedNodeTypes []string `json:"allowed_node_types" structs:"allowed_node_types"`
PasswordSpec *PasswordSpec `json:"password_spec" structs:"password_spec"`

// Splunk user attributes
Roles []string `json:"roles" structs:"roles"`
Expand Down