diff --git a/Makefile b/Makefile index 74b18e461..ebb626e35 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,11 @@ DOCKER_CLI_EXPERIMENTAL ?= enabled ## -------------------------------------- ## Testing ## -------------------------------------- +mock_gen: + mkdir -p proto/agent/mocks + mockgen sigs.k8s.io/apiserver-network-proxy/proto/agent AgentService_ConnectServer > proto/agent/mocks/agent_mock.go + cat hack/go-license-header.txt proto/agent/mocks/agent_mock.go > proto/agent/mocks/agent_mock.licensed.go + mv proto/agent/mocks/agent_mock.licensed.go proto/agent/mocks/agent_mock.go .PHONY: test test: @@ -68,7 +73,7 @@ bin/proxy-server: bin cmd/proxy/main.go proto/agent/agent.pb.go proto/proxy.pb.g ## -------------------------------------- .PHONY: gen -gen: proto/agent/agent.pb.go proto/proxy.pb.go +gen: proto/agent/agent.pb.go proto/proxy.pb.go mock_gen proto/agent/agent.pb.go: proto/agent/agent.proto protoc -I proto proto/agent/agent.proto --go_out=plugins=grpc:proto @@ -244,4 +249,4 @@ release-alias-tag: # Adds the tag to the last build tag. BASE_REF comes from the .PHONY: clean clean: - rm -rf proto/agent/agent.pb.go proto/proxy.pb.go easy-rsa.tar.gz easy-rsa-master cfssl cfssljson certs bin + rm -rf proto/agent/agent.pb.go proto/proxy.pb.go easy-rsa.tar.gz easy-rsa-master cfssl cfssljson certs bin proto/agent/mocks diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 76fdfd685..81062032f 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -68,16 +68,20 @@ type GrpcProxyAgentOptions struct { syncInterval time.Duration probeInterval time.Duration reconnectInterval time.Duration + + // file contains service account authorization token for enabling proxy-server token based authorization + serviceAccountTokenPath string } func (o *GrpcProxyAgentOptions) ClientSetConfig(dialOption grpc.DialOption) *agentclient.ClientSetConfig { return &agentclient.ClientSetConfig{ - Address: fmt.Sprintf("%s:%d", o.proxyServerHost, o.proxyServerPort), - AgentID: o.agentID, - SyncInterval: o.syncInterval, - ProbeInterval: o.probeInterval, - ReconnectInterval: o.reconnectInterval, - DialOption: dialOption, + Address: fmt.Sprintf("%s:%d", o.proxyServerHost, o.proxyServerPort), + AgentID: o.agentID, + SyncInterval: o.syncInterval, + ProbeInterval: o.probeInterval, + ReconnectInterval: o.reconnectInterval, + DialOption: dialOption, + ServiceAccountTokenPath: o.serviceAccountTokenPath, } } @@ -92,6 +96,7 @@ func (o *GrpcProxyAgentOptions) Flags() *pflag.FlagSet { flags.DurationVar(&o.syncInterval, "sync-interval", o.syncInterval, "The interval by which the agent periodically checks that it has connections to all instances of the proxy server.") flags.DurationVar(&o.probeInterval, "probe-interval", o.probeInterval, "The interval by which the agent periodically checks if its connections to the proxy server are ready.") flags.DurationVar(&o.reconnectInterval, "reconnect-interval", o.reconnectInterval, "The interval by which the agent tries to reconnect.") + flags.StringVar(&o.serviceAccountTokenPath, "service-account-token-path", o.serviceAccountTokenPath, "If non-empty proxy agent uses this token to prove its identity to the proxy server.") return flags } @@ -105,6 +110,7 @@ func (o *GrpcProxyAgentOptions) Print() { klog.Warningf("SyncInterval set to %v.\n", o.syncInterval) klog.Warningf("ProbeInterval set to %v.\n", o.probeInterval) klog.Warningf("ReconnectInterval set to %v.\n", o.reconnectInterval) + klog.Warningf("ServiceAccountTokenPath set to \"%s\".\n", o.serviceAccountTokenPath) } func (o *GrpcProxyAgentOptions) Validate() error { @@ -132,20 +138,26 @@ func (o *GrpcProxyAgentOptions) Validate() error { if o.proxyServerPort <= 0 { return fmt.Errorf("proxy server port %d must be greater than 0", o.proxyServerPort) } + if o.serviceAccountTokenPath != "" { + if _, err := os.Stat(o.serviceAccountTokenPath); os.IsNotExist(err) { + return fmt.Errorf("error checking service account token path %s, got %v", o.serviceAccountTokenPath, err) + } + } return nil } func newGrpcProxyAgentOptions() *GrpcProxyAgentOptions { o := GrpcProxyAgentOptions{ - agentCert: "", - agentKey: "", - caCert: "", - proxyServerHost: "127.0.0.1", - proxyServerPort: 8091, - agentID: uuid.New().String(), - syncInterval: 5 * time.Second, - probeInterval: 5 * time.Second, - reconnectInterval: 5 * time.Second, + agentCert: "", + agentKey: "", + caCert: "", + proxyServerHost: "127.0.0.1", + proxyServerPort: 8091, + agentID: uuid.New().String(), + syncInterval: 5 * time.Second, + probeInterval: 5 * time.Second, + reconnectInterval: 5 * time.Second, + serviceAccountTokenPath: "", } return &o } diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 9bec9fe46..f97bbacaf 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -29,14 +29,15 @@ import ( "os/signal" "syscall" - "k8s.io/klog" - "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" "github.com/spf13/pflag" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "k8s.io/klog" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/apiserver-network-proxy/pkg/agent/agentserver" "sigs.k8s.io/apiserver-network-proxy/pkg/util" "sigs.k8s.io/apiserver-network-proxy/proto/agent" @@ -86,6 +87,14 @@ type ProxyRunOptions struct { serverID string // Number of proxy server instances, should be 1 unless it is a HA proxy server. serverCount uint + // Agent pod's namespace for token-based agent authentication + agentNamespace string + // Agent pod's service account for token-based agent authentication + agentServiceAccount string + // Token's audience for token-based agent authentication + authenticationAudience string + // Path to kubeconfig (used by kubernetes client) + kubeconfigPath string } func (o *ProxyRunOptions) Flags() *pflag.FlagSet { @@ -103,6 +112,10 @@ func (o *ProxyRunOptions) Flags() *pflag.FlagSet { flags.UintVar(&o.adminPort, "admin-port", o.adminPort, "Port we listen for admin connections on.") flags.StringVar(&o.serverID, "server-id", o.serverID, "The unique ID of this server.") flags.UintVar(&o.serverCount, "server-count", o.serverCount, "The number of proxy server instances, should be 1 unless it is an HA server.") + flags.StringVar(&o.agentNamespace, "agent-namespace", o.agentNamespace, "Expected agent's namespace during agent authentication (used with agent-service-account, authentication-audience, kubeconfig).") + flags.StringVar(&o.agentServiceAccount, "agent-service-account", o.agentServiceAccount, "Expected agent's service account during agent authentication (used with agent-namespace, authentication-audience, kubeconfig).") + flags.StringVar(&o.kubeconfigPath, "kubeconfig", o.kubeconfigPath, "absolute path to the kubeconfig file (used with agent-namespace, agent-service-account, authentication-audience).") + flags.StringVar(&o.authenticationAudience, "authentication-audience", o.authenticationAudience, "Expected agent's token authentication audience (used with agent-namespace, agent-service-account, kubeconfig).") return flags } @@ -120,6 +133,10 @@ func (o *ProxyRunOptions) Print() { klog.Warningf("Admin port set to %d.\n", o.adminPort) klog.Warningf("ServerID set to %s.\n", o.serverID) klog.Warningf("ServerCount set to %d.\n", o.serverCount) + klog.Warningf("AgentNamespace set to %q.\n", o.agentNamespace) + klog.Warningf("AgentServiceAccount set to %q.\n", o.agentServiceAccount) + klog.Warningf("AuthenticationAudience set to %q.\n", o.authenticationAudience) + klog.Warningf("KubeconfigPath set to %q.\n", o.kubeconfigPath) } func (o *ProxyRunOptions) Validate() error { @@ -202,24 +219,48 @@ func (o *ProxyRunOptions) Validate() error { if o.adminPort < 1024 { return fmt.Errorf("please do not try to use reserved port %d for the admin port", o.adminPort) } + + // validate agent authentication params + // all 4 parametes must be empty or must have value (except kubeconfigPath that might be empty) + if o.agentNamespace != "" || o.agentServiceAccount != "" || o.authenticationAudience != "" || o.kubeconfigPath != "" { + if o.agentNamespace == "" { + return fmt.Errorf("agentNamespace cannot be empty when agent authentication is enabled") + } + if o.agentServiceAccount == "" { + return fmt.Errorf("agentServiceAccount cannot be empty when agent authentication is enabled") + } + if o.authenticationAudience == "" { + return fmt.Errorf("authenticationAudience cannot be empty when agent authentication is enabled") + } + if o.kubeconfigPath != "" { + if _, err := os.Stat(o.kubeconfigPath); os.IsNotExist(err) { + return fmt.Errorf("error checking kubeconfigPath %q, got %v", o.kubeconfigPath, err) + } + } + } + return nil } func newProxyRunOptions() *ProxyRunOptions { o := ProxyRunOptions{ - serverCert: "", - serverKey: "", - serverCaCert: "", - clusterCert: "", - clusterKey: "", - clusterCaCert: "", - mode: "grpc", - udsName: "", - serverPort: 8090, - agentPort: 8091, - adminPort: 8092, - serverID: uuid.New().String(), - serverCount: 1, + serverCert: "", + serverKey: "", + serverCaCert: "", + clusterCert: "", + clusterKey: "", + clusterCaCert: "", + mode: "grpc", + udsName: "", + serverPort: 8090, + agentPort: 8091, + adminPort: 8092, + serverID: uuid.New().String(), + serverCount: 1, + agentNamespace: "", + agentServiceAccount: "", + kubeconfigPath: "", + authenticationAudience: "", } return &o } @@ -247,7 +288,29 @@ func (p *Proxy) run(o *ProxyRunOptions) error { return fmt.Errorf("failed to validate server options with %v", err) } ctx, cancel := context.WithCancel(context.Background()) - server := agentserver.NewProxyServer(o.serverID, int(o.serverCount)) + defer cancel() + + var k8sClient *kubernetes.Clientset + if o.agentNamespace != "" { + config, err := clientcmd.BuildConfigFromFlags("", o.kubeconfigPath) + if err != nil { + return fmt.Errorf("failed to load kubernetes client config: %v", err) + } + + k8sClient, err = kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create kubernetes clientset: %v", err) + } + } + + authOpt := &agentserver.AgentTokenAuthenticationOptions{ + Enabled: o.agentNamespace != "", + AgentNamespace: o.agentNamespace, + AgentServiceAccount: o.agentServiceAccount, + KubernetesClient: k8sClient, + AuthenticationAudience: o.authenticationAudience, + } + server := agentserver.NewProxyServer(o.serverID, int(o.serverCount), authOpt) klog.Info("Starting master server for client connections.") masterStop, err := p.runMasterServer(ctx, o, server) @@ -274,7 +337,6 @@ func (p *Proxy) run(o *ProxyRunOptions) error { if masterStop != nil { masterStop() } - cancel() return nil } diff --git a/examples/kubernetes/README.md b/examples/kubernetes/README.md index 65903e13a..b4c440a25 100644 --- a/examples/kubernetes/README.md +++ b/examples/kubernetes/README.md @@ -20,16 +20,42 @@ KUBIA_IP=$(kubectl get svc kubia -o=jsonpath='{.spec.clusterIP}') PROXY_IMAGE=$(docker images | grep "proxy-server-" -m1 | awk '{print $1}') AGENT_IMAGE=$(docker images | grep "proxy-agent-" -m1 | awk '{print $1}') TEST_CLIENT_IMAGE=$(docker images | grep "proxy-test-client-" -m1 | awk '{print $1}') +SERVER_TOKEN=$(./examples/kubernetes/token_generation.sh 32) CLUSTER_CERT= CLUSTER_KEY= ``` -#### GKE specific configuration +#### GCE sample configuration ```bash CLUSTER_CERT=/etc/srv/kubernetes/pki/apiserver.crt CLUSTER_KEY=/etc/srv/kubernetes/pki/apiserver.key ``` +# Register SERVER_TOKEN in [static-token-file](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#static-token-file) +Append the output of the following line to the [static-token-file](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#static-token-file) and restart **kube-apiserver** on the master +```bash +echo "${SERVER_TOKEN},system:konnectivity-server,uid:system:konnectivity-server" +``` + +#### GCE sample configuration +1. [static-token-file](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#static-token-file) location is: **/etc/srv/kubernetes/known_tokens.csv** + +1. Restart kube-apiserver +```bash +K8S_API_PID=$(sudo crictl ps | grep kube-apiserver | awk '{ print $1; }') +sudo crictl stop ${K8S_API_PID} +``` + +# Save following config at /etc/srv/kubernetes/konnectivity-server/kubeconfig on master VM +```bash +SERVER_TOKEN=${SERVER_TOKEN} envsubst < examples/kubernetes/kubeconfig +``` + +# Create a clusterrolebinding allowing proxy-server authenticate proxy-client +```bash +kubectl create clusterrolebinding --user system:konnectivity-server --clusterrole system:auth-delegator system:konnectivity-server +``` + # Start **proxy-server** as a [static pod](https://kubernetes.io/docs/tasks/configure-pod-container/static-pod/) with following configuration ```bash TAG=${TAG} PROXY_IMAGE=${PROXY_IMAGE} CLUSTER_CERT=${CLUSTER_CERT} CLUSTER_KEY=${CLUSTER_KEY} envsubst < examples/kubernetes/konnectivity-server.yaml diff --git a/examples/kubernetes/konnectivity-agent.yaml b/examples/kubernetes/konnectivity-agent.yaml index f3d0ed940..e62174024 100644 --- a/examples/kubernetes/konnectivity-agent.yaml +++ b/examples/kubernetes/konnectivity-agent.yaml @@ -1,8 +1,14 @@ apiVersion: v1 +kind: ServiceAccount +metadata: + name: konnectivity-agent + namespace: kube-system +--- +apiVersion: v1 kind: Pod metadata: name: konnectivity-agent - namespace: default + namespace: kube-system annotations: scheduler.alpha.kubernetes.io/critical-pod: '' seccomp.security.alpha.kubernetes.io/pod: 'docker/default' @@ -20,6 +26,7 @@ spec: "--ca-cert=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "--proxy-server-host=${CLUSTER_IP}", "--proxy-server-port=8091", + "--service-account-token-path=/var/run/secrets/tokens/konnectivity-agent-token", ] livenessProbe: httpGet: @@ -33,3 +40,14 @@ spec: limits: cpu: 50m memory: 30Mi + volumeMounts: + - mountPath: /var/run/secrets/tokens + name: konnectivity-agent-token + serviceAccountName: konnectivity-agent + volumes: + - name: konnectivity-agent-token + projected: + sources: + - serviceAccountToken: + path: konnectivity-agent-token + audience: system:konnectivity-server \ No newline at end of file diff --git a/examples/kubernetes/konnectivity-server.yaml b/examples/kubernetes/konnectivity-server.yaml index 343f3aed7..5a89bcb6f 100644 --- a/examples/kubernetes/konnectivity-server.yaml +++ b/examples/kubernetes/konnectivity-server.yaml @@ -19,13 +19,17 @@ spec: "--log-file=/var/log/konnectivity-server.log", "--logtostderr=false", "--log-file-max-size=0", - "--uds-name=/etc/srv/kubernetes/konnectivity/konnectivity-server.socket", + "--uds-name=/etc/srv/kubernetes/konnectivity-server/konnectivity-server.socket", "--cluster-cert=/etc/srv/kubernetes/pki/apiserver.crt", "--cluster-key=/etc/srv/kubernetes/pki/apiserver.key", "--server-port=0", "--agent-port=8091", "--admin-port=8092", - "--mode=http-connect" + "--mode=http-connect", + "--agent-namespace=kube-system", + "--agent-service-account=konnectivity-agent", + "--kubeconfig=/etc/srv/kubernetes/konnectivity-server/kubeconfig", + "--authentication-audience=system:konnectivity-server", ] livenessProbe: httpGet: @@ -53,7 +57,7 @@ spec: mountPath: /etc/srv/kubernetes/pki readOnly: true - name: konnectivity-home - mountPath: /etc/srv/kubernetes/konnectivity + mountPath: /etc/srv/kubernetes/konnectivity-server volumes: - name: varlogkonnectivityserver hostPath: @@ -64,6 +68,5 @@ spec: path: /etc/srv/kubernetes/pki - name: konnectivity-home hostPath: - path: /etc/srv/kubernetes/konnectivity + path: /etc/srv/kubernetes/konnectivity-server type: DirectoryOrCreate - diff --git a/examples/kubernetes/konnectivity-test-client.yaml b/examples/kubernetes/konnectivity-test-client.yaml index d274c9de7..32872c574 100644 --- a/examples/kubernetes/konnectivity-test-client.yaml +++ b/examples/kubernetes/konnectivity-test-client.yaml @@ -19,7 +19,7 @@ spec: args: [ "--log-file=/var/log/konnectivity-test-client.log", "--logtostderr=false", - "--proxy-uds=/etc/srv/kubernetes/konnectivity/konnectivity-server.socket", + "--proxy-uds=/etc/srv/kubernetes/konnectivity-server/konnectivity-server.socket", "--proxy-host=", "--proxy-port=0", "--mode=http-connect", @@ -31,7 +31,7 @@ spec: mountPath: /var/log/konnectivity-test-client.log readOnly: false - name: konnectivity-home - mountPath: /etc/srv/kubernetes/konnectivity + mountPath: /etc/srv/kubernetes/konnectivity-server volumes: - name: konnectivity-test-log hostPath: @@ -39,5 +39,5 @@ spec: type: FileOrCreate - name: konnectivity-home hostPath: - path: /etc/srv/kubernetes/konnectivity + path: /etc/srv/kubernetes/konnectivity-server type: DirectoryOrCreate diff --git a/examples/kubernetes/kubeconfig b/examples/kubernetes/kubeconfig new file mode 100644 index 000000000..24b0fd019 --- /dev/null +++ b/examples/kubernetes/kubeconfig @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Config +users: +- name: konnectivity-server + user: + token: ${SERVER_TOKEN} +clusters: +- name: local + cluster: + insecure-skip-tls-verify: true + server: https://localhost:443 +contexts: +- context: + cluster: local + user: konnectivity-server + name: konnectivity-server +current-context: konnectivity-server diff --git a/examples/kubernetes/token_generation.sh b/examples/kubernetes/token_generation.sh new file mode 100755 index 000000000..c6c122a3c --- /dev/null +++ b/examples/kubernetes/token_generation.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# https://github.com/kubernetes/kubernetes/blob/84beab6f26609527752ff441c155813b783fe145/cluster/gce/gci/configure-helper.sh#L44 +# secure_random generates a secure random string of bytes. This function accepts +# a number of secure bytes desired and returns a base64 encoded string with at +# least the requested entropy. Rather than directly reading from /dev/urandom, +# we use uuidgen which calls getrandom(2). getrandom(2) verifies that the +# entropy pool has been initialized sufficiently for the desired operation +# before reading from /dev/urandom. +# +# ARGS: +# #1: number of secure bytes to generate. We round up to the nearest factor of 32. +function secure_random { + local infobytes="${1}" + if ((infobytes <= 0)); then + echo "Invalid argument to secure_random: infobytes='${infobytes}'" 1>&2 + return 1 + fi + + local out="" + for (( i = 0; i < "${infobytes}"; i += 32 )); do + # uuids have 122 random bits, sha256 sums have 256 bits, so concatenate + # three uuids and take their sum. The sum is encoded in ASCII hex, hence the + # 64 character cut. + out+="$( + ( + uuidgen --random; + uuidgen --random; + uuidgen --random; + ) | sha256sum \ + | head -c 64 + )"; + done + # Finally, convert the ASCII hex to base64 to increase the density. + echo -n "${out}" | xxd -r -p | base64 -w 0 +} + +secure_random $1 \ No newline at end of file diff --git a/go.mod b/go.mod index 89b33ddef..9667749b8 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/beorn7/perks v1.0.0 // indirect + github.com/golang/mock v1.4.0 github.com/golang/protobuf v1.3.2 github.com/google/uuid v1.1.1 github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -11,11 +12,11 @@ require ( github.com/prometheus/common v0.4.0 // indirect github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 // indirect github.com/spf13/cobra v0.0.3 - github.com/spf13/pflag v1.0.3 - golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 // indirect - golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12 // indirect - golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 // indirect + github.com/spf13/pflag v1.0.5 + golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 google.golang.org/grpc v1.26.0 - honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc // indirect - k8s.io/klog v0.3.0 + k8s.io/api v0.17.1 + k8s.io/apimachinery v0.17.1 + k8s.io/client-go v0.17.1 + k8s.io/klog v1.0.0 ) diff --git a/go.sum b/go.sum index e97da2ea9..a8b1c740f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,17 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -7,31 +19,99 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= @@ -47,46 +127,93 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12 h1:Zw7eRv6INHGfu15LVRN1vrrwusJbnfJjAZn3D1VkQIE= golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -95,8 +222,36 @@ google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.17.1 h1:i46MidoDOE9tvQ0TTEYggf3ka/pziP1+tHI/GFVeJao= +k8s.io/api v0.17.1/go.mod h1:zxiAc5y8Ngn4fmhWUtSxuUlkfz1ixT7j9wESokELzOg= +k8s.io/apimachinery v0.17.1 h1:zUjS3szTxoUjTDYNvdFkYt2uMEXLcthcbp+7uZvWhYM= +k8s.io/apimachinery v0.17.1/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/client-go v0.17.1 h1:LbbuZ5tI7OYx4et5DfRFcJuoojvpYO0c7vps2rgJsHY= +k8s.io/client-go v0.17.1/go.mod h1:HZtHJSC/VuSHcETN9QA5QDZky1tXiYrkF/7t7vRpO1A= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0 h1:0VPpR+sizsiivjIfIAQH/rl8tan6jvWkS7lU+0di3lE= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/pkg/agent/agentclient/clientset.go b/pkg/agent/agentclient/clientset.go index 1f7a6ef5c..67867fc7f 100644 --- a/pkg/agent/agentclient/clientset.go +++ b/pkg/agent/agentclient/clientset.go @@ -46,6 +46,8 @@ type ClientSet struct { reconnectInterval time.Duration // The interval by which the agent // tries to reconnect. dialOption grpc.DialOption + // file path contains service account token + serviceAccountTokenPath string } func (cs *ClientSet) ClientsCount() int { @@ -105,6 +107,7 @@ type ClientSetConfig struct { ProbeInterval time.Duration ReconnectInterval time.Duration DialOption grpc.DialOption + ServiceAccountTokenPath string } func (cc *ClientSetConfig) NewAgentClientSet() *ClientSet { @@ -116,6 +119,7 @@ func (cc *ClientSetConfig) NewAgentClientSet() *ClientSet { probeInterval: cc.ProbeInterval, reconnectInterval: cc.ReconnectInterval, dialOption: cc.DialOption, + serviceAccountTokenPath: cc.ServiceAccountTokenPath, } } diff --git a/pkg/agent/agentclient/stream.go b/pkg/agent/agentclient/stream.go index 72338a41e..794035ae8 100644 --- a/pkg/agent/agentclient/stream.go +++ b/pkg/agent/agentclient/stream.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "io" + "io/ioutil" "strconv" "sync" "time" @@ -73,6 +74,10 @@ type RedialableAgentClient struct { reconnectInterval time.Duration // interval between recoonects probeInterval time.Duration // interval between probe pings + + // file path contains service account token. + // token's value is auto-rotated by kubernetes, based on projected volume configuration. + serviceAccountTokenPath string } func copyRedialableAgentClient(in RedialableAgentClient) RedialableAgentClient { @@ -88,13 +93,14 @@ func copyRedialableAgentClient(in RedialableAgentClient) RedialableAgentClient { func NewRedialableAgentClient(address, agentID string, cs *ClientSet, opts ...grpc.DialOption) (*RedialableAgentClient, error) { c := &RedialableAgentClient{ - cs: cs, - address: address, - agentID: agentID, - opts: opts, - probeInterval: cs.probeInterval, - reconnectInterval: cs.reconnectInterval, - stopCh: make(chan struct{}), + cs: cs, + address: address, + agentID: agentID, + opts: opts, + probeInterval: cs.probeInterval, + reconnectInterval: cs.reconnectInterval, + stopCh: make(chan struct{}), + serviceAccountTokenPath: cs.serviceAccountTokenPath, } serverID, err := c.Connect() if err != nil { @@ -332,6 +338,20 @@ type connectResult struct { agentServiceClient agent.AgentService_ConnectClient } +func (c *RedialableAgentClient) initializeAuthContext(ctx context.Context) (context.Context, error) { + var err error + var b []byte + + // load current service account's token value + if b, err = ioutil.ReadFile(c.serviceAccountTokenPath); err != nil { + klog.Errorf("Failed to read token from %q. err: %v", c.serviceAccountTokenPath, err) + return nil, err + } + ctx = metadata.AppendToOutgoingContext(ctx, header.AuthenticationTokenContextKey, header.AuthenticationTokenContextSchemePrefix+string(b)) + + return ctx, nil +} + // tryConnect makes the grpc dial to the proxy server. It returns the serverID // it connects to, and the number of servers (1 if server is non-HA). It also // updates c.stream. @@ -344,6 +364,11 @@ func (c *RedialableAgentClient) tryConnect() (connectResult, error) { } ctx := metadata.AppendToOutgoingContext(context.Background(), header.AgentID, c.agentID) + if c.serviceAccountTokenPath != "" { + if ctx, err = c.initializeAuthContext(ctx); err != nil { + return connectResult{}, err + } + } stream, err := agent.NewAgentServiceClient(conn).Connect(ctx) if err != nil { return connectResult{}, err diff --git a/pkg/agent/agentserver/server.go b/pkg/agent/agentserver/server.go index 32340f93d..c2ca7538c 100644 --- a/pkg/agent/agentserver/server.go +++ b/pkg/agent/agentserver/server.go @@ -17,15 +17,19 @@ limitations under the License. package agentserver import ( + "context" "fmt" "io" "math/rand" "net" "strconv" + "strings" "sync" "time" "google.golang.org/grpc/metadata" + authv1 "k8s.io/api/authentication/v1" + "k8s.io/client-go/kubernetes" "k8s.io/klog" "sigs.k8s.io/apiserver-network-proxy/proto/agent" "sigs.k8s.io/apiserver-network-proxy/proto/header" @@ -78,6 +82,17 @@ type ProxyServer struct { serverID string // unique ID of this server serverCount int // Number of proxy server instances, should be 1 unless it is a HA server. + // agent authentication + AgentAuthenticationOptions *AgentTokenAuthenticationOptions +} + +// AgentTokenAuthenticationOptions contains list of parameters required for agent token based authentication +type AgentTokenAuthenticationOptions struct { + Enabled bool + AgentNamespace string + AgentServiceAccount string + AuthenticationAudience string + KubernetesClient kubernetes.Interface } var _ agent.AgentServiceServer = &ProxyServer{} @@ -139,14 +154,15 @@ func (s *ProxyServer) randomBackend() (agent.AgentService_ConnectServer, error) } // NewProxyServer creates a new ProxyServer instance -func NewProxyServer(serverID string, serverCount int) *ProxyServer { +func NewProxyServer(serverID string, serverCount int, agentAuthenticationOptions *AgentTokenAuthenticationOptions) *ProxyServer { return &ProxyServer{ - Frontends: make(map[int64]*ProxyClientConnection), - PendingDial: make(map[int64]*ProxyClientConnection), - serverID: serverID, - serverCount: serverCount, - backends: make(map[string][]agent.AgentService_ConnectServer), - random: rand.New(rand.NewSource(time.Now().UTC().UnixNano())), + Frontends: make(map[int64]*ProxyClientConnection), + PendingDial: make(map[int64]*ProxyClientConnection), + serverID: serverID, + serverCount: serverCount, + backends: make(map[string][]agent.AgentService_ConnectServer), + random: rand.New(rand.NewSource(time.Now().UTC().UnixNano())), + AgentAuthenticationOptions: agentAuthenticationOptions, } } @@ -295,6 +311,76 @@ func agentID(stream agent.AgentService_ConnectServer) (string, error) { return agentIDs[0], nil } +func (s *ProxyServer) validateAuthToken(token string) error { + trReq := &authv1.TokenReview{ + Spec: authv1.TokenReviewSpec{ + Token: token, + Audiences: []string{s.AgentAuthenticationOptions.AuthenticationAudience}, + }, + } + r, err := s.AgentAuthenticationOptions.KubernetesClient.AuthenticationV1().TokenReviews().Create(trReq) + if err != nil { + return fmt.Errorf("Failed to authenticate request. err:%v", err) + } + + if r.Status.Error != "" { + return fmt.Errorf("lookup failed: %s", r.Status.Error) + } + + if !r.Status.Authenticated { + return fmt.Errorf("lookup failed: service account jwt not valid") + } + + // The username is of format: system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT) + parts := strings.Split(r.Status.User.Username, ":") + if len(parts) != 4 { + return fmt.Errorf("lookup failed: unexpected username format") + } + // Validate the user that comes back from token review is a service account + if parts[0] != "system" || parts[1] != "serviceaccount" { + return fmt.Errorf("lookup failed: username returned is not a service account") + } + + ns := parts[2] + sa := parts[3] + if s.AgentAuthenticationOptions.AgentNamespace != ns { + return fmt.Errorf("lookup failed: incoming request from %q namespace. Expected %q", ns, s.AgentAuthenticationOptions.AgentNamespace) + } + + if s.AgentAuthenticationOptions.AgentServiceAccount != sa { + return fmt.Errorf("lookup failed: incoming request from %q service account. Expected %q", sa, s.AgentAuthenticationOptions.AgentServiceAccount) + } + + return nil +} + +func (s *ProxyServer) authenticateAgentViaToken(ctx context.Context) error { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return fmt.Errorf("Failed to retrieve metadata from context") + } + + authContext := md.Get(header.AuthenticationTokenContextKey) + if len(authContext) == 0 { + return fmt.Errorf("Authentication context was not found in metadata") + } + + if len(authContext) > 1 { + return fmt.Errorf("too many (%d) tokens are received", len(authContext)) + } + + if !strings.HasPrefix(authContext[0], header.AuthenticationTokenContextSchemePrefix) { + return fmt.Errorf("received token does not have %q prefix", header.AuthenticationTokenContextSchemePrefix) + } + + if err := s.validateAuthToken(strings.TrimPrefix(authContext[0], header.AuthenticationTokenContextSchemePrefix)); err != nil { + return fmt.Errorf("Failed to validate authentication token, err:%v", err) + } + + klog.Infof("Client successfully authenticated via token") + return nil +} + // Connect is for agent to connect to ProxyServer as next hop func (s *ProxyServer) Connect(stream agent.AgentService_ConnectServer) error { agentID, err := agentID(stream) @@ -313,6 +399,13 @@ func (s *ProxyServer) Connect(stream agent.AgentService_ConnectServer) error { recvCh := make(chan *agent.Packet, 10) stopCh := make(chan error) + if s.AgentAuthenticationOptions.Enabled { + if err := s.authenticateAgentViaToken(stream.Context()); err != nil { + klog.Infof("Client authentication failed. err:%v", err) + return err + } + } + go s.serveRecvBackend(stream, recvCh) defer func() { diff --git a/pkg/agent/agentserver/server_test.go b/pkg/agent/agentserver/server_test.go index 5bc6d374d..e6dc3f212 100644 --- a/pkg/agent/agentserver/server_test.go +++ b/pkg/agent/agentserver/server_test.go @@ -17,10 +17,24 @@ limitations under the License. package agentserver import ( + "context" + "fmt" + "io" "reflect" "testing" + "github.com/golang/mock/gomock" + "google.golang.org/grpc/metadata" + + authv1 "k8s.io/api/authentication/v1" + "k8s.io/apimachinery/pkg/runtime" + k8sfake "k8s.io/client-go/kubernetes/fake" + fakeauthenticationv1 "k8s.io/client-go/kubernetes/typed/authentication/v1/fake" + k8stesting "k8s.io/client-go/testing" + + agentmock "sigs.k8s.io/apiserver-network-proxy/proto/agent/mocks" "sigs.k8s.io/apiserver-network-proxy/proto/agent" + "sigs.k8s.io/apiserver-network-proxy/proto/header" ) type fakeAgentService_ConnectServer struct { @@ -34,7 +48,7 @@ func TestAddRemoveBackends(t *testing.T) { conn22 := new(fakeAgentService_ConnectServer) conn3 := new(fakeAgentService_ConnectServer) - p := NewProxyServer("", 1) + p := NewProxyServer("", 1, &AgentTokenAuthenticationOptions{}) p.addBackend("agent1", conn1) p.removeBackend("agent1", conn1) expectedBackends := make(map[string][]agent.AgentService_ConnectServer) @@ -46,7 +60,7 @@ func TestAddRemoveBackends(t *testing.T) { t.Errorf("expected %v, got %v", e, a) } - p = NewProxyServer("", 1) + p = NewProxyServer("", 1, &AgentTokenAuthenticationOptions{}) p.addBackend("agent1", conn1) p.addBackend("agent1", conn12) p.addBackend("agent2", conn2) @@ -67,3 +81,147 @@ func TestAddRemoveBackends(t *testing.T) { t.Errorf("expected %v, got %v", e, a) } } + +func TestAgentTokenAuthenticationErrorsToken(t *testing.T) { + stub := gomock.NewController(t) + defer stub.Finish() + + ns := "test_ns" + sa := "test_sa" + + testCases := []struct { + desc string + mdKey string + tokens []string + wantNamespace string + wantServiceAccount string + authenticated bool + authError string + tokenReviewError error + wantError bool + }{ + { + desc: "no context", + wantError: true, + }, + { + desc: "non valid metadata key", + mdKey: "someKey", + tokens: []string{"token1"}, + wantError: true, + }, + { + desc: "non valid token prefix", + mdKey: header.AuthenticationTokenContextKey, + tokens: []string{"token1"}, + wantError: true, + }, + { + desc: "multiple valid tokens", + mdKey: header.AuthenticationTokenContextKey, + tokens: []string{header.AuthenticationTokenContextSchemePrefix + "token1", header.AuthenticationTokenContextSchemePrefix + "token2"}, + wantError: true, + }, + { + desc: "not authenticated", + authenticated: false, + mdKey: header.AuthenticationTokenContextKey, + tokens: []string{header.AuthenticationTokenContextSchemePrefix + "token1"}, + wantNamespace: ns, + wantServiceAccount: sa, + wantError: true, + }, + { + desc: "tokenReview error", + authenticated: false, + mdKey: header.AuthenticationTokenContextKey, + tokens: []string{header.AuthenticationTokenContextSchemePrefix + "token1"}, + tokenReviewError: fmt.Errorf("some error"), + wantNamespace: ns, + wantServiceAccount: sa, + wantError: true, + }, + { + desc: "non valid namespace", + authenticated: true, + mdKey: header.AuthenticationTokenContextKey, + tokens: []string{header.AuthenticationTokenContextSchemePrefix + "token1"}, + wantNamespace: "_" + ns, + wantServiceAccount: sa, + wantError: true, + }, + { + desc: "non valid service account", + authenticated: true, + mdKey: header.AuthenticationTokenContextKey, + tokens: []string{header.AuthenticationTokenContextSchemePrefix + "token1"}, + wantNamespace: ns, + wantServiceAccount: "_" + sa, + wantError: true, + }, + { + desc: "authorization succeed", + authenticated: true, + mdKey: header.AuthenticationTokenContextKey, + tokens: []string{header.AuthenticationTokenContextSchemePrefix + "token1"}, + wantNamespace: ns, + wantServiceAccount: sa, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + kcs := k8sfake.NewSimpleClientset() + + kcs.AuthenticationV1().(*fakeauthenticationv1.FakeAuthenticationV1).Fake.PrependReactor("create", "tokenreviews", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { + tr := &authv1.TokenReview{ + Status: authv1.TokenReviewStatus{ + Authenticated: tc.authenticated, + Error: tc.authError, + User: authv1.UserInfo{ + Username: fmt.Sprintf("system:serviceaccount:%v:%v", ns, sa), + }, + }, + } + return true, tr, tc.tokenReviewError + }) + + var md metadata.MD + for _, token := range tc.tokens { + md = metadata.Join(md, metadata.Pairs(tc.mdKey, token)) + } + + md = metadata.Join(md, metadata.Pairs(header.AgentID, "")) + + ctx := context.Background() + defer ctx.Done() + ctx = metadata.NewIncomingContext(ctx, md) + conn := agentmock.NewMockAgentService_ConnectServer(stub) + conn.EXPECT().Context().AnyTimes().Return(ctx) + conn.EXPECT().SendHeader(gomock.Any()).Return(nil) + + // close agent's connection if no error is expected + if !tc.wantError { + conn.EXPECT().Recv().Return(nil, io.EOF) + } + + p := NewProxyServer("", 1, &AgentTokenAuthenticationOptions{ + Enabled: true, + KubernetesClient: kcs, + AgentNamespace: tc.wantNamespace, + AgentServiceAccount: tc.wantServiceAccount, + }) + + err := p.Connect(conn) + if tc.wantError { + if err == nil { + t.Errorf("test case expected for error") + } + } else { + if err != nil { + t.Errorf("did not expected for error but got :%v", err) + } + } + }) + } +} diff --git a/pkg/util/certificates.go b/pkg/util/certificates.go index 47c3edb39..e8b89d9ae 100644 --- a/pkg/util/certificates.go +++ b/pkg/util/certificates.go @@ -23,6 +23,7 @@ import ( "io/ioutil" ) +// getCACertPool loads CA certificates to pool func getCACertPool(caFile string) (*x509.CertPool, error) { certPool := x509.NewCertPool() caCert, err := ioutil.ReadFile(caFile) diff --git a/proto/agent/mocks/agent_mock.go b/proto/agent/mocks/agent_mock.go new file mode 100644 index 000000000..a7f284524 --- /dev/null +++ b/proto/agent/mocks/agent_mock.go @@ -0,0 +1,162 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/apiserver-network-proxy/proto/agent (interfaces: AgentService_ConnectServer) + +// Package mock_agent is a generated GoMock package. +package mock_agent + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + metadata "google.golang.org/grpc/metadata" + reflect "reflect" + agent "sigs.k8s.io/apiserver-network-proxy/proto/agent" +) + +// MockAgentService_ConnectServer is a mock of AgentService_ConnectServer interface +type MockAgentService_ConnectServer struct { + ctrl *gomock.Controller + recorder *MockAgentService_ConnectServerMockRecorder +} + +// MockAgentService_ConnectServerMockRecorder is the mock recorder for MockAgentService_ConnectServer +type MockAgentService_ConnectServerMockRecorder struct { + mock *MockAgentService_ConnectServer +} + +// NewMockAgentService_ConnectServer creates a new mock instance +func NewMockAgentService_ConnectServer(ctrl *gomock.Controller) *MockAgentService_ConnectServer { + mock := &MockAgentService_ConnectServer{ctrl: ctrl} + mock.recorder = &MockAgentService_ConnectServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockAgentService_ConnectServer) EXPECT() *MockAgentService_ConnectServerMockRecorder { + return m.recorder +} + +// Context mocks base method +func (m *MockAgentService_ConnectServer) Context() context.Context { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Context") + ret0, _ := ret[0].(context.Context) + return ret0 +} + +// Context indicates an expected call of Context +func (mr *MockAgentService_ConnectServerMockRecorder) Context() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).Context)) +} + +// Recv mocks base method +func (m *MockAgentService_ConnectServer) Recv() (*agent.Packet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Recv") + ret0, _ := ret[0].(*agent.Packet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Recv indicates an expected call of Recv +func (mr *MockAgentService_ConnectServerMockRecorder) Recv() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recv", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).Recv)) +} + +// RecvMsg mocks base method +func (m *MockAgentService_ConnectServer) RecvMsg(arg0 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RecvMsg", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// RecvMsg indicates an expected call of RecvMsg +func (mr *MockAgentService_ConnectServerMockRecorder) RecvMsg(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecvMsg", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).RecvMsg), arg0) +} + +// Send mocks base method +func (m *MockAgentService_ConnectServer) Send(arg0 *agent.Packet) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Send", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Send indicates an expected call of Send +func (mr *MockAgentService_ConnectServerMockRecorder) Send(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).Send), arg0) +} + +// SendHeader mocks base method +func (m *MockAgentService_ConnectServer) SendHeader(arg0 metadata.MD) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendHeader", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendHeader indicates an expected call of SendHeader +func (mr *MockAgentService_ConnectServerMockRecorder) SendHeader(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeader", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).SendHeader), arg0) +} + +// SendMsg mocks base method +func (m *MockAgentService_ConnectServer) SendMsg(arg0 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendMsg", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendMsg indicates an expected call of SendMsg +func (mr *MockAgentService_ConnectServerMockRecorder) SendMsg(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMsg", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).SendMsg), arg0) +} + +// SetHeader mocks base method +func (m *MockAgentService_ConnectServer) SetHeader(arg0 metadata.MD) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetHeader", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetHeader indicates an expected call of SetHeader +func (mr *MockAgentService_ConnectServerMockRecorder) SetHeader(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetHeader", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).SetHeader), arg0) +} + +// SetTrailer mocks base method +func (m *MockAgentService_ConnectServer) SetTrailer(arg0 metadata.MD) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetTrailer", arg0) +} + +// SetTrailer indicates an expected call of SetTrailer +func (mr *MockAgentService_ConnectServerMockRecorder) SetTrailer(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTrailer", reflect.TypeOf((*MockAgentService_ConnectServer)(nil).SetTrailer), arg0) +} diff --git a/proto/header/header.go b/proto/header/header.go index 676acdf89..0acb98b9f 100644 --- a/proto/header/header.go +++ b/proto/header/header.go @@ -20,4 +20,11 @@ const ( ServerCount = "serverCount" ServerID = "serverID" AgentID = "agentID" + // AuthenticationTokenContextKey will be used as a key to store authentication tokens in grpc call + // (https://tools.ietf.org/html/rfc6750#section-2.1) + AuthenticationTokenContextKey = "Authorization" + + // AuthenticationTokenContextSchemePrefix has a prefix for auth token's content. + // (https://tools.ietf.org/html/rfc6750#section-2.1) + AuthenticationTokenContextSchemePrefix = "Bearer " ) diff --git a/tests/proxy_test.go b/tests/proxy_test.go index 1aafbab9c..d64899e4b 100644 --- a/tests/proxy_test.go +++ b/tests/proxy_test.go @@ -226,7 +226,7 @@ func runGRPCProxyServerWithServerCount(serverCount int) (proxy, func(), error) { var err error var lis, lis2 net.Listener - server := agentserver.NewProxyServer(uuid.New().String(), serverCount) + server := agentserver.NewProxyServer(uuid.New().String(), serverCount, &agentserver.AgentTokenAuthenticationOptions{}) grpcServer := grpc.NewServer() agentServer := grpc.NewServer() cleanup := func() { @@ -263,7 +263,7 @@ func runGRPCProxyServerWithServerCount(serverCount int) (proxy, func(), error) { func runHTTPConnProxyServer() (proxy, func(), error) { var proxy proxy - server := agentserver.NewProxyServer(uuid.New().String(), 0) + server := agentserver.NewProxyServer(uuid.New().String(), 0, &agentserver.AgentTokenAuthenticationOptions{}) agentServer := grpc.NewServer() agent.RegisterAgentServiceServer(agentServer, server)