From f0d6f0bfadd312debc3f0fdb2a1a4a031735e53d Mon Sep 17 00:00:00 2001 From: Adam Talbot Date: Sun, 17 Jun 2018 20:47:34 +0100 Subject: [PATCH] Add tests, docs and update Dockerfile --- .travis.yml | 14 +++- Dockerfile | 20 +++-- README.md | 49 ++++++++++++ cmd/k8s-vault-csr/main.go | 12 +-- deploy.yaml | 61 +++++++++++++++ pkg/vault/token/renewer.go | 15 ++-- pkg/vault/token/renewer_test.go | 108 ++++++++++++++++++++++++++ pkg/vault/token/{type.go => types.go} | 1 - 8 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 deploy.yaml create mode 100644 pkg/vault/token/renewer_test.go rename pkg/vault/token/{type.go => types.go} (93%) diff --git a/.travis.yml b/.travis.yml index 445d522..4a2914f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,4 +22,16 @@ jobs: - stage: test script: make test - stage: build - script: make bin/darwin_amd64/k8s-vault-csr bin/linux_amd64/k8s-vault-csr bin/linux_arm64/k8s-vault-csr bin/linux_arm/k8s-vault-csr \ No newline at end of file + script: make bin/darwin_amd64/k8s-vault-csr bin/linux_amd64/k8s-vault-csr bin/linux_arm64/k8s-vault-csr bin/linux_arm/k8s-vault-csr + deploy: + provider: releases + api_key: + secure: "GpcAkFWnFtCMCbQCW/KUbsfJVyp97WWzdigcrMD1yTqz2g4Yv4/ElbUT6UG7s90JhYlMtislaKuI02Z6hUnCcPde8M/Lu06gxgW67FZ2GAI/xFHFIh5qXkjiC4y/77JkUjHcFSc/54TiN35i49U7GlKHJZU289Svr5CC+7c69HuZ37pPkflRYagUnghCZ0tK4pzxVk3ZyIwvoiswmp0eLysz+Ng8azGxdbRFNOyYAP3V3pOyH3xuuhjQ/3iJ5p/PyDTmKZWt7UCcGW5vVEuHfrjUyxm/mF388tIkNwOvKJn2bVcm01QesS4jbYxvk8krg2F0mW0uvg9PaWojkuWsNLh0k7ZuHiTm+KcHFMXFc3cgGHP7EQtN7UfjQj59LUC3fI3iRbVaKeg9wexPFqh05Ompe3ls5krsK27TvYBxNFqRPbO18+DWQIicZSvWT/wPAcxbWbd35JHWJysAS7vDhiTPWLCsFV1yEPjKGs4tDH1AvE3LZ9tBNYspNfbnYr2fkYYCbCxEb2iOUGJ8SDL3XDXFxb1hzGV3J2XzBbjgHwSPXsE46aVG5S/58akCGSfdMVVNzmuVj1PwXpLpLkkGeKHMQqN5ZwwuiNYJOlspcf4JIBsfXNTS7HcCih9pVTDY4Xj2JnRXT5WmHXXZYXNCDzEoT48asBZI+9cXHD4ZGMg=" + file: + - bin/darwin_amd64/k8s-vault-csr + - bin/linux_amd64/k8s-vault-csr + - bin/linux_arm64/k8s-vault-csr + - bin/linux_arm/k8s-vault-csr + skip_cleanup: true + on: + tags: true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 17af4f2..b4cb868 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,19 @@ -FROM golang:1.10.3-alpine as build +FROM golang:1.10.3 as build -RUN apk add --no-cache git openssh ca-certificates -RUN wget -O - https://raw.githubusercontent.com/golang/dep/master/install.sh | sh +ARG DEP_VERSION=0.4.1 + +# install dep +RUN wget -O /usr/local/bin/dep https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 && \ + chmod +x /usr/local/bin/dep + +# add code ADD . /go/src/github.com/thatsmrtalbot/k8s-vault-csr WORKDIR /go/src/github.com/thatsmrtalbot/k8s-vault-csr -RUN dep ensure -vendor-only -v -RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o /bin/k8s-vault-csr ./cmd/k8s-vault-csr -FROM scratch +# vendor and build +RUN make bin/linux_amd64/k8s-vault-csr USE_DOCKER=0 VENDOR_ONLY=1 -COPY --from=build /bin/k8s-vault-csr /bin/k8s-vault-csr +# final container +FROM scratch +COPY --from=build /go/src/github.com/thatsmrtalbot/k8s-vault-csr/bin/linux_amd64/k8s-vault-csr /bin/k8s-vault-csr ENTRYPOINT [ "/bin/k8s-vault-csr" ] \ No newline at end of file diff --git a/README.md b/README.md index cc8ed26..56cb14e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/ThatsMrTalbot/k8s-vault-csr.svg?branch=master)](https://travis-ci.org/ThatsMrTalbot/k8s-vault-csr) + # Kubernetes CSR Vault Controller Kubernetes supports the approval and signing of x509 Certificate Signing Requests. This can be used internally by Kubernetes for things such as kubelet client certificate rotation. Typically CSRs are signed by the controller-manager with a provided CA and key. @@ -12,3 +14,50 @@ This controller uses much of the same code as the default Kubernetes CSR signer, The latest release of Vault (0.10.2) does not allow a client to specify "key usage" or "extended key usage" when using the `sign-verbatim` endpoint. This has been solved in master (https://github.com/hashicorp/vault/pull/4777) and should be in Vault 0.10.3 +## Installing + +`k8s-vault-csr` can run in cluster or standalone. The fastest path is to run in cluster: + +- The default `csrsigning` controller first need disabling in the controller manager. This can be done through the command line flag `--controllers`, for example `--controllers=-csrsigning` +- If you want to use kubernetes auth in vault then this needs setting up, the signer needs permission to call `/pki/sign-verbatim/role` where `pki` and `role` are the pki mount and role respectively. +- Deploy `kube-vault-signer` and RBAC. See `deploy.yaml` for an example. + +## Flags + +``` +Usage of k8s-vault-csr: + -alsologtostderr + log to standard error as well as files + -kubeconfig string + kubeconfig file to use + -log_backtrace_at value + when logging hits line file:N, emit a stack trace + -log_dir string + If non-empty, write log files in this directory + -logtostderr + log to standard error instead of files + -master string + kubernetes mastere url + -mount string + specify the pki mount to use to generate certificates (default "pki") + -role string + specify role to use, only ttl is used from the role + -service-token-file string + file to load service token from (default "/var/run/secrets/kubernetes.io/serviceaccount") + -service-token-mount string + name of the kubernetes auth mount in vault (default "kubernetes") + -service-token-role string + role to use when authenticating with vault using the service token + -stderrthreshold value + logs at or above this threshold go to stderr + -use-service-token + use service token vault authentication + -v value + log level for V logs + -vault-address string + vault server address + -vmodule value + comma-separated list of pattern=N settings for file-filtered logging + -workers int + number of workers to run (default 4) +``` \ No newline at end of file diff --git a/cmd/k8s-vault-csr/main.go b/cmd/k8s-vault-csr/main.go index 398aa52..a7f400e 100644 --- a/cmd/k8s-vault-csr/main.go +++ b/cmd/k8s-vault-csr/main.go @@ -23,13 +23,13 @@ var ( masterAddr = flag.String("master", "", "kubernetes mastere url") kubeconfig = flag.String("kubeconfig", "", "kubeconfig file to use") vaultAddr = flag.String("vault-address", "", "vault server address") - useServiceToken = flag.Bool("use-service-token", false, "use service token vault authentication") - serviceTokenMount = flag.String("service-token-mount", "kubernetes", "name of the kubernetes auth mount in vault") - serviceTokenRole = flag.String("service-token-role", "", "role to use when authenticating with vault using the service token") + useServiceToken = flag.Bool("use-kubernetes-auth", false, "use service token vault authentication") + serviceTokenMount = flag.String("kubernetes-auth-mount", "kubernetes", "name of the kubernetes auth mount in vault") + serviceTokenRole = flag.String("kubernetes-auth-role", "", "role to use when authenticating with vault using the service token") serviceTokenFile = flag.String("service-token-file", "/var/run/secrets/kubernetes.io/serviceaccount", "file to load service token from") - workers = flag.Int("workers", 4, "number of workers to run") - pkiMount = flag.String("mount", "pki", "specify the pki mount to use to generate certificates") - pkiRole = flag.String("role", "", "specify role to use, only ttl is used from the role") + workers = flag.Int("signer-workers", 4, "number of workers to run") + pkiMount = flag.String("vault-pki-mount", "pki", "specify the pki mount to use to generate certificates") + pkiRole = flag.String("vault-pki-role", "", "specify role to use, only ttl is used from the role") ) func init() { diff --git a/deploy.yaml b/deploy.yaml new file mode 100644 index 0000000..e64c81e --- /dev/null +++ b/deploy.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kube-vault-signer + namespace: kube-system +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: kube-vault-signer +rules: +- apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests + - certificatesigningrequests/status + verbs: + - get + - list + - watch + - patch + - update +--- +kind: CluterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: kube-vault-signer +subjects: +- kind: ServiceAccount + name: kube-vault-signer + namespace: kube-system +roleRef: + kind: ClusterRole + name: kube-vault-signer + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kube-vault-signer + namespace: kube-system + labels: + k8s-app: kube-vault-signer +spec: + replicas: 1 + selector: + matchLabels: + k8s-app: kube-vault-signer + template: + metadata: + labels: + k8s-app: kube-vault-signer + spec: + serviceAccountName: kube-vault-signer + containers: + - name: kube-vault-signer + image: thatsmrtalbot/kube-vault-signer:0.0.1 + args: + - -vault-address=https://vault.example.com + - -use-kubernetes-auth=true + - -kubernetes-auth-role=kube-vault-signer diff --git a/pkg/vault/token/renewer.go b/pkg/vault/token/renewer.go index fbae890..ec389a8 100644 --- a/pkg/vault/token/renewer.go +++ b/pkg/vault/token/renewer.go @@ -2,6 +2,7 @@ package token import ( "time" + "encoding/json" "github.com/golang/glog" "github.com/pkg/errors" "github.com/hashicorp/vault/api" @@ -10,6 +11,8 @@ import ( var ErrNoAuthProvider = errors.New("no vault authentication method provided") +var infinity = time.Duration(^uint64(0) >> 1) + func NewRenewer(client *api.Client, authFn func(*api.Client) error) *Renewer { if authFn == nil { authFn = func(*api.Client) error { @@ -20,7 +23,6 @@ func NewRenewer(client *api.Client, authFn func(*api.Client) error) *Renewer { return &Renewer{ client: client, authFn: authFn, - err: make(chan error), } } @@ -41,6 +43,11 @@ func (r *Renewer) currentTokenStatus() (*tokenStatus, error) { return nil, err } + ttl, err := secret.Data["ttl"].(json.Number).Int64() + if err != nil { + return nil, err + } + if time.Now().UTC().After(expires) { return &tokenStatus{ HasToken: true, @@ -51,7 +58,7 @@ func (r *Renewer) currentTokenStatus() (*tokenStatus, error) { return &tokenStatus{ HasToken: true, ExpiresIn: time.Now().UTC().Sub(expires), - TTL: time.Duration(secret.Data["creation_ttl"].(int)) * time.Second, + TTL: time.Duration(ttl) * time.Second, }, nil } @@ -89,10 +96,6 @@ func (r *Renewer) tick() error { return nil } -func (r *Renewer) Error() <-chan error { - return r.err -} - func (r *Renewer) Run(done <-chan struct{}) error { ticker := time.NewTicker(time.Second) for { diff --git a/pkg/vault/token/renewer_test.go b/pkg/vault/token/renewer_test.go new file mode 100644 index 0000000..c788e57 --- /dev/null +++ b/pkg/vault/token/renewer_test.go @@ -0,0 +1,108 @@ +package token + +import ( + "context" + "testing" + + log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/logging" + "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/physical/inmem" + "github.com/hashicorp/vault/vault" +) + +func TestRenewer(t *testing.T) { + // Set up vault + + logger := logging.NewVaultLogger(log.Trace) + + phys, err := inmem.NewInmem(nil, logger) + if err != nil { + t.Fatal(err) + return + } + + core, err := vault.NewCore(&vault.CoreConfig{ + Physical: phys, + LogicalBackends: map[string]logical.Factory{}, + DisableMlock: true, + }) + + if err != nil { + t.Fatal("error initializing core: ", err) + return + } + + init, err := core.Initialize(context.Background(), &vault.InitParams{ + BarrierConfig: &vault.SealConfig{ + SecretShares: 1, + SecretThreshold: 1, + }, + RecoveryConfig: nil, + }) + + if err != nil { + t.Fatal("error initializing core: ", err) + return + } + + if unsealed, err := core.Unseal(init.SecretShares[0]); err != nil { + t.Fatal("error unsealing core: ", err) + return + } else if !unsealed { + t.Fatal("vault shouldn't be sealed") + return + } + + ln, addr := http.TestServer(nil, core) + defer ln.Close() + + clientConfig := api.DefaultConfig() + clientConfig.Address = addr + client, err := api.NewClient(clientConfig) + + if err != nil { + t.Fatal("error initializing HTTP client: ", err) + return + } + + client.SetToken(init.RootToken) + + // Set token + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + TTL: "3600", + }) + + if err != nil { + t.Fatal("error creating child token: ", err) + return + } + + client.SetToken(secret.Auth.ClientToken) + + // Test case + + renewer := NewRenewer(client, nil) + status, err := renewer.currentTokenStatus() + + if err != nil { + t.Errorf("error getting token status: %s", err) + } + + if !status.HasToken { + t.Error("no token") + } + + if status.Expired { + t.Error("token is expired") + } + + err = renewer.renew() + + if err != nil { + t.Errorf("error renewing token: %s", err) + } +} \ No newline at end of file diff --git a/pkg/vault/token/type.go b/pkg/vault/token/types.go similarity index 93% rename from pkg/vault/token/type.go rename to pkg/vault/token/types.go index 4d322f8..30462be 100644 --- a/pkg/vault/token/type.go +++ b/pkg/vault/token/types.go @@ -14,7 +14,6 @@ type tokenStatus struct { } type Renewer struct { - err chan error client *api.Client authFn func(*api.Client) error } \ No newline at end of file