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

Bài 21: Tích hợp Vault với Spring Boot và Node.js

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

🔒 DevSecOps — Bài 21 Bài 21: Tích hợp Vault với Spring Boot và Node.js

HashiCorp Vault từ Cơ bản đến Nâng cao

Phần 6: Tích hợp ứng dụng thực tế

xdev.asia

1. Spring Boot với Vault

Spring Cloud Vault

Spring Cloud Vault tích hợp Vault như một PropertySource trong Spring Boot — secrets từ Vault tự động binding vào 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>

Cấu hình

# 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

Sử dụng Secrets trong 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 với 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 từ 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 với 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

PatternCách hoạt độngƯu điểmNhược điểm
Direct APIApp gọi Vault API trực tiếpFull controlApp phải biết Vault, handle renewal
Agent SidecarVault Agent render filesApp đọc file, không biết VaultSidecar overhead
Env InjectionSecrets inject vào env varsĐơn giản, universalStatic, không auto-rotate
CSI VolumeSecrets mount vào volumeNative K8s, không sidecarHạn chế dynamic secrets
VSOOperator sync vào K8s SecretRecommended, auto-refreshSecret trong K8s etcd

5. Secret Zero Problem

"Secret Zero" là paradox: để lấy secrets từ Vault, cần một secret đầu tiên để xác thực. Các giải pháp:

  • Platform Identity: Kubernetes SA, AWS IAM Role, Azure Managed Identity → không cần secret

  • Response Wrapping: Orchestrator sinh wrapped SecretID, giao cho app. App unwrap 1 lần

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

6. Tổng kết

  • Spring Cloud Vault — tích hợp sâu nhất, auto PropertySource binding, dynamic credential rotation

  • Node.js (node-vault) — client library linh hoạt, async/await API

  • Python (hvac) — comprehensive client cho scripting và applications

  • Platform Identity — giải quyết Secret Zero problem hiệu quả nhất

Bài tiếp theo sẽ tìm hiểu cách tích hợp Vault với Terraform, Ansible và CI/CD Pipelines — Infrastructure as Code meets Secret Management.