diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java
new file mode 100644
index 00000000..4a94fedf
--- /dev/null
+++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2018 the original author or 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.
+ */
+package org.springframework.vault.core;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.vault.VaultException;
+import org.springframework.vault.client.VaultResponses;
+import org.springframework.vault.support.VaultResponse;
+import org.springframework.vault.support.VaultResponseSupport;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Base class for {@link VaultVersionedKeyValueTemplate} and other Vault KV-accessing
+ * helpers, defining common
+ *
+ * Not intended to be used directly. See {@link VaultVersionedKeyValueTemplate}.
+ *
+ * @author Mark Paluch
+ * @since 2.1
+ */
+public abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport {
+
+ private final VaultOperations vaultOperations;
+
+ private final String path;
+
+ private final ObjectMapper mapper;
+
+ /**
+ * Create a new {@link VaultKeyValueAccessor} given {@link VaultOperations} and the
+ * mount {@code path}.
+ *
+ * @param vaultOperations must not be {@literal null}.
+ * @param path must not be empty or {@literal null}.
+ */
+ public VaultKeyValueAccessor(VaultOperations vaultOperations, String path) {
+
+ Assert.notNull(vaultOperations, "VaultOperations must not be null");
+ Assert.hasText(path, "Path must not be empty");
+
+ this.vaultOperations = vaultOperations;
+ this.path = path;
+ this.mapper = extractObjectMapper(vaultOperations);
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ public List list(String path) {
+
+ String pathToUse = path.equals("/") ? "" : path.endsWith("/") ? path
+ : (path + "/");
+
+ VaultListResponse read = doRead((restOperations, httpEntity) -> {
+ return restOperations.exchange(String.format("%s?list=true",
+ createBackendPath("metadata", pathToUse)), HttpMethod.GET,
+ httpEntity, VaultListResponse.class);
+ });
+
+ if (read == null) {
+ return Collections.emptyList();
+ }
+
+ return (List) read.getRequiredData().get("keys");
+ }
+
+ @Override
+ public void delete(String path) {
+
+ Assert.hasText(path, "Path must not be empty");
+
+ doWithSession(((restOperations, httpHeaders) -> {
+ restOperations.exchange(createDataPath(path), HttpMethod.DELETE,
+ new HttpEntity<>(httpHeaders), Void.class);
+
+ return null;
+ }));
+ }
+
+ /**
+ * Read a secret at {@code path} and deserialize the nested {@literal data} element to
+ * the given {@link Class type}.
+ *
+ * @param path must not be {@literal null}.
+ * @param deserializeAs must not be {@literal null}.
+ * @param mappingFunction Mapping function to convert from the intermediate to the
+ * target data type. Must not be {@literal null}.
+ * @param intermediate data type for {@literal data} deserialization.
+ * @param return type. Value is created by the {@code mappingFunction}.
+ * @return mapped value.
+ */
+ @Nullable
+ T doRead(String path, Class deserializeAs,
+ BiFunction, I, T> mappingFunction) {
+
+ ParameterizedTypeReference> ref = VaultResponses
+ .getTypeReference(JsonNode.class);
+
+ VaultResponseSupport response = doRead(createDataPath(path), ref);
+
+ if (response != null) {
+
+ JsonNode jsonNode = response.getRequiredData().at("/data");
+
+ try {
+
+ I data = mapper.reader().readValue(jsonNode.traverse(), deserializeAs);
+
+ return mappingFunction.apply(response, data);
+ }
+ catch (IOException e) {
+ throw new VaultException("Cannot deserialize response", e);
+ }
+ }
+
+ return null;
+ }
+
+ @Nullable
+ T doRead(String path, ParameterizedTypeReference typeReference) {
+
+ return doRead((restOperations, httpEntity) -> {
+ return restOperations.exchange(path, HttpMethod.GET, httpEntity,
+ typeReference);
+ });
+ }
+
+ @Nullable
+ private T doRead(
+ BiFunction, ResponseEntity> callback) {
+
+ return doWithSession((restOperations, headers) -> {
+
+ try {
+ return callback.apply(restOperations, new HttpEntity<>(headers))
+ .getBody();
+ }
+ catch (HttpStatusCodeException e) {
+
+ if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return null;
+ }
+
+ throw VaultResponses.buildException(e, path);
+ }
+ });
+ }
+
+ @Nullable
+ VaultResponse doWrite(String path, Object body) {
+
+ Assert.hasText(path, "Path must not be empty");
+
+ try {
+
+ return doWithSession((restOperations, httpHeaders) -> {
+ return restOperations.exchange(path, HttpMethod.POST,
+ new HttpEntity<>(body, httpHeaders), VaultResponse.class)
+ .getBody();
+ });
+ }
+ catch (HttpStatusCodeException e) {
+ throw VaultResponses.buildException(e, path);
+ }
+ }
+
+ @Nullable
+ T doWithSession(BiFunction callback) {
+
+ return vaultOperations.doWithSession(restOperations -> {
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("X-Vault-Kv-Client", "v2");
+
+ return callback.apply(restOperations, headers);
+ });
+ }
+
+ String createDataPath(String path) {
+ return createBackendPath("data", path);
+ }
+
+ String createBackendPath(String segment, String path) {
+ return String.format("%s/%s/%s", this.path, segment, path);
+ }
+
+ private static ObjectMapper extractObjectMapper(VaultOperations vaultOperations) {
+
+ Optional mapper = vaultOperations
+ .doWithSession(operations -> {
+
+ if (operations instanceof RestTemplate) {
+
+ RestTemplate template = (RestTemplate) operations;
+
+ Optional jackson2Converter = template
+ .getMessageConverters()
+ .stream()
+ //
+ .filter(AbstractJackson2HttpMessageConverter.class::isInstance) //
+ .map(AbstractJackson2HttpMessageConverter.class::cast) //
+ .findFirst();
+
+ return jackson2Converter
+ .map(AbstractJackson2HttpMessageConverter::getObjectMapper);
+ }
+
+ return Optional.empty();
+ });
+
+ return mapper.orElseGet(ObjectMapper::new);
+ }
+}
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperations.java
new file mode 100644
index 00000000..0cca11e0
--- /dev/null
+++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperations.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 the original author or 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.
+ */
+package org.springframework.vault.core;
+
+import org.springframework.lang.Nullable;
+import org.springframework.vault.support.VaultResponse;
+import org.springframework.vault.support.VaultResponseSupport;
+
+/**
+ * Interface that specifies a basic set of Vault operations using Vault's Key/Value secret
+ * backend. Paths used in this operations interface are relative and outgoing requests
+ * prepend paths with the according operation-specific prefix.
+ *
+ * @author Mark Paluch
+ * @since 2.1
+ */
+public interface VaultKeyValueOperations extends VaultKeyValueOperationsSupport {
+
+ /**
+ * Read the secret at {@code path}.
+ *
+ * @param path must not be {@literal null}.
+ * @return the data. May be {@literal null} if the path does not exist.
+ */
+ @Nullable
+ VaultResponse get(String path);
+
+ /**
+ * Read the secret at {@code path}.
+ *
+ * @param path must not be {@literal null}.
+ * @param responseType must not be {@literal null}.
+ * @return the data. May be {@literal null} if the path does not exist.
+ */
+ @Nullable
+ VaultResponseSupport get(String path, Class responseType);
+
+ /**
+ * Write the secret at {@code path}.
+ *
+ * @param path must not be {@literal null}.
+ * @param body must not be {@literal null}.
+ * @return the resulting {@link VaultResponse}.
+ */
+ void put(String path, Object body);
+}
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperationsSupport.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperationsSupport.java
new file mode 100644
index 00000000..c436134f
--- /dev/null
+++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperationsSupport.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 the original author or 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.
+ */
+package org.springframework.vault.core;
+
+import java.util.List;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * Interface that specifies a basic set of Vault operations using Vault's Key/Value secret
+ * backend. Paths used in this operations interface are relative and outgoing requests
+ * prepend paths with the according operation-specific prefix.
+ *
+ *
+ * @author Mark Paluch
+ * @since 2.1
+ */
+public interface VaultKeyValueOperationsSupport {
+
+ /**
+ * Enumerate keys from a Vault path.
+ *
+ * @param path must not be {@literal null}.
+ * @return the data. May be {@literal null} if the path does not exist.
+ */
+ @Nullable
+ List list(String path);
+
+ /**
+ * Read the secret at {@code path}.
+ *
+ * @param path must not be {@literal null}.
+ * @return the data. May be {@literal null} if the path does not exist.
+ */
+ @Nullable
+ Object get(String path);
+
+ /**
+ * Delete the secret at {@code path}.
+ *
+ * @param path must not be {@literal null}.
+ */
+ void delete(String path);
+}
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueTemplate.java
new file mode 100644
index 00000000..caac0495
--- /dev/null
+++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueTemplate.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018 the original author or 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.
+ */
+package org.springframework.vault.core;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.vault.support.VaultResponse;
+import org.springframework.vault.support.VaultResponseSupport;
+
+/**
+ * Default implementation of {@link VaultKeyValueOperations}.
+ *
+ * @author Mark Paluch
+ * @since 2.1
+ */
+public class VaultKeyValueTemplate extends VaultKeyValueAccessor implements
+ VaultKeyValueOperations {
+
+ /**
+ * Create a new {@link VaultKeyValueTemplate} given {@link VaultOperations} and the
+ * mount {@code path}.
+ *
+ * @param vaultOperations must not be {@literal null}.
+ * @param path must not be empty or {@literal null}.
+ */
+ public VaultKeyValueTemplate(VaultOperations vaultOperations, String path) {
+
+ super(vaultOperations, path);
+ }
+
+ @Nullable
+ @Override
+ public VaultResponse get(String path) {
+
+ Assert.hasText(path, "Path must not be empty");
+
+ return doRead(path, Map.class, (response, data) -> {
+
+ VaultResponse vaultResponse = new VaultResponse();
+ vaultResponse.setRenewable(response.isRenewable());
+ vaultResponse.setAuth(response.getAuth());
+ vaultResponse.setLeaseDuration(response.getLeaseDuration());
+ vaultResponse.setLeaseId(response.getLeaseId());
+ vaultResponse.setMetadata(response.getMetadata());
+ vaultResponse.setRequestId(response.getRequestId());
+ vaultResponse.setWarnings(response.getWarnings());
+ vaultResponse.setWrapInfo(response.getWrapInfo());
+ vaultResponse.setData(data);
+
+ return vaultResponse;
+ });
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ public VaultResponseSupport get(String path, Class responseType) {
+
+ Assert.hasText(path, "Path must not be empty");
+ Assert.notNull(responseType, "Response type must not be null");
+
+ return doRead(path, responseType, (response, data) -> {
+
+ VaultResponseSupport result = response;
+ result.setData(data);
+ return result;
+ });
+ }
+
+ @Override
+ public void put(String path, Object body) {
+
+ Assert.hasText(path, "Path must not be empty");
+
+ doWrite(createDataPath(path), Collections.singletonMap("data", body));
+ }
+}
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultListResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultListResponse.java
new file mode 100644
index 00000000..bd3d8258
--- /dev/null
+++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultListResponse.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2018 the original author or 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.
+ */
+package org.springframework.vault.core;
+
+import java.util.Map;
+
+import org.springframework.vault.support.VaultResponseSupport;
+
+/**
+ * Type declaration for a list response.
+ *
+ * @author Mark Paluch
+ */
+class VaultListResponse extends VaultResponseSupport