diff --git a/cmd/drone-server/server.go b/cmd/drone-server/server.go index f36527bd1..fa689212e 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: "", + }, + 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..f289a8aff --- /dev/null +++ b/plugins/secrets/vault/kubernetes.go @@ -0,0 +1,51 @@ +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, 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, mount) + 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) + if err != nil { + return "", 0, err + } + + 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..7b4eaa7eb 100644 --- a/plugins/secrets/vault/opts.go +++ b/plugins/secrets/vault/opts.go @@ -24,3 +24,22 @@ func WithRenewal(d time.Duration) Opts { v.renew = d } } + +// 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 +func WithKubernetesAuth(addr, role, mount string) Opts { + return func(v *vault) { + 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..873907151 100644 --- a/plugins/secrets/vault/opts_test.go +++ b/plugins/secrets/vault/opts_test.go @@ -26,3 +26,31 @@ func TestWithRenewal(t *testing.T) { t.Errorf("Want renewal %v, got %v", want, got) } } + +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" + 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) }