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

Agent kube projected token #5725

Merged
merged 2 commits into from
Nov 19, 2018
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
61 changes: 57 additions & 4 deletions command/agent/auth/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
Expand All @@ -22,8 +24,17 @@ type kubernetesMethod struct {
mountPath string

role string

// tokenPath is an optional path to a projected service account token inside
// the pod, for use instead of the default service account token.
tokenPath string

// jwtData is a ReaderCloser used to inject a ReadCloser for mocking tests.
catsby marked this conversation as resolved.
Show resolved Hide resolved
jwtData io.ReadCloser
}

// NewKubernetesAuthMethod reads the user configuration and returns a configured
// AuthMethod
func NewKubernetesAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
Expand All @@ -45,6 +56,15 @@ func NewKubernetesAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if !ok {
return nil, errors.New("could not convert 'role' config value to string")
}

tokenPathRaw, ok := conf.Config["token_path"]
if ok {
k.tokenPath, ok = tokenPathRaw.(string)
if !ok {
return nil, errors.New("could not convert 'token_path' config value to string")
}
}

if k.role == "" {
return nil, errors.New("'role' value is empty")
}
Expand All @@ -54,14 +74,15 @@ func NewKubernetesAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {

func (k *kubernetesMethod) Authenticate(ctx context.Context, client *api.Client) (string, map[string]interface{}, error) {
k.logger.Trace("beginning authentication")
content, err := ioutil.ReadFile(serviceAccountFile)

jwtString, err := k.readJWT()
if err != nil {
log.Fatal(err)
return "", nil, errwrap.Wrapf("error reading JWT with Kubernetes Auth: {{err}}", err)
catsby marked this conversation as resolved.
Show resolved Hide resolved
}

return fmt.Sprintf("%s/login", k.mountPath), map[string]interface{}{
"role": k.role,
"jwt": strings.TrimSpace(string(content)),
"jwt": jwtString,
}, nil
}

Expand All @@ -74,3 +95,35 @@ func (k *kubernetesMethod) CredSuccess() {

func (k *kubernetesMethod) Shutdown() {
}

// readJWT reads the JWT data for the Agent to submit to Vault. The default is
// to read the JWT from the default service account location, defined by the
// constant serviceAccountFile. In normal use k.jwtData is nil at invocation and
// the method falls back to reading the token path with os.Open, opening a file
// from either the default location or from the token_path path specified in
// configuration.
func (k *kubernetesMethod) readJWT() (string, error) {
// load configured token path if set, default to serviceAccountFile
tokenFilePath := serviceAccountFile
if k.tokenPath != "" {
tokenFilePath = k.tokenPath
}

data := k.jwtData
// k.jwtData should only be non-nil in tests
if data == nil {
f, err := os.Open(tokenFilePath)
if err != nil {
return "", err
}
data = f
}
defer data.Close()

contentBytes, err := ioutil.ReadAll(data)
if err != nil {
return "", err
}

return strings.TrimSpace(string(contentBytes)), nil
}
121 changes: 121 additions & 0 deletions command/agent/auth/kubernetes/kubernetes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package kubernetes

import (
"bytes"
"context"
"errors"
"io"
"testing"

"github.com/hashicorp/errwrap"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/command/agent/auth"
"github.com/hashicorp/vault/helper/logging"
)

func TestKubernetesAuth_basic(t *testing.T) {
testCases := map[string]struct {
tokenPath string
data *mockJWTFile
e error
}{
"normal": {
data: newMockJWTFile(jwtData),
},
"projected": {
tokenPath: "/some/other/path",
data: newMockJWTFile(jwtProjectedData),
},
"not_found": {
e: errors.New("open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory"),
},
"projected_not_found": {
tokenPath: "/some/other/path",
e: errors.New("open /some/other/path: no such file or directory"),
},
}

for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
authCfg := auth.AuthConfig{
Logger: logging.NewVaultLogger(hclog.Trace),
MountPath: "kubernetes",
Config: map[string]interface{}{
"role": "plugin-test",
},
}

if tc.tokenPath != "" {
authCfg.Config["token_path"] = tc.tokenPath
}

a, err := NewKubernetesAuthMethod(&authCfg)
if err != nil {
t.Fatal(err)
}

// Type assert to set the kubernetesMethod jwtData, to mock out reading
// files from the pod.
k := a.(*kubernetesMethod)
if tc.data != nil {
k.jwtData = tc.data
}

_, data, err := k.Authenticate(context.Background(), nil)
if err != nil && tc.e == nil {
t.Fatal(err)
}

if err != nil && !errwrap.Contains(err, tc.e.Error()) {
t.Fatalf("expected \"no such file\" error, got: (%s)", err)
}

if err == nil && tc.e != nil {
t.Fatal("expected error, but got none")
}

if tc.e == nil {
authJWTraw, ok := data["jwt"]
if !ok {
t.Fatal("expected to find jwt data")
}

authJWT := authJWTraw.(string)
token := jwtData
if tc.tokenPath != "" {
token = jwtProjectedData
}
if authJWT != token {
t.Fatalf("error with auth tokens, expected (%s) got (%s)", token, authJWT)
}
}
})
}

}

// jwt for default service account
var jwtData = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InZhdWx0LWF1dGgtdG9rZW4tdDVwY24iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoidmF1bHQtYXV0aCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImQ3N2Y4OWJjLTkwNTUtMTFlNy1hMDY4LTA4MDAyNzZkOTliZiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0LWF1dGgifQ.HKUcqgrvan5ZC_mnpaMEx4RW3KrhfyH_u8G_IA2vUfkLK8tH3T7fJuJaPr7W6K_BqCrbeM5y3owszOzb4NR0Lvw6GBt2cFcen2x1Ua4Wokr0bJjTT7xQOIOw7UvUDyVS17wAurlfUnmWMwMMMOebpqj5K1t6GnyqghH1wPdHYRGX-q5a6C323dBCgM5t6JY_zTTaBgM6EkFq0poBaifmSMiJRPrdUN_-IgyK8fgQRiFYYkgS6DMIU4k4nUOb_sUFf5xb8vMs3SMteKiuWFAIt4iszXTj5IyBUNqe0cXA3zSY3QiNCV6bJ2CWW0Qf9WDtniT79VAqcR4GYaTC_gxjNA"

// jwt for projected service account
var jwtProjectedData = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJhdWQiOlsia3ViZXJuZXRlcy5kZWZhdWx0LnN2YyJdLCJleHAiOjE2MDMwNTM1NjMsImlhdCI6MTUzOTk4MTU2MywiaXNzIjoia3ViZXJuZXRlcy9zZXJ2aWNlYWNjb3VudCIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsInBvZCI6eyJuYW1lIjoidmF1bHQiLCJ1aWQiOiIxMDA2YTA2Yy1kM2RmLTExZTgtOGZlMi0wODAwMjdlNTVlYTgifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJiMzg5YjNiMi1kMzAyLTExZTgtYjE0Yy0wODAwMjdlNTVlYTgifX0sIm5iZiI6MTUzOTk4MTU2Mywic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCJ9.byu3BpCbs0tzQvEBCRTayXF3-kV1Ey7YvStBcCwovfSl6evBze43FFaDps78HtdDAMszjE_yn55_1BMN87EzOZYsF3GBoPLWxkofxhPIy88wmPTpurBsSx-nCKdjf4ayXhTpqGG9gy0xlkUc_xL4pM3Q8XZiqYqwq_T0PHXOpSfdzVy1oabFSZXr5QTZ377v8bvrMgAVWJF_4vZsSMG3XVCK8KBWNRw4_wt6yOelVKE5OGLPJvNu1CFjEKh4HBFBcQnB_Sgpe1nPlnm5utp-1-OVfd7zopOGDAp_Pk_Apu8OPDdPSafn6HpzIeuhMtWXcv1K8ZhZYDLC1wLywZPNyw"

// mockJWTFile provides a mock ReadCloser struct to inject into
// kubernetesMethod.jwtData
type mockJWTFile struct {
b *bytes.Buffer
}

var _ io.ReadCloser = &mockJWTFile{}

func (j *mockJWTFile) Read(p []byte) (n int, err error) {
return j.b.Read(p)
}

func (j *mockJWTFile) Close() error { return nil }

func newMockJWTFile(s string) *mockJWTFile {
return &mockJWTFile{
b: bytes.NewBufferString(s),
}
}
2 changes: 2 additions & 0 deletions website/source/docs/agent/autoauth/methods/kubernetes.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ method](https://www.vaultproject.io/docs/auth/kubernetes.html).
## Configuration

- `role` `(string: required)` - The role to authenticate against on Vault
- `token_path` `(string: optional)` - The file path to a custom JWT token to use
for authentication. If omitted, the default service account token path is used.