Skip to content

Commit

Permalink
WIP Agent AppRole auto-auth (#5621)
Browse files Browse the repository at this point in the history
  • Loading branch information
alezkv authored and jefferai committed Oct 30, 2018
1 parent c0b97de commit e9d8552
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 0 deletions.
3 changes: 3 additions & 0 deletions command/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/command/agent/auth"
"github.com/hashicorp/vault/command/agent/auth/alicloud"
"github.com/hashicorp/vault/command/agent/auth/approle"
"github.com/hashicorp/vault/command/agent/auth/aws"
"github.com/hashicorp/vault/command/agent/auth/azure"
"github.com/hashicorp/vault/command/agent/auth/gcp"
Expand Down Expand Up @@ -296,6 +297,8 @@ func (c *AgentCommand) Run(args []string) int {
method, err = jwt.NewJWTAuthMethod(authConfig)
case "kubernetes":
method, err = kubernetes.NewKubernetesAuthMethod(authConfig)
case "approle":
method, err = approle.NewApproleAuthMethod(authConfig)
default:
c.UI.Error(fmt.Sprintf("Unknown auth method %q", config.AutoAuth.Method.Type))
return 1
Expand Down
186 changes: 186 additions & 0 deletions command/agent/approle_end_to_end_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package agent

import (
"context"
"io/ioutil"
"os"
"testing"
"time"

log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
"github.com/hashicorp/vault/command/agent/auth"
agentapprole "github.com/hashicorp/vault/command/agent/auth/approle"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/command/agent/sink/file"
"github.com/hashicorp/vault/helper/logging"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)

func TestAppRoleEndToEnd(t *testing.T) {
var err error
logger := logging.NewVaultLogger(log.Trace)
coreConfig := &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: log.NewNullLogger(),
CredentialBackends: map[string]logical.Factory{
"approle": credAppRole.Factory,
},
}

cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})

cluster.Start()
defer cluster.Cleanup()

cores := cluster.Cores

vault.TestWaitActive(t, cores[0].Core)

client := cores[0].Client

err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{
Type: "approle",
})
if err != nil {
t.Fatal(err)
}

_, err = client.Logical().Write("auth/approle/role/role1", map[string]interface{}{
"bind_secret_id": "true",
"period": "300",
})
if err != nil {
t.Fatal(err)
}

vaultResult, err := client.Logical().Write("auth/approle/role/role1/secret-id", nil)
if err != nil {
t.Fatal(err)
}
secretID := vaultResult.Data["secret_id"].(string)

vaultResult, err = client.Logical().Read("auth/approle/role/role1/role-id")
if err != nil {
t.Fatal(err)
}
roleID := vaultResult.Data["role_id"].(string)

rolef, err := ioutil.TempFile("", "auth.role-id.test.")
if err != nil {
t.Fatal(err)
}
role := rolef.Name()
defer rolef.Close()
defer os.Remove(role)

t.Logf("input role_id_path: %s", role)
if err := ioutil.WriteFile(role, []byte(roleID), 0600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test role-id", "path", role)
}

secretf, err := ioutil.TempFile("", "auth.secret-id.test.")
if err != nil {
t.Fatal(err)
}
secret := secretf.Name()
defer secretf.Close()
defer os.Remove(secret)

t.Logf("input secret_id_path: %s", secret)
if err := ioutil.WriteFile(secret, []byte(secretID), 0600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote test secret-id", "path", secret)
}

// We close these right away because we're just basically testing
// permissions and finding a usable file name
ouf, err := ioutil.TempFile("", "auth.tokensink.test.")
if err != nil {
t.Fatal(err)
}
out := ouf.Name()
ouf.Close()
os.Remove(out)
t.Logf("output: %s", out)

ctx, cancelFunc := context.WithCancel(context.Background())
timer := time.AfterFunc(30*time.Second, func() {
cancelFunc()
})
defer timer.Stop()

am, err := agentapprole.NewApproleAuthMethod(&auth.AuthConfig{
Logger: logger.Named("auth.approle"),
MountPath: "auth/approle",
Config: map[string]interface{}{
"role_id_path": role,
"secret_id_path": secret,
},
})
if err != nil {
t.Fatal(err)
}
ahConfig := &auth.AuthHandlerConfig{
Logger: logger.Named("auth.handler"),
Client: client,
}
ah := auth.NewAuthHandler(ahConfig)
go ah.Run(ctx, am)
defer func() {
<-ah.DoneCh
}()

config := &sink.SinkConfig{
Logger: logger.Named("sink.file"),
Config: map[string]interface{}{
"path": out,
},
}
fs, err := file.NewFileSink(config)
if err != nil {
t.Fatal(err)
}
config.Sink = fs

ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: logger.Named("sink.server"),
Client: client,
})
go ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
defer func() {
<-ss.DoneCh
}()

// This has to be after the other defers so it happens first
defer cancelFunc()

// Check that no sink file exists
_, err = os.Lstat(out)
if err == nil {
t.Fatal("expected err")
}
if !os.IsNotExist(err) {
t.Fatal("expected notexist err")
}

token, err := client.Logical().Write("auth/approle/login", map[string]interface{}{
"role_id": roleID,
"secret_id": secretID,
})
if err != nil {
t.Fatal(err)
}
if token.Auth.ClientToken == "" {
t.Fatalf("expected a successful login")
}
}
89 changes: 89 additions & 0 deletions command/agent/auth/approle/approle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package approle

import (
"context"
"errors"
"fmt"
"io/ioutil"
"log"
"strings"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
)

type approleMethod struct {
logger hclog.Logger
mountPath string

roleIDPath string
secretIDPath string
}

func NewApproleAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}

a := &approleMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
}

roleIDPathRaw, ok := conf.Config["role_id_path"]
if !ok {
return nil, errors.New("missing 'role_id_path' value")
}
a.roleIDPath, ok = roleIDPathRaw.(string)
if !ok {
return nil, errors.New("could not convert 'role_id_path' config value to string")
}
if a.roleIDPath == "" {
return nil, errors.New("'role_id_path' value is empty")
}

secretIDPathRaw, ok := conf.Config["secret_id_path"]
if !ok {
return nil, errors.New("missing 'secret_id_path' value")
}
a.secretIDPath, ok = secretIDPathRaw.(string)
if !ok {
return nil, errors.New("could not convert 'secret_id_path' config value to string")
}
if a.secretIDPath == "" {
return nil, errors.New("'secret_id_path' value is empty")
}

return a, nil
}

func (a *approleMethod) Authenticate(ctx context.Context, client *api.Client) (string, map[string]interface{}, error) {
a.logger.Trace("beginning authentication")
roleID, err := ioutil.ReadFile(a.roleIDPath)
if err != nil {
log.Fatal(err)
}
secretID, err := ioutil.ReadFile(a.secretIDPath)
if err != nil {
log.Fatal(err)
}

return fmt.Sprintf("%s/login", a.mountPath), map[string]interface{}{
"role_id": strings.TrimSpace(string(roleID)),
"secret_id": strings.TrimSpace(string(secretID)),
}, nil
}

func (a *approleMethod) NewCreds() chan struct{} {
return nil
}

func (a *approleMethod) CredSuccess() {
}

func (a *approleMethod) Shutdown() {
}
19 changes: 19 additions & 0 deletions website/source/docs/agent/autoauth/methods/approle.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth AppRole Method"
sidebar_title: "AppRole"
sidebar_current: "docs-agent-autoauth-methods-approle"
description: |-
AppRole Method for Vault Agent Auto-Auth
---

# Vault Agent Auto-Auth AppRole Method

The `approle` method reads in a role-id/secret-id from a files and sends it to the [AppRole Auth
method](https://www.vaultproject.io/docs/auth/approle.html).

## Configuration

* `role_id_path` `(string: required)` - The path to the file with role-id

* `secret_id_path` `(string: required)` - The path to the file with secret-id

0 comments on commit e9d8552

Please sign in to comment.