Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vault Agent Auto-auth Certificate Method #6652

Merged
merged 11 commits into from
May 6, 2019
9 changes: 6 additions & 3 deletions command/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"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/cert"
"github.com/hashicorp/vault/command/agent/auth/gcp"
"github.com/hashicorp/vault/command/agent/auth/jwt"
"github.com/hashicorp/vault/command/agent/auth/kubernetes"
Expand Down Expand Up @@ -331,6 +332,8 @@ func (c *AgentCommand) Run(args []string) int {
method, err = aws.NewAWSAuthMethod(authConfig)
case "azure":
method, err = azure.NewAzureAuthMethod(authConfig)
case "cert":
method, err = cert.NewCertAuthMethod(authConfig)
case "gcp":
method, err = gcp.NewGCPAuthMethod(authConfig)
case "jwt":
Expand Down Expand Up @@ -457,9 +460,9 @@ func (c *AgentCommand) Run(args []string) int {
// Start auto-auth and sink servers
if method != nil {
ah := auth.NewAuthHandler(&auth.AuthHandlerConfig{
Logger: c.logger.Named("auth.handler"),
Client: c.client,
WrapTTL: config.AutoAuth.Method.WrapTTL,
Logger: c.logger.Named("auth.handler"),
traviscosgrave marked this conversation as resolved.
Show resolved Hide resolved
Client: c.client,
WrapTTL: config.AutoAuth.Method.WrapTTL,
EnableReauthOnNewCredentials: config.AutoAuth.EnableReauthOnNewCredentials,
})
ahDoneCh = ah.DoneCh
Expand Down
62 changes: 62 additions & 0 deletions command/agent/auth/cert/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cert

import (
"context"
"errors"
"fmt"

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

type certMethod struct {
logger hclog.Logger
mountPath string
name string
}

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

// Not concerned if the conf.Config is empty as the 'name'
// parameter is optional when using TLS Auth

c := &certMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
}

nameRaw, ok := conf.Config["name"]
traviscosgrave marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
nameRaw = ""
}
c.name, ok = nameRaw.(string)
if !ok {
return nil, errors.New("could not convert 'name' config value to string")
}

return c, nil
}

func (c *certMethod) Authenticate(_ context.Context, client *api.Client) (string, map[string]interface{}, error) {
c.logger.Trace("beginning authentication")

authMap := map[string]interface{}{}

if c.name != "" {
authMap["name"] = c.name
}

return fmt.Sprintf("%s/login", c.mountPath), authMap, nil
}

func (c *certMethod) NewCreds() chan struct{} {
return nil
}

func (c *certMethod) CredSuccess() {}

func (c *certMethod) Shutdown() {}
241 changes: 241 additions & 0 deletions command/agent/cert_end_to_end_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package agent

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

hclog "github.com/hashicorp/go-hclog"

"github.com/hashicorp/vault/api"
vaultcert "github.com/hashicorp/vault/builtin/credential/cert"
"github.com/hashicorp/vault/command/agent/auth"
agentcert "github.com/hashicorp/vault/command/agent/auth/cert"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/command/agent/sink/file"
"github.com/hashicorp/vault/helper/dhutil"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)

func TestCertEndToEnd(t *testing.T) {
testCertEndToEnd(t, false)
testCertEndToEnd(t, true)
}

func testCertEndToEnd(t *testing.T, ahWrapping bool) {
logger := logging.NewVaultLogger(hclog.Trace)
coreConfig := &vault.CoreConfig{
Logger: logger,
CredentialBackends: map[string]logical.Factory{
"cert": vaultcert.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()

vault.TestWaitActive(t, cluster.Cores[0].Core)
client := cluster.Cores[0].Client

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

certificatePEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cluster.CACert.Raw})

_, err = client.Logical().Write("auth/cert/certs/test", map[string]interface{}{
"name": "test",
traviscosgrave marked this conversation as resolved.
Show resolved Hide resolved
"certificate": string(certificatePEM),
"policies": "default",
})
if err != nil {
t.Fatal(err)
}

// Generate encryption params
pub, pri, err := dhutil.GeneratePublicPrivateKey()
if err != nil {
t.Fatal(err)
}

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)

dhpathf, err := ioutil.TempFile("", "auth.dhpath.test.")
if err != nil {
t.Fatal(err)
}
dhpath := dhpathf.Name()
dhpathf.Close()
os.Remove(dhpath)

// Write DH public key to file
mPubKey, err := jsonutil.EncodeJSON(&dhutil.PublicKeyInfo{
Curve25519PublicKey: pub,
})
if err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(dhpath, mPubKey, 0600); err != nil {
t.Fatal(err)
} else {
logger.Trace("wrote dh param file", "path", dhpath)
}

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

am, err := agentcert.NewCertAuthMethod(&auth.AuthConfig{
Logger: logger.Named("auth.cert"),
MountPath: "auth/cert",
})
if err != nil {
t.Fatal(err)
}

ahConfig := &auth.AuthHandlerConfig{
Logger: logger.Named("auth.handler"),
Client: client,
EnableReauthOnNewCredentials: true,
}
if ahWrapping {
ahConfig.WrapTTL = 10 * time.Second
}
ah := auth.NewAuthHandler(ahConfig)
go ah.Run(ctx, am)
defer func() {
<-ah.DoneCh
}()

config := &sink.SinkConfig{
Logger: logger.Named("sink.file"),
AAD: "foobar",
DHType: "curve25519",
DHPath: dhpath,
Config: map[string]interface{}{
"path": out,
},
}
if !ahWrapping {
config.WrapTTL = 10 * time.Second
}
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()

cloned, err := client.Clone()
if err != nil {
t.Fatal(err)
}

checkToken := func() string {
timeout := time.Now().Add(5 * time.Second)
for {
if time.Now().After(timeout) {
t.Fatal("did not find a written token after timeout")
}
val, err := ioutil.ReadFile(out)
if err == nil {
os.Remove(out)
if len(val) == 0 {
t.Fatal("written token was empty")
}

// First decrypt it
resp := new(dhutil.Envelope)
if err := jsonutil.DecodeJSON(val, resp); err != nil {
continue
}

aesKey, err := dhutil.GenerateSharedKey(pri, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
if len(aesKey) == 0 {
t.Fatal("got empty aes key")
}

val, err = dhutil.DecryptAES(aesKey, resp.EncryptedPayload, resp.Nonce, []byte("foobar"))
if err != nil {
t.Fatalf("error: %v\nresp: %v", err, string(val))
}

// Now unwrap it
wrapInfo := new(api.SecretWrapInfo)
if err := jsonutil.DecodeJSON(val, wrapInfo); err != nil {
t.Fatal(err)
}
switch {
case wrapInfo.TTL != 10:
t.Fatalf("bad wrap info: %v", wrapInfo.TTL)
case !ahWrapping && wrapInfo.CreationPath != "sys/wrapping/wrap":
t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath)
case ahWrapping && wrapInfo.CreationPath != "auth/cert/login":
t.Fatalf("bad wrap path: %v", wrapInfo.CreationPath)
case wrapInfo.Token == "":
t.Fatal("wrap token is empty")
}
cloned.SetToken(wrapInfo.Token)
secret, err := cloned.Logical().Unwrap("")
if err != nil {
t.Fatal(err)
}
if ahWrapping {
switch {
case secret.Auth == nil:
t.Fatal("unwrap secret auth is nil")
case secret.Auth.ClientToken == "":
t.Fatal("unwrap token is nil")
}
return secret.Auth.ClientToken
} else {
switch {
case secret.Data == nil:
t.Fatal("unwrap secret data is nil")
case secret.Data["token"] == nil:
t.Fatal("unwrap token is nil")
}
return secret.Data["token"].(string)
}
}
time.Sleep(250 * time.Millisecond)
}
}
checkToken()
}
22 changes: 22 additions & 0 deletions website/source/docs/agent/autoauth/methods/cert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
layout: "docs"
page_title: "Vault Agent Auto-Auth Cert Method"
sidebar_title: "Cert"
sidebar_current: "docs-agent-autoauth-methods-cert"
description: |-
Cert Method for Vault Agent Auto-Auth
---

# Vault Agent Auto-Auth Cert Method

The `cert` method uses the configured TLS certificates from the `vault` stanza of
the agent configuration and takes an optional `name` parameter. There is no option
to use certificates which differ from those used in the `vault` stanza.

See TLS settings in the [`vault` Stanza](https://vaultproject.io/docs/agent/index.html#vault-stanza)

## Configuration

* `name` `(string: optional)` - The trusted certificate role which should be used
when authenticating with TLS. If a `name` is not specificed, the auth method will
traviscosgrave marked this conversation as resolved.
Show resolved Hide resolved
try to authenticate against [all trusted certificates](https://www.vaultproject.io/docs/auth/cert.html#authentication).