Skip to content

Commit 756a37a

Browse files
feat: implement token-exchange auth for HashiCorp Vault (#5821)
implement token-exchange auth with jwtlet
1 parent c2bec41 commit 756a37a

16 files changed

Lines changed: 794 additions & 707 deletions

extensions/common/vault/vault-hashicorp/README.md

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,37 @@ creating secrets the EDC should consume.
1313

1414
## Configuration
1515

16-
| Key | Description | Mandatory | Default |
17-
|:--------------------------------------------|:-----------------------------------------------------------------------------------------------------------------|-----------|------------------|
18-
| edc.vault.hashicorp.url | URL to connect to the HashiCorp Vault | X | | |
19-
| edc.vault.hashicorp.token | Value for [Token Authentication](https://www.vaultproject.io/docs/auth/token) with the vault | X | | |
20-
| edc.vault.hashicorp.timeout.seconds | Request timeout in seconds when contacting the vault | | `30` |
21-
| edc.vault.hashicorp.health.check.enabled | Enable health checks to ensure vault is initialized, unsealed and active | | `true` |
22-
| edc.vault.hashicorp.health.check.standby.ok | Specifies if a vault in standby is healthy. This is useful when Vault is behind a non-configurable load balancer | | `false` |
23-
| edc.vault.hashicorp.api.secret.path | Path to the [secret api](https://www.vaultproject.io/api-docs/secret/kv/kv-v1) | | `/v1/secret` |
24-
| edc.vault.hashicorp.api.health.check.path | Path to the [health api](https://www.vaultproject.io/api-docs/system/health) | | `/v1/sys/health` |
16+
| Key | Description | Mandatory | Default |
17+
|:-----------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------|-----------|------------------|
18+
| edc.vault.hashicorp.url | URL to connect to the HashiCorp Vault | X | | |
19+
| edc.vault.hashicorp.token | Value for [Token Authentication](https://www.vaultproject.io/docs/auth/token). Required unless token exchange is configured | (*) | | |
20+
| edc.vault.hashicorp.auth.tokenexchange.url | Base URL of the token-exchange service (e.g. `jwtlet`). Setting it enables token-exchange authentication | (*) | | |
21+
| edc.vault.hashicorp.auth.tokenexchange.subjecttokenpath | Path to the file holding the projected workload (ServiceAccount) token used as the subject token | | | |
22+
| edc.vault.hashicorp.auth.tokenexchange.audience | Audience requested for the exchanged token; must match the token-exchange service's configured audience | | `edcv` |
23+
| edc.vault.hashicorp.auth.tokenexchange.scope | Abstract scope (tier) requested for the exchanged token | | `read` |
24+
| edc.vault.hashicorp.auth.tokenexchange.resource | Participant context id used as the `resource` for the default vault partition (optional) | | | |
25+
| edc.vault.hashicorp.auth.jwt.role | The Vault JWT auth method role to authenticate with | | `participant` |
26+
| edc.vault.hashicorp.timeout.seconds | Request timeout in seconds when contacting the vault | | `30` |
27+
| edc.vault.hashicorp.health.check.enabled | Enable health checks to ensure vault is initialized, unsealed and active | | `true` |
28+
| edc.vault.hashicorp.health.check.standby.ok | Specifies if a vault in standby is healthy. This is useful when Vault is behind a non-configurable load balancer | | `false` |
29+
| edc.vault.hashicorp.api.secret.path | Path to the [secret api](https://www.vaultproject.io/api-docs/secret/kv/kv-v1) | | `/v1/secret` |
30+
| edc.vault.hashicorp.api.health.check.path | Path to the [health api](https://www.vaultproject.io/api-docs/system/health) | | `/v1/sys/health` |
31+
32+
(*) Exactly one authentication mechanism must be configured: either a static `edc.vault.hashicorp.token`, or token
33+
exchange (`edc.vault.hashicorp.auth.tokenexchange.url`). Both may be combined, in which case the static token is used for
34+
the default vault partition and token exchange for named (per-participant) partitions.
35+
36+
## Authentication
37+
38+
The extension supports two ways of obtaining a Vault token:
39+
40+
- **Static token** (`edc.vault.hashicorp.token`): the configured token is used directly as the `X-Vault-Token`.
41+
- **Token exchange** (`edc.vault.hashicorp.auth.tokenexchange.*`): the connector reads its projected Kubernetes
42+
ServiceAccount token from `subjecttokenpath`, exchanges it at the token-exchange service (RFC 8693, e.g. `jwtlet`) for
43+
a participant-scoped JWT, and presents that JWT to Vault's [JWT auth method](https://developer.hashicorp.com/vault/docs/auth/jwt)
44+
(`v1/auth/jwt/login`, role `edc.vault.hashicorp.auth.jwt.role`). For named vault partitions the participant context id
45+
is used as the exchange `resource`, so the issued Vault token is scoped to that participant. The resulting Vault token
46+
is cached until shortly before it expires.
2547

2648
## Health Check
2749

extensions/common/vault/vault-hashicorp/src/main/java/org/eclipse/edc/vault/hashicorp/HashicorpVault.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.eclipse.edc.spi.result.Result;
2323
import org.eclipse.edc.spi.security.Vault;
2424
import org.eclipse.edc.vault.hashicorp.client.HashicorpVaultConfig;
25-
import org.eclipse.edc.vault.hashicorp.spi.auth.HashicorpVaultTokenProvider;
25+
import org.eclipse.edc.vault.hashicorp.spi.auth.HashicorpVaultTokenProviderFactory;
2626
import org.jetbrains.annotations.NotNull;
2727
import org.jetbrains.annotations.Nullable;
2828

@@ -37,18 +37,18 @@ class HashicorpVault implements Vault {
3737
private final ParticipantContextConfig participantContextConfig;
3838
private final Monitor monitor;
3939
private final HashicorpVaultConfig vaultConfig;
40-
private final HashicorpVaultTokenProvider defaultTokenProvider;
40+
private final HashicorpVaultTokenProviderFactory tokenProviderFactory;
4141
private final EdcHttpClient edcHttpClient;
4242
private final ObjectMapper mapper;
4343

4444
HashicorpVault(ParticipantContextConfig participantContextConfig,
4545
Monitor monitor,
46-
HashicorpVaultConfig vaultConfig, HashicorpVaultTokenProvider defaultTokenProvider,
46+
HashicorpVaultConfig vaultConfig, HashicorpVaultTokenProviderFactory tokenProviderFactory,
4747
EdcHttpClient edcHttpClient) {
4848
this.participantContextConfig = participantContextConfig;
4949
this.monitor = monitor;
5050
this.vaultConfig = vaultConfig;
51-
this.defaultTokenProvider = defaultTokenProvider;
51+
this.tokenProviderFactory = tokenProviderFactory;
5252
this.edcHttpClient = edcHttpClient;
5353
this.mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
5454

@@ -99,7 +99,7 @@ public Result<Void> deleteSecret(String vaultPartition, String key) {
9999

100100
var settings = forParticipant(vaultPartition, participantContextConfig);
101101
if (settings != null) {
102-
return new HashicorpVaultClient(monitor, settings.config(), edcHttpClient, mapper, settings.tokenProvider(edcHttpClient));
102+
return new HashicorpVaultClient(monitor, settings.config(), edcHttpClient, mapper, tokenProviderFactory.create(vaultPartition));
103103
}
104104

105105
if (vaultConfig.isAllowFallback()) {
@@ -112,7 +112,7 @@ public Result<Void> deleteSecret(String vaultPartition, String key) {
112112
* creates a new HashicorpVaultClient with the "global" configuration / auth-settings taken from the runtime configuration.
113113
*/
114114
private HashicorpVaultClient createDefault() {
115-
return new HashicorpVaultClient(monitor, vaultConfig, edcHttpClient, mapper, defaultTokenProvider);
115+
return new HashicorpVaultClient(monitor, vaultConfig, edcHttpClient, mapper, tokenProviderFactory.create(null));
116116
}
117117

118118
}

extensions/common/vault/vault-hashicorp/src/main/java/org/eclipse/edc/vault/hashicorp/HashicorpVaultExtension.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import org.eclipse.edc.vault.hashicorp.client.HashicorpVaultHealthService;
3434
import org.eclipse.edc.vault.hashicorp.client.HashicorpVaultTokenRenewService;
3535
import org.eclipse.edc.vault.hashicorp.client.HashicorpVaultTokenRenewTask;
36-
import org.eclipse.edc.vault.hashicorp.spi.auth.HashicorpVaultTokenProvider;
36+
import org.eclipse.edc.vault.hashicorp.spi.auth.HashicorpVaultTokenProviderFactory;
3737
import org.jetbrains.annotations.NotNull;
3838

3939
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
@@ -50,7 +50,7 @@ public class HashicorpVaultExtension implements ServiceExtension {
5050
private ExecutorInstrumentation executorInstrumentation;
5151

5252
@Inject
53-
private HashicorpVaultTokenProvider tokenProvider;
53+
private HashicorpVaultTokenProviderFactory tokenProviderFactory;
5454

5555
@Inject
5656
private ParticipantContextConfig participantContextConfig;
@@ -72,19 +72,19 @@ public String name() {
7272

7373
@Provider
7474
public Vault hashicorpVault() {
75-
return new HashicorpVault(participantContextConfig, monitor, defaultVaultConfig, tokenProvider, httpClient);
75+
return new HashicorpVault(participantContextConfig, monitor, defaultVaultConfig, tokenProviderFactory, httpClient);
7676
}
7777

7878
@Provider
7979
public SignatureService signatureService() {
80-
return new HashicorpVaultSignatureService(monitor, defaultVaultConfig, httpClient, MAPPER, tokenProvider);
80+
return new HashicorpVaultSignatureService(monitor, defaultVaultConfig, httpClient, MAPPER, tokenProviderFactory.create(null));
8181
}
8282

8383
@Override
8484
public void initialize(ServiceExtensionContext context) {
8585
monitor = context.getMonitor().withPrefix(NAME);
86-
87-
var tokenRenewService = new HashicorpVaultTokenRenewService(httpClient, MAPPER, defaultVaultConfig, tokenProvider, monitor);
86+
87+
var tokenRenewService = new HashicorpVaultTokenRenewService(httpClient, MAPPER, defaultVaultConfig, tokenProviderFactory.create(null), monitor);
8888
tokenRenewalTask = new HashicorpVaultTokenRenewTask(
8989
NAME,
9090
executorInstrumentation,
@@ -96,7 +96,7 @@ public void initialize(ServiceExtensionContext context) {
9696
@Provider
9797
public @NotNull HashicorpVaultHealthService createHealthService() {
9898
if (healthService == null) {
99-
healthService = new HashicorpVaultHealthService(httpClient, defaultVaultConfig, tokenProvider);
99+
healthService = new HashicorpVaultHealthService(httpClient, defaultVaultConfig, tokenProviderFactory.create(null));
100100
}
101101
return healthService;
102102
}
Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2025 Metaform Systems, Inc.
2+
* Copyright (c) 2025 Cofinity-X
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Apache License, Version 2.0 which is available at
@@ -8,7 +8,7 @@
88
* SPDX-License-Identifier: Apache-2.0
99
*
1010
* Contributors:
11-
* Metaform Systems, Inc. - initial API and implementation
11+
* Cofinity-X - initial API and implementation
1212
*
1313
*/
1414

@@ -17,23 +17,21 @@
1717
import com.fasterxml.jackson.core.JsonProcessingException;
1818
import com.fasterxml.jackson.databind.DeserializationFeature;
1919
import com.fasterxml.jackson.databind.ObjectMapper;
20-
import org.eclipse.edc.http.spi.EdcHttpClient;
2120
import org.eclipse.edc.participantcontext.spi.config.ParticipantContextConfig;
2221
import org.eclipse.edc.spi.EdcException;
2322
import org.eclipse.edc.util.string.StringUtils;
24-
import org.eclipse.edc.vault.hashicorp.auth.HashicorpJwtTokenProvider;
25-
import org.eclipse.edc.vault.hashicorp.auth.HashicorpVaultTokenProviderImpl;
2623
import org.eclipse.edc.vault.hashicorp.client.HashicorpVaultConfig;
27-
import org.eclipse.edc.vault.hashicorp.client.HashicorpVaultCredentials;
28-
import org.eclipse.edc.vault.hashicorp.spi.auth.HashicorpVaultTokenProvider;
2924
import org.jetbrains.annotations.Nullable;
3025

3126
import static org.eclipse.edc.vault.hashicorp.VaultConstants.VAULT_CONFIG;
3227

3328
/**
34-
* POJO that contains a config object and a credentials object. This is intended to be serialized, e.g., as DTO in API calls.
29+
* POJO that contains the per-partition vault configuration. This is intended to be serialized, e.g., as DTO in API calls.
30+
* Authentication is configured globally (see {@code HashicorpVaultAuthenticationExtension}); the only per-partition value
31+
* relevant to authentication is the partition key itself (the participant context id), which is derived from the
32+
* partition and used as the token-exchange {@code resource}.
3533
*/
36-
public record HashicorpVaultSettings(HashicorpVaultConfig config, HashicorpVaultCredentials credentials) {
34+
public record HashicorpVaultSettings(HashicorpVaultConfig config) {
3735

3836
private static final ObjectMapper MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
3937

@@ -57,25 +55,4 @@ public record HashicorpVaultSettings(HashicorpVaultConfig config, HashicorpVault
5755
}
5856
}
5957

60-
/**
61-
* Generates a {@link HashicorpJwtTokenProvider} for the given settings. If a static token is provided in the credentials,
62-
* a simple token provider is returned. If client credentials are provided, a JWT token provider is returned.
63-
*
64-
* @param edcHttpClient An {@link EdcHttpClient} instance to be used for HTTP requests. This is only needed for the JWT token provider.
65-
* @return A {@link HashicorpVaultTokenProvider} instance.
66-
*/
67-
public HashicorpVaultTokenProvider tokenProvider(EdcHttpClient edcHttpClient) {
68-
if (credentials.getToken() != null) {
69-
return new HashicorpVaultTokenProviderImpl(credentials.getToken());
70-
}
71-
72-
return HashicorpJwtTokenProvider.Builder.newInstance()
73-
.clientId(credentials.getClientId())
74-
.clientSecret(credentials.getClientSecret())
75-
.tokenUrl(credentials.getTokenUrl())
76-
.vaultUrl(config.getVaultUrl())
77-
.httpClient(edcHttpClient)
78-
.build();
79-
}
80-
8158
}

0 commit comments

Comments
 (0)