0
# Vault Integration
1
2
The vault integration framework provides secure secrets management by integrating with external vault systems. It enables Keycloak to retrieve sensitive configuration values like passwords, API keys, and certificates from secure storage.
3
4
## Core Vault Interfaces
5
6
### VaultProvider
7
8
Provider interface for vault implementations.
9
10
```java { .api }
11
public interface VaultProvider extends Provider {
12
/**
13
* Obtains a raw secret from the vault.
14
*
15
* @param vaultSecretId the vault secret identifier
16
* @return vault raw secret or empty optional
17
*/
18
VaultRawSecret obtainSecret(String vaultSecretId);
19
}
20
```
21
22
### VaultTranscriber
23
24
Transcribes vault references in configuration values to actual secret values.
25
26
```java { .api }
27
public interface VaultTranscriber {
28
/**
29
* Transcribes a value, replacing vault references with actual secret values.
30
*
31
* @param value the value potentially containing vault references
32
* @return transcribed value with secrets resolved
33
*/
34
String transcribe(String value);
35
}
36
```
37
38
## Vault Secret Types
39
40
### VaultRawSecret
41
42
Base interface for raw vault secrets.
43
44
```java { .api }
45
public interface VaultRawSecret extends AutoCloseable {
46
/**
47
* Gets the raw secret as byte array.
48
*
49
* @return optional raw secret bytes
50
*/
51
Optional<byte[]> get();
52
53
/**
54
* Gets the raw secret as byte array or default value.
55
*
56
* @param defaultValue default value if secret not found
57
* @return raw secret bytes or default
58
*/
59
default byte[] getOrDefault(byte[] defaultValue) {
60
return get().orElse(defaultValue);
61
}
62
63
@Override
64
void close();
65
}
66
```
67
68
### VaultCharSecret
69
70
Interface for character-based vault secrets.
71
72
```java { .api }
73
public interface VaultCharSecret extends VaultRawSecret {
74
/**
75
* Gets the secret as character array.
76
*
77
* @return optional character array
78
*/
79
Optional<char[]> getAsArray();
80
81
/**
82
* Gets the secret as character array or default value.
83
*
84
* @param defaultValue default value if secret not found
85
* @return character array or default
86
*/
87
default char[] getAsArray(char[] defaultValue) {
88
return getAsArray().orElse(defaultValue);
89
}
90
91
@Override
92
default Optional<byte[]> get() {
93
return getAsArray().map(chars -> {
94
byte[] bytes = new byte[chars.length];
95
for (int i = 0; i < chars.length; i++) {
96
bytes[i] = (byte) chars[i];
97
}
98
return bytes;
99
});
100
}
101
}
102
```
103
104
### VaultStringSecret
105
106
Interface for string-based vault secrets.
107
108
```java { .api }
109
public interface VaultStringSecret extends VaultCharSecret {
110
/**
111
* Gets the secret as string.
112
*
113
* @return optional string value
114
*/
115
Optional<String> get();
116
117
/**
118
* Gets the secret as string or default value.
119
*
120
* @param defaultValue default value if secret not found
121
* @return string value or default
122
*/
123
default String getOrDefault(String defaultValue) {
124
return get().orElse(defaultValue);
125
}
126
127
@Override
128
default Optional<char[]> getAsArray() {
129
return get().map(String::toCharArray);
130
}
131
}
132
```
133
134
## Vault Key Resolution
135
136
### VaultKeyResolver
137
138
Resolves vault keys from configuration values.
139
140
```java { .api }
141
public interface VaultKeyResolver {
142
/**
143
* Checks if a value contains a vault key reference.
144
*
145
* @param value the value to check
146
* @return true if value contains vault reference
147
*/
148
boolean isVaultKey(String value);
149
150
/**
151
* Extracts the vault key from a vault reference.
152
*
153
* @param value the vault reference value
154
* @return vault key or null if not a vault reference
155
*/
156
String getVaultKey(String value);
157
158
/**
159
* Creates a vault reference from a key.
160
*
161
* @param key the vault key
162
* @return vault reference string
163
*/
164
String createVaultReference(String key);
165
}
166
```
167
168
## Vault SPI
169
170
### VaultSpi
171
172
SPI definition for vault providers.
173
174
```java { .api }
175
public class VaultSpi implements Spi {
176
@Override
177
public boolean isInternal() {
178
return true;
179
}
180
181
@Override
182
public String getName() {
183
return "vault";
184
}
185
186
@Override
187
public Class<? extends Provider> getProviderClass() {
188
return VaultProvider.class;
189
}
190
191
@Override
192
public Class<? extends ProviderFactory> getProviderFactoryClass() {
193
return VaultProviderFactory.class;
194
}
195
}
196
```
197
198
### VaultProviderFactory
199
200
Factory for creating vault provider instances.
201
202
```java { .api }
203
public interface VaultProviderFactory extends ProviderFactory<VaultProvider> {
204
/**
205
* Creates a vault provider instance.
206
*
207
* @param session Keycloak session
208
* @return vault provider
209
*/
210
@Override
211
VaultProvider create(KeycloakSession session);
212
213
/**
214
* Gets the provider ID.
215
*
216
* @return provider ID
217
*/
218
@Override
219
String getId();
220
}
221
```
222
223
## Usage Examples
224
225
### Creating a Custom Vault Provider
226
227
```java
228
public class HashiCorpVaultProvider implements VaultProvider {
229
private final VaultClient vaultClient;
230
private final String mountPath;
231
232
public HashiCorpVaultProvider(VaultClient vaultClient, String mountPath) {
233
this.vaultClient = vaultClient;
234
this.mountPath = mountPath;
235
}
236
237
@Override
238
public VaultRawSecret obtainSecret(String vaultSecretId) {
239
try {
240
// Parse vault secret ID (e.g., "secret/myapp/database")
241
String[] parts = vaultSecretId.split("/");
242
if (parts.length < 2) {
243
return new EmptyVaultSecret();
244
}
245
246
String path = vaultSecretId;
247
if (!path.startsWith(mountPath)) {
248
path = mountPath + "/" + path;
249
}
250
251
// Retrieve secret from HashiCorp Vault
252
Map<String, Object> secretData = vaultClient.read(path);
253
if (secretData == null || secretData.isEmpty()) {
254
return new EmptyVaultSecret();
255
}
256
257
// Return the secret value
258
Object secretValue = secretData.get("value");
259
if (secretValue == null) {
260
// Try default field names
261
secretValue = secretData.get("password");
262
if (secretValue == null) {
263
secretValue = secretData.get("secret");
264
}
265
}
266
267
if (secretValue != null) {
268
return new StringVaultSecret(secretValue.toString());
269
}
270
271
return new EmptyVaultSecret();
272
273
} catch (Exception e) {
274
logger.error("Failed to retrieve secret: " + vaultSecretId, e);
275
return new EmptyVaultSecret();
276
}
277
}
278
279
@Override
280
public void close() {
281
if (vaultClient != null) {
282
vaultClient.close();
283
}
284
}
285
286
// Inner class for string secrets
287
private static class StringVaultSecret implements VaultStringSecret {
288
private final String secret;
289
private volatile boolean closed = false;
290
291
public StringVaultSecret(String secret) {
292
this.secret = secret;
293
}
294
295
@Override
296
public Optional<String> get() {
297
if (closed) {
298
throw new IllegalStateException("Secret has been closed");
299
}
300
return Optional.ofNullable(secret);
301
}
302
303
@Override
304
public void close() {
305
closed = true;
306
// Clear the secret from memory (not possible with String, but shown for demonstration)
307
}
308
}
309
310
// Inner class for empty secrets
311
private static class EmptyVaultSecret implements VaultStringSecret {
312
@Override
313
public Optional<String> get() {
314
return Optional.empty();
315
}
316
317
@Override
318
public void close() {
319
// Nothing to close
320
}
321
}
322
}
323
```
324
325
### Creating a Vault Provider Factory
326
327
```java
328
public class HashiCorpVaultProviderFactory implements VaultProviderFactory {
329
public static final String PROVIDER_ID = "hashicorp-vault";
330
331
private volatile VaultClient vaultClient;
332
333
@Override
334
public VaultProvider create(KeycloakSession session) {
335
if (vaultClient == null) {
336
synchronized (this) {
337
if (vaultClient == null) {
338
vaultClient = initializeVaultClient();
339
}
340
}
341
}
342
return new HashiCorpVaultProvider(vaultClient, getMountPath());
343
}
344
345
@Override
346
public void init(Config.Scope config) {
347
// Initialize configuration
348
}
349
350
@Override
351
public void postInit(KeycloakSessionFactory factory) {
352
// Post-initialization
353
}
354
355
@Override
356
public void close() {
357
if (vaultClient != null) {
358
vaultClient.close();
359
}
360
}
361
362
@Override
363
public String getId() {
364
return PROVIDER_ID;
365
}
366
367
@Override
368
public List<ProviderConfigProperty> getConfigMetadata() {
369
return Arrays.asList(
370
new ProviderConfigProperty("vault.url", "Vault URL", "HashiCorp Vault server URL",
371
ProviderConfigProperty.STRING_TYPE, "https://vault.example.com:8200"),
372
new ProviderConfigProperty("vault.token", "Vault Token", "Authentication token for Vault",
373
ProviderConfigProperty.PASSWORD, null),
374
new ProviderConfigProperty("vault.mount-path", "Mount Path", "Vault mount path for secrets",
375
ProviderConfigProperty.STRING_TYPE, "secret"),
376
new ProviderConfigProperty("vault.namespace", "Namespace", "Vault namespace (Enterprise feature)",
377
ProviderConfigProperty.STRING_TYPE, null),
378
new ProviderConfigProperty("vault.timeout", "Timeout", "Connection timeout in seconds",
379
ProviderConfigProperty.STRING_TYPE, "30")
380
);
381
}
382
383
private VaultClient initializeVaultClient() {
384
String vaultUrl = getConfig().get("vault.url", "https://vault.example.com:8200");
385
String vaultToken = getConfig().get("vault.token");
386
String namespace = getConfig().get("vault.namespace");
387
int timeout = getConfig().getInt("vault.timeout", 30);
388
389
VaultConfig vaultConfig = new VaultConfig()
390
.address(vaultUrl)
391
.token(vaultToken)
392
.openTimeout(timeout)
393
.readTimeout(timeout);
394
395
if (namespace != null && !namespace.isEmpty()) {
396
vaultConfig.nameSpace(namespace);
397
}
398
399
return new Vault(vaultConfig).logical();
400
}
401
402
private String getMountPath() {
403
return getConfig().get("vault.mount-path", "secret");
404
}
405
406
private Config.Scope getConfig() {
407
// Return configuration scope
408
return Config.scope("vault", PROVIDER_ID);
409
}
410
}
411
```
412
413
### Using Vault Transcriber
414
415
```java
416
// Using vault transcriber to resolve secrets in configuration
417
try (KeycloakSession session = sessionFactory.create()) {
418
VaultTranscriber transcriber = session.vault();
419
420
// Configuration values with vault references
421
String dbPassword = "${vault.secret/myapp/database}";
422
String apiKey = "${vault.api-keys/external-service}";
423
String certificateKeystore = "${vault.certificates/ssl-keystore}";
424
425
// Transcribe vault references to actual secret values
426
String actualDbPassword = transcriber.transcribe(dbPassword);
427
String actualApiKey = transcriber.transcribe(apiKey);
428
String actualKeystorePassword = transcriber.transcribe(certificateKeystore);
429
430
// Use the resolved secrets
431
DataSource dataSource = createDataSource("jdbc:postgresql://localhost/mydb",
432
"dbuser", actualDbPassword);
433
434
HttpClient httpClient = createHttpClient(actualApiKey);
435
436
KeyStore keyStore = loadKeyStore("keystore.p12", actualKeystorePassword);
437
}
438
```
439
440
### Direct Vault Provider Usage
441
442
```java
443
// Direct usage of vault provider
444
try (KeycloakSession session = sessionFactory.create()) {
445
VaultProvider vaultProvider = session.getProvider(VaultProvider.class);
446
447
// Retrieve database password
448
try (VaultStringSecret dbSecret = (VaultStringSecret) vaultProvider.obtainSecret("secret/myapp/database")) {
449
String dbPassword = dbSecret.getOrDefault("defaultPassword");
450
451
// Use the password
452
Connection connection = DriverManager.getConnection(
453
"jdbc:postgresql://localhost/mydb", "dbuser", dbPassword);
454
}
455
456
// Retrieve API key
457
try (VaultStringSecret apiSecret = (VaultStringSecret) vaultProvider.obtainSecret("api-keys/payment-gateway")) {
458
String apiKey = apiSecret.get().orElse(null);
459
if (apiKey != null) {
460
PaymentGatewayClient client = new PaymentGatewayClient(apiKey);
461
}
462
}
463
464
// Retrieve certificate data
465
try (VaultRawSecret certSecret = vaultProvider.obtainSecret("certificates/client-cert")) {
466
byte[] certData = certSecret.getOrDefault(new byte[0]);
467
if (certData.length > 0) {
468
X509Certificate certificate = loadCertificate(certData);
469
}
470
}
471
}
472
```
473
474
### Custom Vault Key Resolver
475
476
```java
477
public class CustomVaultKeyResolver implements VaultKeyResolver {
478
private static final Pattern VAULT_PATTERN = Pattern.compile("\\$\\{vault\\.([^}]+)\\}");
479
private static final String VAULT_PREFIX = "${vault.";
480
private static final String VAULT_SUFFIX = "}";
481
482
@Override
483
public boolean isVaultKey(String value) {
484
return value != null && value.startsWith(VAULT_PREFIX) && value.endsWith(VAULT_SUFFIX);
485
}
486
487
@Override
488
public String getVaultKey(String value) {
489
if (!isVaultKey(value)) {
490
return null;
491
}
492
493
Matcher matcher = VAULT_PATTERN.matcher(value);
494
if (matcher.matches()) {
495
return matcher.group(1);
496
}
497
498
return null;
499
}
500
501
@Override
502
public String createVaultReference(String key) {
503
if (key == null || key.isEmpty()) {
504
throw new IllegalArgumentException("Vault key cannot be null or empty");
505
}
506
507
return VAULT_PREFIX + key + VAULT_SUFFIX;
508
}
509
}
510
```
511
512
### Environment-Specific Vault Configuration
513
514
```java
515
// Configure vault provider based on environment
516
public class EnvironmentVaultProviderFactory implements VaultProviderFactory {
517
public static final String PROVIDER_ID = "environment-vault";
518
519
@Override
520
public VaultProvider create(KeycloakSession session) {
521
String environment = System.getProperty("keycloak.environment", "development");
522
523
switch (environment.toLowerCase()) {
524
case "production":
525
return createProductionVaultProvider();
526
case "staging":
527
return createStagingVaultProvider();
528
case "development":
529
default:
530
return createDevelopmentVaultProvider();
531
}
532
}
533
534
private VaultProvider createProductionVaultProvider() {
535
// Use HashiCorp Vault for production
536
return new HashiCorpVaultProvider(
537
createVaultClient("https://vault.prod.example.com:8200"),
538
"prod-secrets"
539
);
540
}
541
542
private VaultProvider createStagingVaultProvider() {
543
// Use HashiCorp Vault for staging
544
return new HashiCorpVaultProvider(
545
createVaultClient("https://vault.staging.example.com:8200"),
546
"staging-secrets"
547
);
548
}
549
550
private VaultProvider createDevelopmentVaultProvider() {
551
// Use file-based vault for development
552
return new FileBasedVaultProvider("/etc/keycloak/dev-secrets");
553
}
554
555
@Override
556
public String getId() {
557
return PROVIDER_ID;
558
}
559
}
560
561
// Simple file-based vault for development
562
public class FileBasedVaultProvider implements VaultProvider {
563
private final Path secretsDirectory;
564
565
public FileBasedVaultProvider(String secretsPath) {
566
this.secretsDirectory = Paths.get(secretsPath);
567
}
568
569
@Override
570
public VaultRawSecret obtainSecret(String vaultSecretId) {
571
try {
572
Path secretFile = secretsDirectory.resolve(vaultSecretId + ".txt");
573
if (!Files.exists(secretFile)) {
574
return new EmptyVaultSecret();
575
}
576
577
String content = Files.readString(secretFile, StandardCharsets.UTF_8).trim();
578
return new StringVaultSecret(content);
579
580
} catch (IOException e) {
581
logger.error("Failed to read secret file: " + vaultSecretId, e);
582
return new EmptyVaultSecret();
583
}
584
}
585
586
@Override
587
public void close() {
588
// Nothing to close for file-based implementation
589
}
590
}
591
```
592
593
### Vault Secret Caching
594
595
```java
596
// Vault provider with caching for better performance
597
public class CachedVaultProvider implements VaultProvider {
598
private final VaultProvider delegate;
599
private final Cache<String, VaultRawSecret> secretCache;
600
private final Duration cacheExpiration;
601
602
public CachedVaultProvider(VaultProvider delegate, Duration cacheExpiration) {
603
this.delegate = delegate;
604
this.cacheExpiration = cacheExpiration;
605
this.secretCache = Caffeine.newBuilder()
606
.expireAfterWrite(cacheExpiration)
607
.maximumSize(1000)
608
.removalListener((key, value, cause) -> {
609
if (value instanceof VaultRawSecret) {
610
((VaultRawSecret) value).close();
611
}
612
})
613
.build();
614
}
615
616
@Override
617
public VaultRawSecret obtainSecret(String vaultSecretId) {
618
return secretCache.get(vaultSecretId, key -> {
619
VaultRawSecret secret = delegate.obtainSecret(key);
620
621
// Convert to cacheable secret
622
if (secret instanceof VaultStringSecret) {
623
VaultStringSecret stringSecret = (VaultStringSecret) secret;
624
String value = stringSecret.getOrDefault(null);
625
secret.close(); // Close original
626
627
return value != null ? new CacheableStringSecret(value) : new EmptyVaultSecret();
628
}
629
630
return secret;
631
});
632
}
633
634
@Override
635
public void close() {
636
secretCache.invalidateAll();
637
delegate.close();
638
}
639
640
private static class CacheableStringSecret implements VaultStringSecret {
641
private final String value;
642
643
public CacheableStringSecret(String value) {
644
this.value = value;
645
}
646
647
@Override
648
public Optional<String> get() {
649
return Optional.ofNullable(value);
650
}
651
652
@Override
653
public void close() {
654
// Nothing to close for cached values
655
}
656
}
657
}
658
```