From 39a167e9012076eee63458d30e7c9f8da8eb8095 Mon Sep 17 00:00:00 2001
From: Tim Allclair <tallclair@google.com>
Date: Mon, 10 Oct 2022 18:15:22 -0700
Subject: [PATCH] Validate etcd paths

---
 .../apiserver/pkg/storage/etcd3/store.go      | 139 +++++++---
 .../apiserver/pkg/storage/etcd3/store_test.go | 253 ++++++++++++------
 2 files changed, 261 insertions(+), 131 deletions(-)

diff --git a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go
index 75a6c3b5..2069777c 100644
--- a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go
+++ b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go
@@ -99,16 +99,21 @@ func New(c *clientv3.Client, codec runtime.Codec, newFunc func() runtime.Object,
 
 func newStore(c *clientv3.Client, codec runtime.Codec, newFunc func() runtime.Object, prefix string, groupResource schema.GroupResource, transformer value.Transformer, pagingEnabled bool, leaseManagerConfig LeaseManagerConfig) *store {
 	versioner := APIObjectVersioner{}
+	// for compatibility with etcd2 impl.
+	// no-op for default prefix of '/registry'.
+	// keeps compatibility with etcd2 impl for custom prefixes that don't start with '/'
+	pathPrefix := path.Join("/", prefix)
+	if !strings.HasSuffix(pathPrefix, "/") {
+		// Ensure the pathPrefix ends in "/" here to simplify key concatenation later.
+		pathPrefix += "/"
+	}
 	result := &store{
-		client:        c,
-		codec:         codec,
-		versioner:     versioner,
-		transformer:   transformer,
-		pagingEnabled: pagingEnabled,
-		// for compatibility with etcd2 impl.
-		// no-op for default prefix of '/registry'.
-		// keeps compatibility with etcd2 impl for custom prefixes that don't start with '/'
-		pathPrefix:          path.Join("/", prefix),
+		client:              c,
+		codec:               codec,
+		versioner:           versioner,
+		transformer:         transformer,
+		pagingEnabled:       pagingEnabled,
+		pathPrefix:          pathPrefix,
 		groupResource:       groupResource,
 		groupResourceString: groupResource.String(),
 		watcher:             newWatcher(c, codec, newFunc, versioner, transformer),
@@ -124,9 +129,12 @@ func (s *store) Versioner() storage.Versioner {
 
 // Get implements storage.Interface.Get.
 func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, out runtime.Object) error {
-	key = path.Join(s.pathPrefix, key)
+	preparedKey, err := s.prepareKey(key)
+	if err != nil {
+		return err
+	}
 	startTime := time.Now()
-	getResp, err := s.client.KV.Get(ctx, key)
+	getResp, err := s.client.KV.Get(ctx, preparedKey)
 	metrics.RecordEtcdRequestLatency("get", getTypeName(out), startTime)
 	if err != nil {
 		return err
@@ -139,11 +147,11 @@ func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, ou
 		if opts.IgnoreNotFound {
 			return runtime.SetZeroValue(out)
 		}
-		return storage.NewKeyNotFoundError(key, 0)
+		return storage.NewKeyNotFoundError(preparedKey, 0)
 	}
 	kv := getResp.Kvs[0]
 
-	data, _, err := s.transformer.TransformFromStorage(ctx, kv.Value, authenticatedDataString(key))
+	data, _, err := s.transformer.TransformFromStorage(ctx, kv.Value, authenticatedDataString(preparedKey))
 	if err != nil {
 		return storage.NewInternalError(err.Error())
 	}
@@ -153,6 +161,11 @@ func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, ou
 
 // Create implements storage.Interface.Create.
 func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error {
+	preparedKey, err := s.prepareKey(key)
+	if err != nil {
+		return err
+	}
+
 	if version, err := s.versioner.ObjectResourceVersion(obj); err == nil && version != 0 {
 		return errors.New("resourceVersion should not be set on objects to be created")
 	}
@@ -163,30 +176,29 @@ func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object,
 	if err != nil {
 		return err
 	}
-	key = path.Join(s.pathPrefix, key)
 
 	opts, err := s.ttlOpts(ctx, int64(ttl))
 	if err != nil {
 		return err
 	}
 
-	newData, err := s.transformer.TransformToStorage(ctx, data, authenticatedDataString(key))
+	newData, err := s.transformer.TransformToStorage(ctx, data, authenticatedDataString(preparedKey))
 	if err != nil {
 		return storage.NewInternalError(err.Error())
 	}
 
 	startTime := time.Now()
 	txnResp, err := s.client.KV.Txn(ctx).If(
-		notFound(key),
+		notFound(preparedKey),
 	).Then(
-		clientv3.OpPut(key, string(newData), opts...),
+		clientv3.OpPut(preparedKey, string(newData), opts...),
 	).Commit()
 	metrics.RecordEtcdRequestLatency("create", getTypeName(obj), startTime)
 	if err != nil {
 		return err
 	}
 	if !txnResp.Succeeded {
-		return storage.NewKeyExistsError(key, 0)
+		return storage.NewKeyExistsError(preparedKey, 0)
 	}
 
 	if out != nil {
@@ -200,12 +212,15 @@ func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object,
 func (s *store) Delete(
 	ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions,
 	validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error {
+	preparedKey, err := s.prepareKey(key)
+	if err != nil {
+		return err
+	}
 	v, err := conversion.EnforcePtr(out)
 	if err != nil {
 		return fmt.Errorf("unable to convert output object to pointer: %v", err)
 	}
-	key = path.Join(s.pathPrefix, key)
-	return s.conditionalDelete(ctx, key, out, v, preconditions, validateDeletion, cachedExistingObject)
+	return s.conditionalDelete(ctx, preparedKey, out, v, preconditions, validateDeletion, cachedExistingObject)
 }
 
 func (s *store) conditionalDelete(
@@ -318,6 +333,10 @@ func (s *store) conditionalDelete(
 func (s *store) GuaranteedUpdate(
 	ctx context.Context, key string, out runtime.Object, ignoreNotFound bool,
 	preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object) error {
+	preparedKey, err := s.prepareKey(key)
+	if err != nil {
+		return err
+	}
 	trace := utiltrace.New("GuaranteedUpdate etcd3", utiltrace.Field{"type", getTypeName(out)})
 	defer trace.LogIfLong(500 * time.Millisecond)
 
@@ -325,16 +344,15 @@ func (s *store) GuaranteedUpdate(
 	if err != nil {
 		return fmt.Errorf("unable to convert output object to pointer: %v", err)
 	}
-	key = path.Join(s.pathPrefix, key)
 
 	getCurrentState := func() (*objState, error) {
 		startTime := time.Now()
-		getResp, err := s.client.KV.Get(ctx, key)
+		getResp, err := s.client.KV.Get(ctx, preparedKey)
 		metrics.RecordEtcdRequestLatency("get", getTypeName(out), startTime)
 		if err != nil {
 			return nil, err
 		}
-		return s.getState(ctx, getResp, key, v, ignoreNotFound)
+		return s.getState(ctx, getResp, preparedKey, v, ignoreNotFound)
 	}
 
 	var origState *objState
@@ -350,9 +368,9 @@ func (s *store) GuaranteedUpdate(
 	}
 	trace.Step("initial value restored")
 
-	transformContext := authenticatedDataString(key)
+	transformContext := authenticatedDataString(preparedKey)
 	for {
-		if err := preconditions.Check(key, origState.obj); err != nil {
+		if err := preconditions.Check(preparedKey, origState.obj); err != nil {
 			// If our data is already up to date, return the error
 			if origStateIsCurrent {
 				return err
@@ -435,11 +453,11 @@ func (s *store) GuaranteedUpdate(
 
 		startTime := time.Now()
 		txnResp, err := s.client.KV.Txn(ctx).If(
-			clientv3.Compare(clientv3.ModRevision(key), "=", origState.rev),
+			clientv3.Compare(clientv3.ModRevision(preparedKey), "=", origState.rev),
 		).Then(
-			clientv3.OpPut(key, string(newData), opts...),
+			clientv3.OpPut(preparedKey, string(newData), opts...),
 		).Else(
-			clientv3.OpGet(key),
+			clientv3.OpGet(preparedKey),
 		).Commit()
 		metrics.RecordEtcdRequestLatency("update", getTypeName(out), startTime)
 		if err != nil {
@@ -448,8 +466,8 @@ func (s *store) GuaranteedUpdate(
 		trace.Step("Transaction committed")
 		if !txnResp.Succeeded {
 			getResp := (*clientv3.GetResponse)(txnResp.Responses[0].GetResponseRange())
-			klog.V(4).Infof("GuaranteedUpdate of %s failed because of a conflict, going to retry", key)
-			origState, err = s.getState(ctx, getResp, key, v, ignoreNotFound)
+			klog.V(4).Infof("GuaranteedUpdate of %s failed because of a conflict, going to retry", preparedKey)
+			origState, err = s.getState(ctx, getResp, preparedKey, v, ignoreNotFound)
 			if err != nil {
 				return err
 			}
@@ -481,18 +499,21 @@ func getNewItemFunc(listObj runtime.Object, v reflect.Value) func() runtime.Obje
 }
 
 func (s *store) Count(key string) (int64, error) {
-	key = path.Join(s.pathPrefix, key)
+	preparedKey, err := s.prepareKey(key)
+	if err != nil {
+		return 0, err
+	}
 
 	// We need to make sure the key ended with "/" so that we only get children "directories".
 	// e.g. if we have key "/a", "/a/b", "/ab", getting keys with prefix "/a" will return all three,
 	// while with prefix "/a/" will return only "/a/b" which is the correct answer.
-	if !strings.HasSuffix(key, "/") {
-		key += "/"
+	if !strings.HasSuffix(preparedKey, "/") {
+		preparedKey += "/"
 	}
 
 	startTime := time.Now()
-	getResp, err := s.client.KV.Get(context.Background(), key, clientv3.WithRange(clientv3.GetPrefixRangeEnd(key)), clientv3.WithCountOnly())
-	metrics.RecordEtcdRequestLatency("listWithCount", key, startTime)
+	getResp, err := s.client.KV.Get(context.Background(), preparedKey, clientv3.WithRange(clientv3.GetPrefixRangeEnd(preparedKey)), clientv3.WithCountOnly())
+	metrics.RecordEtcdRequestLatency("listWithCount", preparedKey, startTime)
 	if err != nil {
 		return 0, err
 	}
@@ -561,6 +582,10 @@ func encodeContinue(key, keyPrefix string, resourceVersion int64) (string, error
 
 // GetList implements storage.Interface.
 func (s *store) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error {
+	preparedKey, err := s.prepareKey(key)
+	if err != nil {
+		return err
+	}
 	recursive := opts.Recursive
 	resourceVersion := opts.ResourceVersion
 	match := opts.ResourceVersionMatch
@@ -580,16 +605,15 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
 	if err != nil || v.Kind() != reflect.Slice {
 		return fmt.Errorf("need ptr to slice: %v", err)
 	}
-	key = path.Join(s.pathPrefix, key)
 
 	// For recursive lists, we need to make sure the key ended with "/" so that we only
 	// get children "directories". e.g. if we have key "/a", "/a/b", "/ab", getting keys
 	// with prefix "/a" will return all three, while with prefix "/a/" will return only
 	// "/a/b" which is the correct answer.
-	if recursive && !strings.HasSuffix(key, "/") {
-		key += "/"
+	if recursive && !strings.HasSuffix(preparedKey, "/") {
+		preparedKey += "/"
 	}
-	keyPrefix := key
+	keyPrefix := preparedKey
 
 	// set the appropriate clientv3 options to filter the returned data set
 	var limitOption *clientv3.OpOption
@@ -626,7 +650,7 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
 
 		rangeEnd := clientv3.GetPrefixRangeEnd(keyPrefix)
 		options = append(options, clientv3.WithRange(rangeEnd))
-		key = continueKey
+		preparedKey = continueKey
 
 		// If continueRV > 0, the LIST request needs a specific resource version.
 		// continueRV==0 is invalid.
@@ -693,7 +717,7 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
 	}()
 	for {
 		startTime := time.Now()
-		getResp, err = s.client.KV.Get(ctx, key, options...)
+		getResp, err = s.client.KV.Get(ctx, preparedKey, options...)
 		if recursive {
 			metrics.RecordEtcdRequestLatency("list", getTypeName(listPtr), startTime)
 		} else {
@@ -765,7 +789,7 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
 			}
 			*limitOption = clientv3.WithLimit(limit)
 		}
-		key = string(lastKey) + "\x00"
+		preparedKey = string(lastKey) + "\x00"
 		if withRev == 0 {
 			withRev = returnedRV
 			options = append(options, clientv3.WithRev(withRev))
@@ -830,12 +854,15 @@ func growSlice(v reflect.Value, maxCapacity int, sizes ...int) {
 
 // Watch implements storage.Interface.Watch.
 func (s *store) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) {
+	preparedKey, err := s.prepareKey(key)
+	if err != nil {
+		return nil, err
+	}
 	rev, err := s.versioner.ParseResourceVersion(opts.ResourceVersion)
 	if err != nil {
 		return nil, err
 	}
-	key = path.Join(s.pathPrefix, key)
-	return s.watcher.Watch(ctx, key, int64(rev), opts.Recursive, opts.ProgressNotify, opts.Predicate)
+	return s.watcher.Watch(ctx, preparedKey, int64(rev), opts.Recursive, opts.ProgressNotify, opts.Predicate)
 }
 
 func (s *store) getState(ctx context.Context, getResp *clientv3.GetResponse, key string, v reflect.Value, ignoreNotFound bool) (*objState, error) {
@@ -947,6 +974,30 @@ func (s *store) validateMinimumResourceVersion(minimumResourceVersion string, ac
 	return nil
 }
 
+func (s *store) prepareKey(key string) (string, error) {
+	if key == ".." ||
+		strings.HasPrefix(key, "../") ||
+		strings.HasSuffix(key, "/..") ||
+		strings.Contains(key, "/../") {
+		return "", fmt.Errorf("invalid key: %q", key)
+	}
+	if key == "." ||
+		strings.HasPrefix(key, "./") ||
+		strings.HasSuffix(key, "/.") ||
+		strings.Contains(key, "/./") {
+		return "", fmt.Errorf("invalid key: %q", key)
+	}
+	if key == "" || key == "/" {
+		return "", fmt.Errorf("empty key: %q", key)
+	}
+	// We ensured that pathPrefix ends in '/' in construction, so skip any leading '/' in the key now.
+	startIndex := 0
+	if key[0] == '/' {
+		startIndex = 1
+	}
+	return s.pathPrefix + key[startIndex:], nil
+}
+
 // decode decodes value of bytes into object. It will also set the object resource version to rev.
 // On success, objPtr would be set to the object.
 func decode(codec runtime.Codec, versioner storage.Versioner, value []byte, objPtr runtime.Object, rev int64) error {
diff --git a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go
index fedbcce3..65a73c21 100644
--- a/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go
+++ b/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store_test.go
@@ -64,6 +64,7 @@ var scheme = runtime.NewScheme()
 var codecs = serializer.NewCodecFactory(scheme)
 
 const defaultTestPrefix = "test!"
+const basePath = "/keybase"
 
 func init() {
 	metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
@@ -260,12 +261,12 @@ func TestGet(t *testing.T) {
 		rv:               strconv.FormatInt(math.MaxInt64, 10),
 	}, { // test get on non-existing item with ignoreNotFound=false
 		name:              "get non-existing",
-		key:               "/non-existing",
+		key:               basePath + "/non-existing",
 		ignoreNotFound:    false,
 		expectNotFoundErr: true,
 	}, { // test get on non-existing item with ignoreNotFound=true
 		name:              "get non-existing, ignore not found",
-		key:               "/non-existing",
+		key:               basePath + "/non-existing",
 		ignoreNotFound:    true,
 		expectNotFoundErr: false,
 		expectedOut:       &example.Pod{},
@@ -671,7 +672,7 @@ func TestGetListNonRecursive(t *testing.T) {
 
 func TestGuaranteedUpdate(t *testing.T) {
 	ctx, store, etcdClient := testSetup(t)
-	key := "/testkey"
+	key := basePath + "/testkey"
 
 	tests := []struct {
 		name                string
@@ -685,7 +686,7 @@ func TestGuaranteedUpdate(t *testing.T) {
 		hasSelfLink         bool
 	}{{
 		name:                "non-existing key, ignoreNotFound=false",
-		key:                 "/non-existing",
+		key:                 basePath + "/non-existing",
 		ignoreNotFound:      false,
 		precondition:        nil,
 		expectNotFoundErr:   true,
@@ -693,7 +694,7 @@ func TestGuaranteedUpdate(t *testing.T) {
 		expectNoUpdate:      false,
 	}, {
 		name:                "non-existing key, ignoreNotFound=true",
-		key:                 "/non-existing",
+		key:                 basePath + "/non-existing",
 		ignoreNotFound:      true,
 		precondition:        nil,
 		expectNotFoundErr:   false,
@@ -1044,13 +1045,13 @@ func TestTransformationFailure(t *testing.T) {
 		obj       *example.Pod
 		storedObj *example.Pod
 	}{{
-		key: "/one-level/test",
+		key: basePath + "/one-level/test",
 		obj: &example.Pod{
 			ObjectMeta: metav1.ObjectMeta{Name: "bar"},
 			Spec:       storagetesting.DeepEqualSafePodSpec(),
 		},
 	}, {
-		key: "/two-level/1/test",
+		key: basePath + "/two-level/1/test",
 		obj: &example.Pod{
 			ObjectMeta: metav1.ObjectMeta{Name: "baz"},
 			Spec:       storagetesting.DeepEqualSafePodSpec(),
@@ -1082,7 +1083,7 @@ func TestTransformationFailure(t *testing.T) {
 		Predicate: storage.Everything,
 		Recursive: true,
 	}
-	if err := store.GetList(ctx, "/", storageOpts, &got); !storage.IsInternalError(err) {
+	if err := store.GetList(ctx, basePath, storageOpts, &got); !storage.IsInternalError(err) {
 		t.Errorf("Unexpected error %v", err)
 	}
 
@@ -1144,30 +1145,30 @@ func TestList(t *testing.T) {
 		storedObj *example.Pod
 	}{
 		{
-			key: "/one-level/test",
+			key: basePath + "/one-level/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/two-level/1/test",
+			key: basePath + "/two-level/1/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/two-level/2/test",
+			key: basePath + "/two-level/2/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "bar"}},
 		},
 		{
-			key: "/z-level/3/test",
+			key: basePath + "/z-level/3/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "fourth"}},
 		},
 		{
-			key: "/z-level/3/test-2",
+			key: basePath + "/z-level/3/test-2",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "bar"}},
 		},
 	}
 
 	// we want to figure out the resourceVersion before we create anything
 	initialList := &example.PodList{}
-	if err := store.GetList(ctx, "/", storage.ListOptions{Predicate: storage.Everything, Recursive: true}, initialList); err != nil {
+	if err := store.GetList(ctx, basePath, storage.ListOptions{Predicate: storage.Everything, Recursive: true}, initialList); err != nil {
 		t.Errorf("Unexpected List error: %v", err)
 	}
 	initialRV := initialList.ResourceVersion
@@ -1186,7 +1187,7 @@ func TestList(t *testing.T) {
 		Predicate:       storage.Everything,
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/two-level", storageOpts, list); err != nil {
+	if err := store.GetList(ctx, basePath+"/two-level", storageOpts, list); err != nil {
 		t.Errorf("Unexpected error: %v", err)
 	}
 	continueRV, _ := strconv.Atoi(list.ResourceVersion)
@@ -1216,14 +1217,14 @@ func TestList(t *testing.T) {
 	}{
 		{
 			name:        "rejects invalid resource version",
-			prefix:      "/",
+			prefix:      basePath,
 			pred:        storage.Everything,
 			rv:          "abc",
 			expectError: true,
 		},
 		{
 			name:   "rejects resource version and continue token",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Label:    labels.Everything(),
 				Field:    fields.Everything(),
@@ -1235,26 +1236,26 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:             "rejects resource version set too high",
-			prefix:           "/",
+			prefix:           basePath,
 			rv:               strconv.FormatInt(math.MaxInt64, 10),
 			expectRVTooLarge: true,
 		},
 		{
 			name:        "test List on existing key",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[0].storedObj},
 		},
 		{
 			name:        "test List on existing key with resource version set to 0",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[0].storedObj},
 			rv:          "0",
 		},
 		{
 			name:        "test List on existing key with resource version set before first write, match=Exact",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{},
 			rv:          initialRV,
@@ -1263,7 +1264,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List on existing key with resource version set to 0, match=NotOlderThan",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[0].storedObj},
 			rv:          "0",
@@ -1271,7 +1272,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List on existing key with resource version set to 0, match=Invalid",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			rv:          "0",
 			rvMatch:     "Invalid",
@@ -1279,7 +1280,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List on existing key with resource version set before first write, match=NotOlderThan",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[0].storedObj},
 			rv:          initialRV,
@@ -1287,7 +1288,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List on existing key with resource version set before first write, match=Invalid",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			rv:          initialRV,
 			rvMatch:     "Invalid",
@@ -1295,14 +1296,14 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List on existing key with resource version set to current resource version",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[0].storedObj},
 			rv:          list.ResourceVersion,
 		},
 		{
 			name:        "test List on existing key with resource version set to current resource version, match=Exact",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[0].storedObj},
 			rv:          list.ResourceVersion,
@@ -1311,7 +1312,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List on existing key with resource version set to current resource version, match=NotOlderThan",
-			prefix:      "/one-level/",
+			prefix:      basePath + "/one-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[0].storedObj},
 			rv:          list.ResourceVersion,
@@ -1319,13 +1320,13 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List on non-existing key",
-			prefix:      "/non-existing/",
+			prefix:      basePath + "/non-existing/",
 			pred:        storage.Everything,
 			expectedOut: nil,
 		},
 		{
 			name:   "test List with pod name matching",
-			prefix: "/one-level/",
+			prefix: basePath + "/one-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.ParseSelectorOrDie("metadata.name!=foo"),
@@ -1334,7 +1335,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with limit",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.Everything(),
@@ -1346,7 +1347,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with limit at current resource version",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.Everything(),
@@ -1360,7 +1361,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with limit at current resource version and match=Exact",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.Everything(),
@@ -1375,7 +1376,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with limit at resource version 0",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.Everything(),
@@ -1389,7 +1390,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with limit at resource version 0 match=NotOlderThan",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.Everything(),
@@ -1404,7 +1405,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with limit at resource version before first write and match=Exact",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.Everything(),
@@ -1419,7 +1420,7 @@ func TestList(t *testing.T) {
 		{
 			name:          "test List with limit when paging disabled",
 			disablePaging: true,
-			prefix:        "/two-level/",
+			prefix:        basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label: labels.Everything(),
 				Field: fields.Everything(),
@@ -1430,7 +1431,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with pregenerated continue token",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label:    labels.Everything(),
 				Field:    fields.Everything(),
@@ -1441,7 +1442,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "ignores resource version 0 for List with pregenerated continue token",
-			prefix: "/two-level/",
+			prefix: basePath + "/two-level/",
 			pred: storage.SelectionPredicate{
 				Label:    labels.Everything(),
 				Field:    fields.Everything(),
@@ -1453,13 +1454,13 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:        "test List with multiple levels of directories and expect flattened result",
-			prefix:      "/two-level/",
+			prefix:      basePath + "/two-level/",
 			pred:        storage.Everything,
 			expectedOut: []*example.Pod{preset[1].storedObj, preset[2].storedObj},
 		},
 		{
 			name:   "test List with filter returning only one item, ensure only a single page returned",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field: fields.OneTermEqualSelector("metadata.name", "fourth"),
 				Label: labels.Everything(),
@@ -1470,7 +1471,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with filter returning only one item, covers the entire list",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field: fields.OneTermEqualSelector("metadata.name", "fourth"),
 				Label: labels.Everything(),
@@ -1481,7 +1482,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with filter returning only one item, covers the entire list, with resource version 0",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field: fields.OneTermEqualSelector("metadata.name", "fourth"),
 				Label: labels.Everything(),
@@ -1493,7 +1494,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "test List with filter returning two items, more pages possible",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field: fields.OneTermEqualSelector("metadata.name", "foo"),
 				Label: labels.Everything(),
@@ -1504,7 +1505,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "filter returns two items split across multiple pages",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field: fields.OneTermEqualSelector("metadata.name", "bar"),
 				Label: labels.Everything(),
@@ -1514,7 +1515,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "filter returns one item for last page, ends on last item, not full",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field:    fields.OneTermEqualSelector("metadata.name", "bar"),
 				Label:    labels.Everything(),
@@ -1525,7 +1526,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "filter returns one item for last page, starts on last item, full",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field:    fields.OneTermEqualSelector("metadata.name", "bar"),
 				Label:    labels.Everything(),
@@ -1536,7 +1537,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "filter returns one item for last page, starts on last item, partial page",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field:    fields.OneTermEqualSelector("metadata.name", "bar"),
 				Label:    labels.Everything(),
@@ -1547,7 +1548,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "filter returns two items, page size equal to total list size",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field: fields.OneTermEqualSelector("metadata.name", "bar"),
 				Label: labels.Everything(),
@@ -1557,7 +1558,7 @@ func TestList(t *testing.T) {
 		},
 		{
 			name:   "filter returns one item, page size equal to total list size",
-			prefix: "/",
+			prefix: basePath,
 			pred: storage.SelectionPredicate{
 				Field: fields.OneTermEqualSelector("metadata.name", "fourth"),
 				Label: labels.Everything(),
@@ -1636,7 +1637,7 @@ func TestListContinuation(t *testing.T) {
 	ctx := context.Background()
 
 	// Setup storage with the following structure:
-	//  /
+	//  /keybase/
 	//   - one-level/
 	//  |            - test
 	//  |
@@ -1653,15 +1654,15 @@ func TestListContinuation(t *testing.T) {
 		storedObj *example.Pod
 	}{
 		{
-			key: "/one-level/test",
+			key: basePath + "/one-level/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/two-level/1/test",
+			key: basePath + "/two-level/1/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/two-level/2/test",
+			key: basePath + "/two-level/2/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "bar"}},
 		},
 	}
@@ -1693,7 +1694,7 @@ func TestListContinuation(t *testing.T) {
 		Predicate:       pred(1, ""),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get initial list: %v", err)
 	}
 	if len(out.Continue) == 0 {
@@ -1718,13 +1719,13 @@ func TestListContinuation(t *testing.T) {
 		Predicate:       pred(0, continueFromSecondItem),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get second page: %v", err)
 	}
 	if len(out.Continue) != 0 {
 		t.Fatalf("Unexpected continuation token set")
 	}
-	key, rv, err := decodeContinue(continueFromSecondItem, "/")
+	key, rv, err := decodeContinue(continueFromSecondItem, basePath)
 	t.Logf("continue token was %d %s %v", rv, key, err)
 	expectNoDiff(t, "incorrect second page", []example.Pod{*preset[1].storedObj, *preset[2].storedObj}, out.Items)
 	if transformer.reads != 2 {
@@ -1743,7 +1744,7 @@ func TestListContinuation(t *testing.T) {
 		Predicate:       pred(1, continueFromSecondItem),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get second page: %v", err)
 	}
 	if len(out.Continue) == 0 {
@@ -1767,7 +1768,7 @@ func TestListContinuation(t *testing.T) {
 		Predicate:       pred(1, continueFromThirdItem),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get second page: %v", err)
 	}
 	if len(out.Continue) != 0 {
@@ -1796,7 +1797,7 @@ func TestListPaginationRareObject(t *testing.T) {
 	podCount := 1000
 	var pods []*example.Pod
 	for i := 0; i < podCount; i++ {
-		key := fmt.Sprintf("/one-level/pod-%d", i)
+		key := basePath + fmt.Sprintf("/one-level/pod-%d", i)
 		obj := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("pod-%d", i)}}
 		storedObj := &example.Pod{}
 		err := store.Create(ctx, key, obj, storedObj, 0)
@@ -1819,7 +1820,7 @@ func TestListPaginationRareObject(t *testing.T) {
 		},
 		Recursive: true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get initial list: %v", err)
 	}
 	if len(out.Continue) != 0 {
@@ -1874,26 +1875,26 @@ func TestListContinuationWithFilter(t *testing.T) {
 		storedObj *example.Pod
 	}{
 		{
-			key: "/1",
+			key: basePath + "/1",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/2",
+			key: basePath + "/2",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "bar"}}, // this should not match
 		},
 		{
-			key: "/3",
+			key: basePath + "/3",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/4",
+			key: basePath + "/4",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 	}
 
 	for i, ps := range preset {
 		preset[i].storedObj = &example.Pod{}
-		err := store.Create(ctx, ps.key, ps.obj, preset[i].storedObj, 0)
+		err := store.Create(ctx, basePath+ps.key, ps.obj, preset[i].storedObj, 0)
 		if err != nil {
 			t.Fatalf("Set failed: %v", err)
 		}
@@ -1921,7 +1922,7 @@ func TestListContinuationWithFilter(t *testing.T) {
 		Predicate:       pred(2, ""),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Errorf("Unable to get initial list: %v", err)
 	}
 	if len(out.Continue) == 0 {
@@ -1953,7 +1954,7 @@ func TestListContinuationWithFilter(t *testing.T) {
 		Predicate:       pred(2, cont),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Errorf("Unable to get second page: %v", err)
 	}
 	if len(out.Continue) != 0 {
@@ -1977,7 +1978,7 @@ func TestListInconsistentContinuation(t *testing.T) {
 	ctx := context.Background()
 
 	// Setup storage with the following structure:
-	//  /
+	//  /keybase/
 	//   - one-level/
 	//  |            - test
 	//  |
@@ -1994,15 +1995,15 @@ func TestListInconsistentContinuation(t *testing.T) {
 		storedObj *example.Pod
 	}{
 		{
-			key: "/one-level/test",
+			key: basePath + "/one-level/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/two-level/1/test",
+			key: basePath + "/two-level/1/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
 		},
 		{
-			key: "/two-level/2/test",
+			key: basePath + "/two-level/2/test",
 			obj: &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "bar"}},
 		},
 	}
@@ -2034,7 +2035,7 @@ func TestListInconsistentContinuation(t *testing.T) {
 		Predicate:       pred(1, ""),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get initial list: %v", err)
 	}
 	if len(out.Continue) == 0 {
@@ -2078,7 +2079,7 @@ func TestListInconsistentContinuation(t *testing.T) {
 		Predicate:       pred(0, continueFromSecondItem),
 		Recursive:       true,
 	}
-	err = store.GetList(ctx, "/", options, out)
+	err = store.GetList(ctx, basePath, options, out)
 	if err == nil {
 		t.Fatalf("unexpected no error")
 	}
@@ -2100,7 +2101,7 @@ func TestListInconsistentContinuation(t *testing.T) {
 		Predicate:       pred(1, inconsistentContinueFromSecondItem),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get second page: %v", err)
 	}
 	if len(out.Continue) == 0 {
@@ -2117,7 +2118,7 @@ func TestListInconsistentContinuation(t *testing.T) {
 		Predicate:       pred(1, continueFromThirdItem),
 		Recursive:       true,
 	}
-	if err := store.GetList(ctx, "/", options, out); err != nil {
+	if err := store.GetList(ctx, basePath, options, out); err != nil {
 		t.Fatalf("Unable to get second page: %v", err)
 	}
 	if len(out.Continue) != 0 {
@@ -2150,7 +2151,7 @@ func testSetup(t *testing.T) (context.Context, *store, *clientv3.Client) {
 // keys and stored objects.
 func testPropogateStore(ctx context.Context, t *testing.T, store *store, obj *example.Pod) (string, *example.Pod) {
 	// Setup store with a key and grab the output for returning.
-	key := "/testkey"
+	key := basePath + "/testkey"
 	return key, testPropogateStoreWithKey(ctx, t, store, key, obj)
 }
 
@@ -2177,9 +2178,9 @@ func TestPrefix(t *testing.T) {
 	codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
 	transformer := &prefixTransformer{prefix: []byte(defaultTestPrefix)}
 	testcases := map[string]string{
-		"custom/prefix":     "/custom/prefix",
-		"/custom//prefix//": "/custom/prefix",
-		"/registry":         "/registry",
+		"custom/prefix":     "/custom/prefix/",
+		"/custom//prefix//": "/custom/prefix/",
+		"/registry":         "/registry/",
 	}
 	for configuredPrefix, effectivePrefix := range testcases {
 		store := newStore(client, codec, nil, configuredPrefix, schema.GroupResource{Resource: "widgets"}, transformer, true, newTestLeaseManagerConfig())
@@ -2375,7 +2376,7 @@ func TestConsistentList(t *testing.T) {
 		Predicate: predicate,
 		Recursive: true,
 	}
-	if err := store.GetList(ctx, "/", options, &result1); err != nil {
+	if err := store.GetList(ctx, basePath, options, &result1); err != nil {
 		t.Fatalf("failed to list objects: %v", err)
 	}
 
@@ -2388,7 +2389,7 @@ func TestConsistentList(t *testing.T) {
 	}
 
 	result2 := example.PodList{}
-	if err := store.GetList(ctx, "/", options, &result2); err != nil {
+	if err := store.GetList(ctx, basePath, options, &result2); err != nil {
 		t.Fatalf("failed to list objects: %v", err)
 	}
 
@@ -2398,7 +2399,7 @@ func TestConsistentList(t *testing.T) {
 	options.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan
 
 	result3 := example.PodList{}
-	if err := store.GetList(ctx, "/", options, &result3); err != nil {
+	if err := store.GetList(ctx, basePath, options, &result3); err != nil {
 		t.Fatalf("failed to list objects: %v", err)
 	}
 
@@ -2406,7 +2407,7 @@ func TestConsistentList(t *testing.T) {
 	options.ResourceVersionMatch = metav1.ResourceVersionMatchExact
 
 	result4 := example.PodList{}
-	if err := store.GetList(ctx, "/", options, &result4); err != nil {
+	if err := store.GetList(ctx, basePath, options, &result4); err != nil {
 		t.Fatalf("failed to list objects: %v", err)
 	}
 
@@ -2467,15 +2468,15 @@ func TestLeaseMaxObjectCount(t *testing.T) {
 		expectAttachedCount int64
 	}{
 		{
-			key:                 "testkey1",
+			key:                 basePath + "/testkey1",
 			expectAttachedCount: 1,
 		},
 		{
-			key:                 "testkey2",
+			key:                 basePath + "/testkey2",
 			expectAttachedCount: 2,
 		},
 		{
-			key: "testkey3",
+			key: basePath + "testkey3",
 			// We assume each time has 1 object attached to the lease
 			// so after granting a new lease, the recorded count is set to 1
 			expectAttachedCount: 1,
@@ -2503,3 +2504,81 @@ func expectNoDiff(t *testing.T, msg string, expected, got interface{}) {
 		}
 	}
 }
+
+func TestValidateKey(t *testing.T) {
+	validKeys := []string{
+		"/foo/bar/baz/a.b.c/",
+		"/foo",
+		"foo/bar/baz",
+		"/foo/bar..baz/",
+		"/foo/bar..",
+		"foo",
+		"foo/bar",
+		"/foo/bar/",
+	}
+	invalidKeys := []string{
+		"/foo/bar/../a.b.c/",
+		"..",
+		"/..",
+		"../",
+		"/foo/bar/..",
+		"../foo/bar",
+		"/../foo",
+		"/foo/bar/../",
+		".",
+		"/.",
+		"./",
+		"/./",
+		"/foo/.",
+		"./bar",
+		"/foo/./bar/",
+	}
+	const (
+		pathPrefix   = "/first/second"
+		expectPrefix = pathPrefix + "/"
+	)
+	client := testserver.RunEtcd(t, nil)
+	codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
+	store := newStore(client, codec, newPod, pathPrefix, schema.GroupResource{Resource: "pods"},
+		&prefixTransformer{prefix: []byte(defaultTestPrefix)}, true, newTestLeaseManagerConfig())
+
+	for _, key := range validKeys {
+		k, err := store.prepareKey(key)
+		if err != nil {
+			t.Errorf("key %q should be valid; unexpected error: %v", key, err)
+		} else if !strings.HasPrefix(k, expectPrefix) {
+			t.Errorf("key %q should have prefix %q", k, expectPrefix)
+		}
+	}
+
+	for _, key := range invalidKeys {
+		_, err := store.prepareKey(key)
+		if err == nil {
+			t.Errorf("key %q should be invalid", key)
+		}
+	}
+}
+
+func TestInvalidKeys(t *testing.T) {
+	const invalidKey = "/foo/bar/../baz"
+	expectedError := fmt.Sprintf("invalid key: %q", invalidKey)
+
+	expectInvalidKey := func(methodName string, err error) {
+		if err == nil {
+			t.Errorf("[%s] expected invalid key error; got nil", methodName)
+		} else if err.Error() != expectedError {
+			t.Errorf("[%s] expected invalid key error; got %v", methodName, err)
+		}
+	}
+
+	ctx, store, _ := testSetup(t)
+	expectInvalidKey("Create", store.Create(ctx, invalidKey, nil, nil, 0))
+	expectInvalidKey("Delete", store.Delete(ctx, invalidKey, nil, nil, nil, nil))
+	_, watchErr := store.Watch(ctx, invalidKey, storage.ListOptions{})
+	expectInvalidKey("Watch", watchErr)
+	expectInvalidKey("Get", store.Get(ctx, invalidKey, storage.GetOptions{}, nil))
+	expectInvalidKey("GetList", store.GetList(ctx, invalidKey, storage.ListOptions{}, nil))
+	expectInvalidKey("GuaranteedUpdate", store.GuaranteedUpdate(ctx, invalidKey, nil, true, nil, nil, nil))
+	_, countErr := store.Count(invalidKey)
+	expectInvalidKey("Count", countErr)
+}
