Chuyển đến nội dung chính

Lesson 21: Integrating Vault with Spring Boot and Node.js

Spring Cloud Vault, Spring Boot auto-configuration, PropertySource binding, Node.js with node-vault, Python hvac, Go SDK. Application patterns, Secret Zero problem.

🔒 DevSecOps — Lesson 21 Lesson 21: Integrating Vault with Spring Boot and Node.js

HashiCorp Vault from Basic to Advanced

Part 6: Integrating practical applications

xdev.asia

1. Spring Boot with Vault

Spring Cloud Vault

Spring Cloud Vault integrates Vault as a PropertySource in Spring Boot — secrets from Vault automatically bind to application properties.

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-core</artifactId>
</dependency>

Configuration

# application.yml
spring:
  cloud:
    vault:
      uri: https://vault.company.com:8200
      authentication: KUBERNETES
      kubernetes:
        role: webapp
        kubernetes-path: auth/kubernetes
        service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
      kv:
        enabled: true
        backend: secret
        default-context: production/webapp
        profile-separator: /
      database:
        enabled: true
        role: webapp-db
        backend: database
      config:
        lifecycle:
          enabled: true
          min-renewal: 10s
          expiry-threshold: 1m

AppRole Authentication

spring:
  cloud:
    vault:
      uri: https://vault.company.com:8200
      authentication: APPROLE
      app-role:
        role-id: ${VAULT_ROLE_ID}
        secret-id: ${VAULT_SECRET_ID}
        app-role-path: auth/approle

Using Secrets in Code

// Secrets từ Vault tự động binding vào @Value
@Service
public class DatabaseService {

    @Value("${db.username}")
    private String dbUsername;

    @Value("${db.password}")
    private String dbPassword;

    @Value("${api.key}")
    private String apiKey;
}

// Hoặc dùng @ConfigurationProperties
@Configuration
@ConfigurationProperties(prefix = "db")
public class DatabaseConfig {
    private String host;
    private int port;
    private String username;
    private String password;
    // getters, setters
}

Dynamic Database Credentials

@Configuration
public class VaultDatabaseConfig {

    @Bean
    public DataSource dataSource(
            SecretLeaseContainer leaseContainer,
            @Value("${spring.datasource.url}") String url) {

        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(url);

        // Lắng nghe secret rotation
        leaseContainer.addLeaseListener(event -> {
            if (event.getSource().getPath().equals("database/creds/webapp-db")) {
                if (event instanceof SecretLeaseExpiredEvent ||
                    event instanceof SecretLeaseCreatedEvent) {

                    Map<String, Object> secrets = event.getSource().getSecrets();
                    dataSource.setUsername((String) secrets.get("username"));
                    dataSource.setPassword((String) secrets.get("password"));

                    // Soft evict connections
                    dataSource.getHikariPoolMXBean()
                        .softEvictConnections();
                }
            }
        });

        return dataSource;
    }
}

Transit Encryption trong Java

@Service
public class EncryptionService {

    private final VaultTransitOperations transit;

    public EncryptionService(VaultTemplate vaultTemplate) {
        this.transit = vaultTemplate.opsForTransit();
    }

    public String encrypt(String plaintext) {
        return transit.encrypt("my-app-key", plaintext);
    }

    public String decrypt(String ciphertext) {
        return transit.decrypt("my-app-key", ciphertext);
    }

    // Batch encryption
    public List<VaultEncryptionResult> encryptBatch(List<String> plaintexts) {
        List<Plaintext> batch = plaintexts.stream()
            .map(Plaintext::of)
            .collect(Collectors.toList());
        return transit.encrypt("my-app-key", batch);
    }
}

2. Node.js with Vault

node-vault Client

// npm install node-vault
const vault = require('node-vault')({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR || 'https://vault.company.com:8200',
});

// Login với AppRole
async function login() {
  const result = await vault.approleLogin({
    role_id: process.env.VAULT_ROLE_ID,
    secret_id: process.env.VAULT_SECRET_ID,
  });
  vault.token = result.auth.client_token;
  return result;
}

// Đọc KV secret
async function getSecret(path) {
  const result = await vault.read(`secret/data/${path}`);
  return result.data.data;
}

// Sinh database credentials
async function getDatabaseCreds(role) {
  const result = await vault.read(`database/creds/${role}`);
  return {
    username: result.data.username,
    password: result.data.password,
    leaseId: result.lease_id,
    leaseDuration: result.lease_duration,
  };
}

// Encrypt với Transit
async function encrypt(plaintext) {
  const result = await vault.write('transit/encrypt/my-key', {
    plaintext: Buffer.from(plaintext).toString('base64'),
  });
  return result.data.ciphertext;
}

// Main
(async () => {
  await login();
  const dbConfig = await getSecret('production/db');
  console.log(`Connecting to ${dbConfig.host}:${dbConfig.port}`);
})();

Kubernetes Auth from Node.js

const fs = require('fs');

async function k8sLogin() {
  const jwt = fs.readFileSync(
    '/var/run/secrets/kubernetes.io/serviceaccount/token',
    'utf8'
  );

  const result = await vault.kubernetesLogin({
    role: 'webapp',
    jwt: jwt,
  });

  vault.token = result.auth.client_token;
  return result;
}

3. Python with hvac

# pip install hvac
import hvac

client = hvac.Client(url='https://vault.company.com:8200')

# AppRole login
client.auth.approle.login(
    role_id=os.environ['VAULT_ROLE_ID'],
    secret_id=os.environ['VAULT_SECRET_ID'],
)

# KV v2
secret = client.secrets.kv.v2.read_secret_version(
    path='production/db',
    mount_point='secret',
)
db_password = secret['data']['data']['password']

# Database dynamic credentials
creds = client.secrets.database.generate_credentials(
    name='app-role',
    mount_point='database',
)
print(f"Username: {creds['data']['username']}")
print(f"Password: {creds['data']['password']}")

# Transit encrypt
result = client.secrets.transit.encrypt_data(
    name='my-key',
    plaintext=base64.b64encode(b'sensitive data').decode(),
)
ciphertext = result['data']['ciphertext']

4. Application Patterns

PatternHow it worksAdvantagesDisadvantages
Direct APIApp calls Vault API directlyFull controlApp must know Vault, handle renewal
Agent SidecarVault Agent render filesApp reads files, unknown VaultSidecar overhead
Env InjectionSecrets inject into env varsSimple, universalStatic, no auto-rotate
CSI VolumeSecrets mount to volumeNative K8s, no sidecarRestrict dynamic secrets
VSOOperator sync into K8s SecretRecommended, auto-refreshSecret in K8s etcd

5. Secret Zero Problem

"Secret Zero" is a paradox: to get secrets from the Vault, a first secret is needed for authentication. Solutions:

  • Platform Identity: Kubernetes SA, AWS IAM Role, Azure Managed Identity → no secret needed

  • Response Wrapping: Orchestrator generates wrapped SecretID, assigned to app. App unwrap once

  • CI/CD OIDC: GitHub Actions/GitLab CI OIDC tokens → JWT auth, no static secrets

6. Summary

  • Spring Cloud Vault — deepest integration, auto PropertySource binding, dynamic credential rotation

  • Node.js (node-vault) — flexible client library, async/await API

  • Python (hvac) — comprehensive client for scripting and applications

  • Platform Identity — solve the Secret Zero problem most effectively

The next article will learn how to integrate Vault with Terraform, Ansible and CI/CD Pipelines — Infrastructure as Code meets Secret Management.