From 79428aa2318ff960e70cda1af7a4a3b83dd95269 Mon Sep 17 00:00:00 2001 From: Matt Leung Date: Tue, 24 Apr 2018 14:48:50 -0700 Subject: [PATCH] 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 f36527bd..2729e6a4 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 00000000..1e3abd12 --- /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 00000000..ca23021d --- /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 00000000..1e1fbede --- /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 e2833aaa..037071ea 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 + } +}