Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Spring Vault as alternative properties store #308

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions README.fork.ip6li.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Generation vapi Keypair

Alovoa need for class *PushAsyncService* used in *NotificationService* a key pair.

```
$ git clone --depth 1 https://github.com/web-push-libs/webpush-java.git
$ cd webpush-java
$ ./gradlew run --args="generate-key"
> Task :run
PublicKey:
BBGKmyzGuw53tFpqxy-ol7v1FF5932sRV6iKPdtGtcFPTSC76QmevQw89pVSSzvPQCN9f_v2JbsF7n4ieAG2nhQ=
PrivateKey:
JIEdQkCnF-YFAI-kOExRzPIDO4qxSFeHT-RGdxmTuFs=
```

Save generated private and public key in vault as *app.vapid.public* and *app.vapid.private*.
You should consider to save at least private key in vault.

# Vault

It is a really bad idea to store credentials or private keys in source code or
property files. Spring Boot support vault solutions like Hashicorp Vault.

Before starting Alovoa the first time you should set up Hashicorp Vault.

## Config

Following config is able to store mostly everything which may be configurable
in *application.properties*. Alovoa fork (ab)uses vault as an alternative
properties store.

Generate an individual vapi keypair as shown above.

```
vault secrets enable -path=alovoa kv
vault kv put -mount=alovoa creds app.text.key=32charKey app.text.salt=16charSalt app.admin.key=InitialAdmiPassword app.admin.email=AdminEmail spring.mail.username=SmtpUsername spring.mail.password=SmtpPassword app.vapid.private=vapidPrivateKey app.vapid.public=vapidPublicKey
```

Feel free to add more properties you would not like to see in application.properties.

## Database Credentials

Add a MySQL user which is allowed to set passwords. Configure that user in Hashicorp Vault:

```
vault write database/config/alovoa \
plugin_name=mysql-database-plugin \
connection_url="{{username}}:{{password}}@tcp(mysql)/" \
allowed_roles="mysqlrole" \
username="vault" \
password="PasswordForMysqlUserVault"
```

Tell vault how to create a temp user. This user is retrieved and used by Alovoa:

```
vault write database/roles/mysqlrole \
db_name=alovoa \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT ALL PRIVILEGES ON alovoa.* TO {{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"
```

## Vault Policy

Alovoa should not use root vault token, it should use a dedicated token with specific entitelments:

```
vault policy write alovoa-policy - << EOF
path "database/creds/mysqlrole" {
capabilities = ["read"]
}
path "oauth-idp/keycloak" {
capabilities = ["read"]
}
path "alovoa/creds" {
capabilities = ["read"]
}
EOF
```

## Vault Token

Now generate a dedicated token for Alovoa.

```
vault token create -policy=alovoa-policy
```

Save output of that command.
14 changes: 12 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.9</version>
<version>3.1.2</version>
<relativePath />
</parent>
<groupId>com.alovoa</groupId>
Expand All @@ -16,7 +16,7 @@
<description>Alovoa</description>

<properties>
<java.version>1.8</java.version>
<java.version>17</java.version>
</properties>

<dependencies>
Expand All @@ -25,6 +25,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-vault-config-databases</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
Expand Down
34 changes: 26 additions & 8 deletions src/main/java/com/nonononoki/alovoa/AlovoaApplication.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.nonononoki.alovoa;

import java.security.Security;

import com.nonononoki.alovoa.config.VaultPropertySource;
import com.nonononoki.alovoa.model.AlovoaException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;

import java.security.Security;


@SpringBootApplication
@EnableJpaRepositories("com.nonononoki.alovoa.repo")
Expand All @@ -18,9 +20,25 @@
@EnableScheduling
public class AlovoaApplication {

public static void main(String[] args) {

Security.addProvider(new BouncyCastleProvider());
SpringApplication.run(AlovoaApplication.class, args);
}
public static void main(String[] args) {
System.setProperty("javax.net.ssl.trustStore", "classpath:truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

Security.addProvider(new BouncyCastleProvider());

new SpringApplicationBuilder()
.sources(AlovoaApplication.class)
.initializers(context -> {
try {
context
.getEnvironment() // preserve default sources
.getPropertySources() // preserve default sources
.addFirst(VaultPropertySource.getInstance(context));
} catch (AlovoaException e) {
throw new RuntimeException(e);
}
}
)
.run(args);
}
}
63 changes: 63 additions & 0 deletions src/main/java/com/nonononoki/alovoa/config/DatasourceConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.nonononoki.alovoa.config;

import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Properties;

@ConfigurationProperties(prefix = "spring.datasource")
@Configuration
public class DatasourceConfig {
private static final Logger logger = LoggerFactory.getLogger(DatasourceConfig.class);

@Value("${spring.datasource.url}")
private String dsUrl;

@Value("${spring.datasource.driver-class-name}")
private String dsDriver;

private URI vaultUri;
@Value("${spring.cloud.vault.uri}")
private void setVaultUri(String newVaultUri) throws URISyntaxException {
vaultUri = new URI(newVaultUri);
};

@Value("${spring.cloud.vault.token}")
private String vaultToken;

@Value("${spring.cloud.vault.templatepath}")
private String vaultTemplatePath;

@Bean
public VaultHikariDataSource getHikariDataSource() {
Properties properties = new Properties();
properties.setProperty("cachePrepStmts" , "true");
properties.setProperty("prepStmtCacheSize" , "250" );
properties.setProperty("prepStmtCacheSqlLimit" , "2048" );

VaultHikariDataSource vaultHikariDataSource = new VaultHikariDataSource();
vaultHikariDataSource.setDriverClassName(dsDriver);
vaultHikariDataSource.setJdbcUrl(dsUrl);

vaultHikariDataSource.getMysqlCredentials().setVaultUrl(vaultUri);
vaultHikariDataSource.getMysqlCredentials().setVaultToken(vaultToken);
vaultHikariDataSource.getMysqlCredentials().setVaultTemplatePath(vaultTemplatePath);
vaultHikariDataSource.getMysqlCredentials().update();

vaultHikariDataSource.setDataSourceProperties(properties);
return vaultHikariDataSource;
}

@PostConstruct
private void postConstruct() {
logger.info("postConstruct called");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.nonononoki.alovoa.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.VaultResponse;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;

@Component
public class DatasourceCredentials {
private static final Logger logger = LoggerFactory.getLogger(DatasourceCredentials.class);

private static DatasourceCredentials datasourceCredentials;
private static String username;
private static String password;

private static URI vaultUrl;
private static String vaultToken;
private static String vaultTemplatePath;

private DatasourceCredentials() {}

public static DatasourceCredentials getInstance() {

if (datasourceCredentials == null) {
logger.info(String.format("Created new %s instance", DatasourceCredentials.class));
datasourceCredentials = new DatasourceCredentials();
} else {
logger.info(String.format("Using existing %s instance", DatasourceCredentials.class));
}
return datasourceCredentials;
}

public void setVaultUrl(URI newVaultUrl) {
vaultUrl = newVaultUrl;
}

public void setVaultToken(String newVaultToken) {
vaultToken = newVaultToken;
}

public void setVaultTemplatePath(String newVaultTemplatePath) {
vaultTemplatePath = newVaultTemplatePath;
}

private VaultTemplate getVaultTemplate() {
VaultEndpoint vaultEndpoint = new VaultEndpoint();
vaultEndpoint.setHost(vaultUrl.getHost());
vaultEndpoint.setScheme(vaultUrl.getScheme());
vaultEndpoint.setPort(vaultUrl.getPort());
return new VaultTemplate(
vaultEndpoint,
new TokenAuthentication(vaultToken)
);
}

@Scheduled(fixedDelay = 300000)
public void update() {
VaultTemplate vaultTemplate = getVaultTemplate();
VaultResponse vaultResponse = vaultTemplate.read(vaultTemplatePath);
assert vaultResponse != null;
Map<String, Object> vaultResponseData = vaultResponse.getData();
assert vaultResponseData != null;
username = vaultResponseData.get("username").toString();
password = vaultResponseData.get("password").toString();
logger.debug(String.format("Database credentials updated with username %s", username));
}

public Map<String, String> getVaultCredentials(String vaultPath) {
VaultTemplate vaultTemplate = getVaultTemplate();
VaultResponse vaultResponse = vaultTemplate.read(vaultPath);
assert vaultResponse != null;
Map<String, Object> vaultResponseData = vaultResponse.getData();
Map<String, String> dataMap = new HashMap<>();
assert vaultResponseData != null;
vaultResponseData.forEach((k, v) -> {
dataMap.put(k, v.toString());
});
return dataMap;
}

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}

}
Loading