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

レッスン 21: Vault と Spring Boot および Node.js の統合

Spring Cloud Vault、Spring Boot 自動構成、PropertySource バインディング、ノード ボールトを備えた Node.js、Python hvac、Go SDK。応用パターン、シークレットゼロ問題。

🔒 D​​evSecOps — レッスン 21 レッスン 21: Vault と Spring Boot および の統合 Node.js

HashiCorp Vault の基本から上級まで

パート 6: 実用的なアプリケーションの統合

xdev.asia

1。 Vault

を使用した Spring Boot

Spring Cloud Vault

Spring Cloud Vault は、Spring Boot の PropertySource として Vault を統合します。Vault のシークレットはアプリケーション プロパティに自動的にバインドされます。

<!-- 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>

構成

# 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

コードでのシークレットの使用

// 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。 Vault

を使用した Node.js

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}`);
})();

Node.js からの Kubernetes 認証

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。 hvac

を使用した Python
# 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

パターン仕組みメリットデメリット
直接 APIアプリは Vault API を直接呼び出しますフル コントロールアプリは Vault を認識し、更新を処理する必要があります
Agent SidecarVault Agent レンダリング ファイルApp がファイルを読み取る、不明 VaultSidecar オーバーヘッド
Env InjectionSecrets を env vars に注入シンプル、ユニバーサル静的、自動回転なし
CSI ボリュームボリュームにシークレットをマウントネイティブ K8、サイドカーなし動的シークレットを制限
VSOOperator を K8s Secret に同期推奨、自動更新K8s の Secret etcd

5. Secret Zero Problem

「シークレット ゼロ」は矛盾しています。Vault からシークレットを取得するには、認証に最初のシークレットが必要です。解決策:

  • プラットフォーム ID: Kubernetes SA、AWS IAM ロール、Azure マネージド ID → シークレットは不要

  • Response Wrapping: Orchestrator はラップされた SecretID を生成し、アプリに割り当てます。アプリは一度アンラップ

  • CI/CD OIDC: GitHub Actions/GitLab CI OIDC トークン → JWT 認証、静的シークレットなし

6。概要

  • Spring Cloud Vault — 最も深い統合、自動 PropertySource バインディング、動的な認証情報のローテーション

  • Node.js (node-vault) — 柔軟なクライアント ライブラリ、非同期/待機 API

  • Python (hvac) — スクリプトおよびアプリケーション用の包括的なクライアント

  • プラットフォーム ID — シークレット ゼロの問題を最も効果的に解決します

次の記事では、Vault を Terraform、Ansible、CI/CD パイプラインと統合する方法を学びます — コードとしてのインフラストラクチャとシークレット管理の融合。