10 Commits

7 changed files with 94 additions and 65 deletions

View File

@ -1,3 +1,8 @@
## 0.4.1 [2016-12-24]
* [fix] Factory Null-tolerant for trusted certificate (#6)
* [test] StackTraces testet for secret leaks
* [test] Tested against Vault 0.6.4
## 0.4.0 [2016-11-06]
* [feature] Option to provide a trusted CA certificate (#2)
* [feature] Deletion, revocation and renewal of secrets (#3)

View File

@ -5,26 +5,31 @@ Java Vault Connector is a connector library for [Vault](https://www.vaultproject
**Current available features:**
* HTTP(S) backend connector
* Ability to provide or enforce custom CA certificate
* Authorization methods:
* Token
* Username/Password
* AppID (register and authenticate)
* AppID (register and authenticate) [_deprecated_]
* AppRole (register and authenticate)
* Tokens
* Creation and lookup of tokens
* TokenBuilder for speaking creation of complex configuraitons
* Secrets
* Read secrets
* Write secrets
* List secrets
* Delete secrets
* Renew/revoke leases
* Raw secret content or JSON decoding
* Connector Factory with builder pattern
* Tested against Vault 0.6.2
* Tested against Vault 0.6.4
**Usage Example**
```java
// Instanciate using builder pattern style factory
VaultConnector vault = VaultConnectorFactory.httpFactory()
.wiithHost("127.0.0.1")
.withHost("127.0.0.1")
.withPort(8200)
.withTLS()
.build();
@ -41,7 +46,7 @@ String secret = vault.readSecret("some/secret/key").getValue();
<dependency>
<groupId>de.stklcode.jvault</groupId>
<artifactId>connector</artifactId>
<version>0.4.0</version>
<version>0.4.1</version>
</dependency>
```

View File

@ -6,7 +6,7 @@
<groupId>de.stklcode.jvault</groupId>
<artifactId>connector</artifactId>
<version>0.4.0</version>
<version>0.4.1</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -17,7 +17,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
@ -41,12 +41,12 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.4</version>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.4</version>
<version>2.8.5</version>
</dependency>
<dependency>

View File

@ -49,7 +49,6 @@ public class HTTPVaultConnector implements VaultConnector {
private static final String PATH_SEAL_STATUS = "sys/seal-status";
private static final String PATH_SEAL = "sys/seal";
private static final String PATH_UNSEAL = "sys/unseal";
private static final String PATH_INIT = "sys/init";
private static final String PATH_RENEW = "sys/renew";
private static final String PATH_AUTH = "sys/auth";
private static final String PATH_TOKEN = "auth/token";
@ -136,7 +135,8 @@ public class HTTPVaultConnector implements VaultConnector {
/**
* Create connector using full URL and trusted certificate.
*
* @param baseURL The URL
* @param baseURL The URL
* @param sslContext Custom SSL Context
*/
public HTTPVaultConnector(String baseURL, SSLContext sslContext) {
this.baseURL = baseURL;
@ -196,12 +196,6 @@ public class HTTPVaultConnector implements VaultConnector {
return authorized && (tokenTTL == 0 || tokenTTL >= System.currentTimeMillis());
}
@Override
public boolean init() {
/* TODO: implement init() */
return true;
}
@Override
public List<AuthBackend> getAuthBackends() throws VaultConnectorException {
try {
@ -240,6 +234,7 @@ public class HTTPVaultConnector implements VaultConnector {
}
@Override
@Deprecated
public AuthResponse authAppId(final String appID, final String userID) throws VaultConnectorException {
final Map<String, String> payload = new HashMap<>();
payload.put("app_id", appID);
@ -281,6 +276,7 @@ public class HTTPVaultConnector implements VaultConnector {
}
@Override
@Deprecated
public boolean registerAppId(final String appID, final String policy, final String displayName) throws VaultConnectorException {
if (!isAuthorized())
throw new AuthorizationRequiredException();
@ -296,6 +292,7 @@ public class HTTPVaultConnector implements VaultConnector {
}
@Override
@Deprecated
public boolean registerUserId(final String appID, final String userID) throws VaultConnectorException {
if (!isAuthorized())
throw new AuthorizationRequiredException();
@ -499,7 +496,7 @@ public class HTTPVaultConnector implements VaultConnector {
}
@Override
public boolean writeSecret(final String key, final String value) throws VaultConnectorException {
public void writeSecret(final String key, final String value) throws VaultConnectorException {
if (!isAuthorized())
throw new AuthorizationRequiredException();
@ -508,11 +505,12 @@ public class HTTPVaultConnector implements VaultConnector {
Map<String, String> param = new HashMap<>();
param.put("value", value);
return requestPost(PATH_SECRET + "/" + key, param).equals("");
if (!requestPost(PATH_SECRET + "/" + key, param).equals(""))
throw new InvalidResponseException("Received response where none was expected.");
}
@Override
public boolean deleteSecret(String key) throws VaultConnectorException {
public void deleteSecret(String key) throws VaultConnectorException {
if (!isAuthorized())
throw new AuthorizationRequiredException();
@ -521,13 +519,11 @@ public class HTTPVaultConnector implements VaultConnector {
/* Response should be code 204 without content */
if (!response.equals(""))
throw new InvalidResponseException("Received response where non was expected.");
return true;
throw new InvalidResponseException("Received response where none was expected.");
}
@Override
public boolean revoke(String leaseID) throws VaultConnectorException {
public void revoke(String leaseID) throws VaultConnectorException {
if (!isAuthorized())
throw new AuthorizationRequiredException();
@ -536,9 +532,7 @@ public class HTTPVaultConnector implements VaultConnector {
/* Response should be code 204 without content */
if (!response.equals(""))
throw new InvalidResponseException("Received response where non was expected.");
return true;
throw new InvalidResponseException("Received response where none was expected.");
}
@Override

View File

@ -32,13 +32,6 @@ import java.util.List;
* @since 0.1
*/
public interface VaultConnector {
/**
* Verify that vault connection is initialized.
*
* @return TRUE if correctly initialized
*/
boolean init();
/**
* Reset authorization information.
*/
@ -387,34 +380,32 @@ public interface VaultConnector {
*
* @param key Secret path
* @param value Secret value
* @return TRUE on success
* @throws VaultConnectorException on error
*/
boolean writeSecret(final String key, final String value) throws VaultConnectorException;
void writeSecret(final String key, final String value) throws VaultConnectorException;
/**
* Delete secret from Vault.
*
* @param key Secret path
* @return TRUE on succevss
* @throws VaultConnectorException on error
*/
boolean deleteSecret(final String key) throws VaultConnectorException;
void deleteSecret(final String key) throws VaultConnectorException;
/**
* Revoke given lease immediately.
*
* @param leaseID the lease ID
* @return TRUE on success
* @throws VaultConnectorException on error
*/
boolean revoke(final String leaseID) throws VaultConnectorException;
void revoke(final String leaseID) throws VaultConnectorException;
/**
* Renew lease with given ID.
*
* @param leaseID the lase ID
* @return Renewed lease
* @throws VaultConnectorException on error
*/
default SecretResponse renew(final String leaseID) throws VaultConnectorException {
return renew(leaseID, null);
@ -426,6 +417,7 @@ public interface VaultConnector {
* @param leaseID the lase ID
* @param increment number of seconds to extend lease time
* @return Renewed lease
* @throws VaultConnectorException on error
*/
SecretResponse renew(final String leaseID, final Integer increment) throws VaultConnectorException;

View File

@ -128,10 +128,13 @@ public class HTTPVaultConnectorFactory extends VaultConnectorFactory {
*
* @param cert path to certificate file
* @return self
* @throws VaultConnectorException on error
* @since 0.4.0
*/
public HTTPVaultConnectorFactory withTrustedCA(Path cert) throws VaultConnectorException {
return withSslContext(createSslContext(cert));
if (cert != null)
return withSslContext(createSslContext(cert));
return this;
}
/**

View File

@ -30,10 +30,7 @@ import org.junit.*;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestName;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.*;
import java.net.ServerSocket;
import java.nio.file.Paths;
import java.util.Arrays;
@ -46,7 +43,8 @@ import static org.junit.Assert.*;
import static org.junit.Assume.*;
/**
* JUnit Test for HTTP Vault connector.
* JUnit test for HTTP Vault connector.
* This test requires Vault binary in executable Path as it instantiates a real Vault server on given test data.
*
* @author Stefan Kalscheuer
* @since 0.1
@ -143,10 +141,13 @@ public class HTTPVaultConnectorTest {
@Test
public void authTokenTest() {
TokenResponse res;
final String invalidToken = "52135869df23a5e64c5d33a9785af5edb456b8a4a235d1fe135e6fba1c35edf6";
try {
connector.authToken("52135869df23a5e64c5d33a9785af5edb456b8a4a235d1fe135e6fba1c35edf6");
connector.authToken(invalidToken);
fail("Logged in with invalid token");
} catch (VaultConnectorException ignored) {
} catch (VaultConnectorException e) {
/* Assert that the exception does not reveal the token */
assertThat(stackTrace(e), not(stringContainsInOrder(invalidToken)));
}
try {
@ -164,10 +165,15 @@ public class HTTPVaultConnectorTest {
@Test
public void authUserPassTest() {
AuthResponse res = null;
final String invalidUser = "foo";
final String invalidPass = "bar";
try {
connector.authUserPass("foo", "bar");
connector.authUserPass(invalidUser, invalidPass);
fail("Logged in with invalid credentials");
} catch (VaultConnectorException ignored) {
} catch (VaultConnectorException e) {
/* Assert that the exception does not reveal credentials */
assertThat(stackTrace(e), not(stringContainsInOrder(invalidUser)));
assertThat(stackTrace(e), not(stringContainsInOrder(invalidPass)));
}
try {
@ -183,6 +189,7 @@ public class HTTPVaultConnectorTest {
* App-ID authentication roundtrip.
*/
@Test
@SuppressWarnings("deprecation")
public void authAppIdTest() {
authRoot();
assumeTrue(connector.isAuthorized());
@ -231,19 +238,27 @@ public class HTTPVaultConnectorTest {
}
/* Authenticate with valid secret ID against unknown role */
final String invalidRole = "foo";
try {
AuthResponse res = connector.authAppRole("foo", APPROLE_SECRET);
connector.authAppRole(invalidRole, APPROLE_SECRET);
fail("Successfully logged in with unknown role");
} catch (VaultConnectorException e) {
assertThat(e, is(instanceOf(InvalidResponseException.class)));
/* Assert that the exception does not reveal role ID or secret */
assertThat(stackTrace(e), not(stringContainsInOrder(invalidRole)));
assertThat(stackTrace(e), not(stringContainsInOrder(APPROLE_SECRET)));
}
/* Authenticate without wrong secret ID */
final String invalidSecret = "foo";
try {
AuthResponse res = connector.authAppRole(APPROLE_ROLE, "foo");
fail("Successfully logged in without secret ID");
} catch (VaultConnectorException e) {
assertThat(e, is(instanceOf(InvalidResponseException.class)));
/* Assert that the exception does not reveal role ID or secret */
assertThat(stackTrace(e), not(stringContainsInOrder(APPROLE_ROLE)));
assertThat(stackTrace(e), not(stringContainsInOrder(invalidSecret)));
}
/* Authenticate without secret ID */
@ -252,6 +267,8 @@ public class HTTPVaultConnectorTest {
fail("Successfully logged in without secret ID");
} catch (VaultConnectorException e) {
assertThat(e, is(instanceOf(InvalidResponseException.class)));
/* Assert that the exception does not reveal role ID */
assertThat(stackTrace(e), not(stringContainsInOrder(APPROLE_ROLE)));
}
/* Authenticate with secret ID on role with CIDR whitelist */
@ -437,11 +454,17 @@ public class HTTPVaultConnectorTest {
/* Try to read path user has no permission to read */
SecretResponse res = null;
final String invalidPath = "invalid/path";
try {
res = connector.readSecret("invalid/path");
res = connector.readSecret(invalidPath);
fail("Invalid secret path successfully read.");
} catch (VaultConnectorException e) {
assertThat(e, instanceOf(PermissionDeniedException.class));
/* Assert that the exception does not reveal secret or credentials */
assertThat(stackTrace(e), not(stringContainsInOrder(invalidPath)));
assertThat(stackTrace(e), not(stringContainsInOrder(USER_VALID)));
assertThat(stackTrace(e), not(stringContainsInOrder(PASS_VALID)));
assertThat(stackTrace(e), not(matchesPattern("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}")));
}
/* Try to read accessible path with known value */
@ -523,29 +546,28 @@ public class HTTPVaultConnectorTest {
/* Try to write to null path */
try {
boolean res = connector.writeSecret(null, "someValue");
connector.writeSecret(null, "someValue");
fail("Secret written to null path.");
} catch (VaultConnectorException e) {
assertThat(e, instanceOf(InvalidRequestException.class));
}
/* Try to write to invalid path */
try {
boolean res = connector.writeSecret("", "someValue");
connector.writeSecret("", "someValue");
fail("Secret written to invalid path.");
} catch (VaultConnectorException e) {
assertThat(e, instanceOf(InvalidRequestException.class));
}
/* Try to write to a path the user has no access for */
try {
boolean res = connector.writeSecret("invalid/path", "someValue");
connector.writeSecret("invalid/path", "someValue");
fail("Secret written to inaccessible path.");
} catch (VaultConnectorException e) {
assertThat(e, instanceOf(PermissionDeniedException.class));
}
/* Perform a valid write/read roundtrip to valid path. Also check UTF8-encoding. */
try {
boolean res = connector.writeSecret(SECRET_PATH + "/temp", "Abc123äöü,!");
assertThat("Secret could not be written to valid path.", res, is(true));
connector.writeSecret(SECRET_PATH + "/temp", "Abc123äöü,!");
} catch (VaultConnectorException e) {
fail("Secret written to inaccessible path.");
}
@ -567,8 +589,7 @@ public class HTTPVaultConnectorTest {
/* Write a test secret to vault */
try {
boolean res = connector.writeSecret(SECRET_PATH + "/toDelete", "secret content");
assumeThat("Secret could not be written path.", res, is(true));
connector.writeSecret(SECRET_PATH + "/toDelete", "secret content");
} catch (VaultConnectorException e) {
fail("Secret written to inaccessible path.");
}
@ -582,8 +603,7 @@ public class HTTPVaultConnectorTest {
/* Delete secret */
try {
boolean deleted = connector.deleteSecret(SECRET_PATH + "/toDelete");
assertThat("Revocation of secret faiked.", deleted, is(true));
connector.deleteSecret(SECRET_PATH + "/toDelete");
} catch (VaultConnectorException e) {
fail("Revocation threw unexpected exception.");
}
@ -608,8 +628,7 @@ public class HTTPVaultConnectorTest {
/* Write a test secret to vault */
try {
boolean res = connector.writeSecret(SECRET_PATH + "/toRevoke", "secret content");
assumeThat("Secret could not be written path.", res, is(true));
connector.writeSecret(SECRET_PATH + "/toRevoke", "secret content");
} catch (VaultConnectorException e) {
fail("Secret written to inaccessible path.");
}
@ -623,8 +642,7 @@ public class HTTPVaultConnectorTest {
/* Revoke secret */
try {
boolean revoked = connector.revoke(SECRET_PATH + "/toRevoke");
assertThat("Revocation of secret faiked.", revoked, is(true));
connector.revoke(SECRET_PATH + "/toRevoke");
} catch (VaultConnectorException e) {
fail("Revocation threw unexpected exception.");
}
@ -752,7 +770,7 @@ public class HTTPVaultConnectorTest {
/* Write configuration file */
BufferedWriter bw = null;
File configFile = null;
File configFile;
try {
configFile = tmpDir.newFile("vault.conf");
bw = new BufferedWriter(new FileWriter(configFile));
@ -831,4 +849,16 @@ public class HTTPVaultConnectorTest {
}
throw new IllegalStateException("Unable to find a free TCP port.");
}
/**
* Retrieve StackTrace from throwable as string
*
* @param th the throwable
* @return the stack trace
*/
private static String stackTrace(final Throwable th) {
StringWriter sw = new StringWriter();
th.printStackTrace(new PrintWriter(sw, true));
return sw.getBuffer().toString();
}
}