From 79428aa2318ff960e70cda1af7a4a3b83dd95269 Mon Sep 17 00:00:00 2001 From: Matt Leung Date: Tue, 24 Apr 2018 14:48:50 -0700 Subject: [PATCH 1/4] Enable Vault auth through kubernetes auth method Added a feature to obtain the initial Vault token from the Kubernetes auth method. This works by making a request to the Vault server at the specified auth method mount point's login path and presenting the JWT located in a file on a running pod, along with the Kubernetes role to authenticate as. Vault will then respond with a token and its TTL, if the request is valid. --- cmd/drone-server/server.go | 18 +++++++ plugins/secrets/vault/fixtures/fakeJwt | 1 + plugins/secrets/vault/kubernetes.go | 47 ++++++++++++++++ plugins/secrets/vault/kubernetes_test.go | 69 ++++++++++++++++++++++++ plugins/secrets/vault/opts.go | 24 ++++++++- 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 plugins/secrets/vault/fixtures/fakeJwt create mode 100644 plugins/secrets/vault/kubernetes.go create mode 100644 plugins/secrets/vault/kubernetes_test.go diff --git a/cmd/drone-server/server.go b/cmd/drone-server/server.go index f36527bd1..2729e6a43 100644 --- a/cmd/drone-server/server.go +++ b/cmd/drone-server/server.go @@ -175,6 +175,24 @@ var flags = []cli.Flag{ Usage: "token to secure prometheus metrics endpoint", Value: "", }, + cli.StringFlag{ + EnvVar: "DRONE_VAULT_AUTH_TYPE", + Name: "drone-vault-auth-type", + Usage: "auth backend type used for connecting to vault", + Value: "token", + }, + cli.StringFlag{ + EnvVar: "DRONE_VAULT_AUTH_MOUNT_POINT", + Name: "drone-vault-auth-mount-point", + Usage: "mount point for desired vault auth backend", + Value: "", + }, + cli.StringFlag{ + EnvVar: "DRONE_VAULT_KUBERNETES_ROLE", + Name: "drone-vault-kubernetes-role", + Usage: "role to authenticate as for vault kubernetes auth", + Value: "", + }, // // resource limit parameters // diff --git a/plugins/secrets/vault/fixtures/fakeJwt b/plugins/secrets/vault/fixtures/fakeJwt new file mode 100644 index 000000000..1e3abd126 --- /dev/null +++ b/plugins/secrets/vault/fixtures/fakeJwt @@ -0,0 +1 @@ +fakeJwt diff --git a/plugins/secrets/vault/kubernetes.go b/plugins/secrets/vault/kubernetes.go new file mode 100644 index 000000000..ca23021d8 --- /dev/null +++ b/plugins/secrets/vault/kubernetes.go @@ -0,0 +1,47 @@ +package vault + +import ( + "fmt" + "github.com/drone/drone/plugins/internal" + "io/ioutil" + "time" +) + +/* +Vault JSON Response +{ + "auth": { + "client_token" = "token", + "lease_duration" = "1234s" + } +} +*/ +type VaultAuth struct { + Token string `json:"client_token"` + Lease string `json:"lease_duration"` +} +type VaultResp struct { + Auth VaultAuth +} + +func getKubernetesToken(addr, role, mountPoint, tokenFile string) (string, time.Duration, error) { + b, err := ioutil.ReadFile(tokenFile) + if err != nil { + return "", 0, err + } + + var resp VaultResp + path := fmt.Sprintf("%s/v1/auth/%s/login", addr, mountPoint) + data := map[string]string{ + "jwt": string(b), + "role": role, + } + + err = internal.Send("POST", path, data, &resp) + if err != nil { + return "", 0, err + } + + ttl, err := time.ParseDuration(resp.Auth.Lease) + return resp.Auth.Token, ttl, nil +} diff --git a/plugins/secrets/vault/kubernetes_test.go b/plugins/secrets/vault/kubernetes_test.go new file mode 100644 index 000000000..1e1fbedee --- /dev/null +++ b/plugins/secrets/vault/kubernetes_test.go @@ -0,0 +1,69 @@ +package vault + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestGetKubernetesToken(t *testing.T) { + fakeRole := "fakeRole" + fakeMountPoint := "kubernetes" + fakeJwtFile := "fixtures/fakeJwt" + b, _ := ioutil.ReadFile(fakeJwtFile) + fakeJwt := string(b) + fakeClientToken := "fakeClientToken" + fakeLeaseMinutes := "10m" + fakeLeaseDuration, _ := time.ParseDuration(fakeLeaseMinutes) + fakeResp := fmt.Sprintf("{\"auth\": {\"client_token\": \"%s\", \"lease_duration\": \"%s\"}}", fakeClientToken, fakeLeaseMinutes) + expectedPath := "/v1/auth/kubernetes/login" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if r.Method != "POST" { + t.Errorf("Expected 'POST' request, got '%s'", r.Method) + } + if r.URL.EscapedPath() != expectedPath { + t.Errorf("Expected request to '%s', got '%s'", expectedPath, r.URL.EscapedPath()) + } + + var postdata struct { + Jwt string + Role string + } + err := json.NewDecoder(r.Body).Decode(&postdata) + if err != nil { + t.Errorf("Encountered error parsing request JSON: %s", err) + } + + jwt := postdata.Jwt + + if jwt != fakeJwt { + t.Errorf("Expected request to have jwt with value '%s', got: '%s'", fakeJwt, jwt) + } + role := postdata.Role + if role != fakeRole { + t.Errorf("Expected request to have role with value '%s', got: '%s'", fakeRole, role) + } + + fmt.Fprintf(w, fakeResp) + })) + defer ts.Close() + + url := ts.URL + token, ttl, err := getKubernetesToken(url, fakeRole, fakeMountPoint, fakeJwtFile) + if err != nil { + t.Errorf("getKubernetesToken returned an error: %s", err) + } + + if token != fakeClientToken { + t.Errorf("Expected returned token to have value '%s', got: '%s'", fakeClientToken, token) + } + if ttl != fakeLeaseDuration { + t.Errorf("Expected TTL to have value '%s', got: '%s'", fakeLeaseDuration.Seconds(), ttl.Seconds()) + } +} diff --git a/plugins/secrets/vault/opts.go b/plugins/secrets/vault/opts.go index e2833aaa3..037071ea8 100644 --- a/plugins/secrets/vault/opts.go +++ b/plugins/secrets/vault/opts.go @@ -4,7 +4,11 @@ package vault -import "time" +import ( + "github.com/Sirupsen/logrus" + "os" + "time" +) // Opts sets custom options for the vault client. type Opts func(v *vault) @@ -24,3 +28,21 @@ func WithRenewal(d time.Duration) Opts { v.renew = d } } + +func WithKubernetesAuth() Opts { + return func(v *vault) { + addr := os.Getenv("VAULT_ADDR") + role := os.Getenv("DRONE_VAULT_KUBERNETES_ROLE") + mount := os.Getenv("DRONE_VAULT_AUTH_MOUNT_POINT") + jwtFile := "/var/run/secrets/kubernetes.io/serviceaccount/token" + token, ttl, err := getKubernetesToken(addr, role, mount, jwtFile) + if err != nil { + logrus.Debugf("vault: failed to obtain token via kubernetes-auth backend: %s", err) + return + } + + v.client.SetToken(token) + v.ttl = ttl + v.renew = ttl / 2 + } +} From c4fe6496b5d99ff097fa393e2199a16b0cd8709a Mon Sep 17 00:00:00 2001 From: Matt Leung Date: Tue, 24 Apr 2018 15:26:12 -0700 Subject: [PATCH 2/4] fixup: catch err from parseduration --- plugins/secrets/vault/kubernetes.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/secrets/vault/kubernetes.go b/plugins/secrets/vault/kubernetes.go index ca23021d8..be5d8ed04 100644 --- a/plugins/secrets/vault/kubernetes.go +++ b/plugins/secrets/vault/kubernetes.go @@ -43,5 +43,9 @@ func getKubernetesToken(addr, role, mountPoint, tokenFile string) (string, time. } ttl, err := time.ParseDuration(resp.Auth.Lease) + if err != nil { + return "", 0, err + } + return resp.Auth.Token, ttl, nil } From db698f9ef450d9a722a52707dbafb6de47527065 Mon Sep 17 00:00:00 2001 From: Matt Leung Date: Fri, 27 Apr 2018 14:22:20 -0700 Subject: [PATCH 3/4] fixup: some comments, added opts test, address pr concerns --- plugins/secrets/vault/kubernetes.go | 12 ++++----- plugins/secrets/vault/opts.go | 27 ++++++------------- plugins/secrets/vault/opts_test.go | 18 +++++++++++++ plugins/secrets/vault/vault.go | 40 +++++++++++++++++++++++++---- 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/plugins/secrets/vault/kubernetes.go b/plugins/secrets/vault/kubernetes.go index be5d8ed04..f289a8aff 100644 --- a/plugins/secrets/vault/kubernetes.go +++ b/plugins/secrets/vault/kubernetes.go @@ -16,22 +16,22 @@ Vault JSON Response } } */ -type VaultAuth struct { +type vaultAuth struct { Token string `json:"client_token"` Lease string `json:"lease_duration"` } -type VaultResp struct { - Auth VaultAuth +type vaultResp struct { + Auth vaultAuth } -func getKubernetesToken(addr, role, mountPoint, tokenFile string) (string, time.Duration, error) { +func getKubernetesToken(addr, role, mount, tokenFile string) (string, time.Duration, error) { b, err := ioutil.ReadFile(tokenFile) if err != nil { return "", 0, err } - var resp VaultResp - path := fmt.Sprintf("%s/v1/auth/%s/login", addr, mountPoint) + var resp vaultResp + path := fmt.Sprintf("%s/v1/auth/%s/login", addr, mount) data := map[string]string{ "jwt": string(b), "role": role, diff --git a/plugins/secrets/vault/opts.go b/plugins/secrets/vault/opts.go index 037071ea8..081857854 100644 --- a/plugins/secrets/vault/opts.go +++ b/plugins/secrets/vault/opts.go @@ -4,11 +4,7 @@ package vault -import ( - "github.com/Sirupsen/logrus" - "os" - "time" -) +import "time" // Opts sets custom options for the vault client. type Opts func(v *vault) @@ -29,20 +25,13 @@ func WithRenewal(d time.Duration) Opts { } } -func WithKubernetesAuth() Opts { +// WithKubernetes returns an options that sets +// kubernetes-auth parameters required to retrieve +// an initial Vault token +func WithKubernetesAuth(addr, role, mount string) Opts { return func(v *vault) { - addr := os.Getenv("VAULT_ADDR") - role := os.Getenv("DRONE_VAULT_KUBERNETES_ROLE") - mount := os.Getenv("DRONE_VAULT_AUTH_MOUNT_POINT") - jwtFile := "/var/run/secrets/kubernetes.io/serviceaccount/token" - token, ttl, err := getKubernetesToken(addr, role, mount, jwtFile) - if err != nil { - logrus.Debugf("vault: failed to obtain token via kubernetes-auth backend: %s", err) - return - } - - v.client.SetToken(token) - v.ttl = ttl - v.renew = ttl / 2 + v.kubeAuth.addr = addr + v.kubeAuth.role = role + v.kubeAuth.mount = mount } } diff --git a/plugins/secrets/vault/opts_test.go b/plugins/secrets/vault/opts_test.go index 217a98892..79f01160f 100644 --- a/plugins/secrets/vault/opts_test.go +++ b/plugins/secrets/vault/opts_test.go @@ -26,3 +26,21 @@ func TestWithRenewal(t *testing.T) { t.Errorf("Want renewal %v, got %v", want, got) } } + +func TestWithKubernetesAuth(t *testing.T) { + v := new(vault) + addr := "https://address.fake" + role := "fakeRole" + mount := "kubernetes" + opt := WithKubernetesAuth(addr, role, mount) + opt(v) + if got, want := v.kubeAuth.addr, addr; got != want { + t.Errorf("Want addr %v, got %v", want, got) + } + if got, want := v.kubeAuth.role, role; got != want { + t.Errorf("Want role %v, got %v", want, got) + } + if got, want := v.kubeAuth.mount, mount; got != want { + t.Errorf("Want mount %v, got %v", want, got) + } +} diff --git a/plugins/secrets/vault/vault.go b/plugins/secrets/vault/vault.go index 2ec801158..ebad6859b 100644 --- a/plugins/secrets/vault/vault.go +++ b/plugins/secrets/vault/vault.go @@ -41,11 +41,17 @@ type vaultConfig struct { } type vault struct { - store model.ConfigStore - client *api.Client - ttl time.Duration - renew time.Duration - done chan struct{} + store model.ConfigStore + client *api.Client + ttl time.Duration + renew time.Duration + auth string + kubeAuth kubeAuth + done chan struct{} +} + +type kubeAuth struct { + addr, role, mount string } // New returns a new store with secrets loaded from vault. @@ -61,10 +67,34 @@ func New(store model.ConfigStore, opts ...Opts) (secrets.Plugin, error) { for _, opt := range opts { opt(v) } + if v.auth == "kubernetes" { + err = v.initKubernetes() + if err != nil { + return nil, err + } + } v.start() // start the refresh process. return v, nil } +func (v *vault) initKubernetes() error { + token, ttl, err := getKubernetesToken( + v.kubeAuth.addr, + v.kubeAuth.role, + v.kubeAuth.mount, + "/var/run/secrets/kubernetes.io/serviceaccount/token", + ) + if err != nil { + logrus.Debugf("vault: failed to obtain token via kubernetes-auth backend: %s", err) + return err + } + + v.client.SetToken(token) + v.ttl = ttl + v.renew = ttl / 2 + return nil +} + func (v *vault) SecretListBuild(repo *model.Repo, build *model.Build) ([]*model.Secret, error) { return v.list(repo, build) } From 187d1d65bbba6afb541484e94a169f81a4ccc874 Mon Sep 17 00:00:00 2001 From: Matt Leung Date: Fri, 27 Apr 2018 14:50:47 -0700 Subject: [PATCH 4/4] add additional opt to set v.auth --- cmd/drone-server/server.go | 2 +- plugins/secrets/vault/opts.go | 10 +++++++++- plugins/secrets/vault/opts_test.go | 10 ++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cmd/drone-server/server.go b/cmd/drone-server/server.go index 2729e6a43..fa689212e 100644 --- a/cmd/drone-server/server.go +++ b/cmd/drone-server/server.go @@ -179,7 +179,7 @@ var flags = []cli.Flag{ EnvVar: "DRONE_VAULT_AUTH_TYPE", Name: "drone-vault-auth-type", Usage: "auth backend type used for connecting to vault", - Value: "token", + Value: "", }, cli.StringFlag{ EnvVar: "DRONE_VAULT_AUTH_MOUNT_POINT", diff --git a/plugins/secrets/vault/opts.go b/plugins/secrets/vault/opts.go index 081857854..7b4eaa7eb 100644 --- a/plugins/secrets/vault/opts.go +++ b/plugins/secrets/vault/opts.go @@ -25,9 +25,17 @@ func WithRenewal(d time.Duration) Opts { } } +// WithAuth returns an options that sets the vault +// method to use for authentication +func WithAuth(method string) Opts { + return func(v *vault) { + v.auth = method + } +} + // WithKubernetes returns an options that sets // kubernetes-auth parameters required to retrieve -// an initial Vault token +// an initial vault token func WithKubernetesAuth(addr, role, mount string) Opts { return func(v *vault) { v.kubeAuth.addr = addr diff --git a/plugins/secrets/vault/opts_test.go b/plugins/secrets/vault/opts_test.go index 79f01160f..873907151 100644 --- a/plugins/secrets/vault/opts_test.go +++ b/plugins/secrets/vault/opts_test.go @@ -27,6 +27,16 @@ func TestWithRenewal(t *testing.T) { } } +func TestWithAuth(t *testing.T) { + v := new(vault) + method := "kubernetes" + opt := WithAuth(method) + opt(v) + if got, want := v.auth, method; got != want { + t.Errorf("Want auth %v, got %v", want, got) + } +} + func TestWithKubernetesAuth(t *testing.T) { v := new(vault) addr := "https://address.fake"