From e9d855217bc82922b9062a44ed8a35d1139effbf Mon Sep 17 00:00:00 2001 From: Aleksey Zhukov Date: Tue, 30 Oct 2018 19:17:19 +0300 Subject: [PATCH 1/2] WIP Agent AppRole auto-auth (#5621) --- command/agent.go | 3 + command/agent/approle_end_to_end_test.go | 186 ++++++++++++++++++ command/agent/auth/approle/approle.go | 89 +++++++++ .../agent/autoauth/methods/approle.html.md | 19 ++ 4 files changed, 297 insertions(+) create mode 100644 command/agent/approle_end_to_end_test.go create mode 100644 command/agent/auth/approle/approle.go create mode 100644 website/source/docs/agent/autoauth/methods/approle.html.md diff --git a/command/agent.go b/command/agent.go index 72780e7c62cc..c8f254e57f9d 100644 --- a/command/agent.go +++ b/command/agent.go @@ -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" @@ -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 diff --git a/command/agent/approle_end_to_end_test.go b/command/agent/approle_end_to_end_test.go new file mode 100644 index 000000000000..120db361839e --- /dev/null +++ b/command/agent/approle_end_to_end_test.go @@ -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") + } +} diff --git a/command/agent/auth/approle/approle.go b/command/agent/auth/approle/approle.go new file mode 100644 index 000000000000..a49f58c6c3b8 --- /dev/null +++ b/command/agent/auth/approle/approle.go @@ -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() { +} diff --git a/website/source/docs/agent/autoauth/methods/approle.html.md b/website/source/docs/agent/autoauth/methods/approle.html.md new file mode 100644 index 000000000000..514eee46799e --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/approle.html.md @@ -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 From e7eadd09ba7106d81a4f736d46c40f7f83c6ff62 Mon Sep 17 00:00:00 2001 From: RJ Spiker Date: Tue, 30 Oct 2018 10:33:51 -0600 Subject: [PATCH 2/2] website: community page content update (#5641) --- website/source/community.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/community.html.erb b/website/source/community.html.erb index 1d9e860255dc..073d5c9c5007 100644 --- a/website/source/community.html.erb +++ b/website/source/community.html.erb @@ -26,6 +26,6 @@ description: "Vault is an open source project with a growing community." on GitHub](https://github.com/hashicorp/vault/issues) for reporting bugs. Use IRC or the mailing list for general help.' }, { header: 'Training', - body: '[Paid HashiCorp](https://www.hashicorp.com/training.html) training courses are also available in a city near you. Private training courses are also available.' + body: '[Paid HashiCorp](https://www.hashicorp.com/training.html) training courses are available in a city near you. Private training courses are also available.' } ]) %>">