Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## 2.11.0-beta.1 (Unreleased)

### Features Added
- Added support for Azure Workload Identity authentication for Azure Kubernetes Service (AKS) workloads.
- Automatically detects and uses federated token authentication when `AZURE_FEDERATED_TOKEN_FILE`, `AZURE_CLIENT_ID`, and `AZURE_TENANT_ID` environment variables are present.
- Provides credential-free authentication for AKS pods configured with Workload Identity-enabled service accounts.

### Breaking Changes

Expand Down
68 changes: 59 additions & 9 deletions sdk/keyvault/azure-security-keyvault-jca/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,51 @@ The JCA library supports SSL/TLS and mTLS (Mutual TLS) to enhance security in se
The JCA library provides support for Java Archive (JAR) signing, ensuring the integrity and authenticity of JAR files using certificates stored in Azure Key Vault.

## Examples

### Authentication Methods
The JCA provider supports three authentication methods, which are automatically selected based on the configuration:

#### 1. Service Principal (Client Credentials)
Use this method when you have explicit credentials (tenant ID, client ID, client secret):
```java
System.setProperty("azure.keyvault.uri", "<your-azure-keyvault-uri>");
System.setProperty("azure.keyvault.tenant-id", "<your-tenant-id>");
System.setProperty("azure.keyvault.client-id", "<your-client-id>");
System.setProperty("azure.keyvault.client-secret", "<your-client-secret>");
```

#### 2. Managed Identity
Use this method when running on Azure services (VMs, App Service, Container Apps) with Managed Identity enabled. **Only set the Key Vault URI**:
```java
// System-assigned managed identity
System.setProperty("azure.keyvault.uri", "<your-azure-keyvault-uri>");

// User-assigned managed identity (specify the object ID)
System.setProperty("azure.keyvault.uri", "<your-azure-keyvault-uri>");
System.setProperty("azure.keyvault.managed-identity", "<managed-identity-object-id>");
```

#### 3. Workload Identity (AKS)
Use this method when running in Azure Kubernetes Service with Workload Identity enabled. **Only set the Key Vault URI** - the federated credentials are automatically detected from environment variables (`AZURE_FEDERATED_TOKEN_FILE`, `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`):
```java
System.setProperty("azure.keyvault.uri", "<your-azure-keyvault-uri>");
```

**Authentication Selection Logic:**
- If `tenant-id`, `client-id`, and `client-secret` are set → Service Principal authentication
- If only `azure.keyvault.uri` is set → Automatic detection:
- **Workload Identity** (if `AZURE_FEDERATED_TOKEN_FILE` environment variable exists)
- **Managed Identity** on App Service (if `MSI_ENDPOINT` environment variable exists)
- **Managed Identity** on Container Apps/AKS (if `IDENTITY_ENDPOINT` environment variable exists)
- **Managed Identity** on VM (via IMDS endpoint at 169.254.169.254)

### Exposed Options
The JCA library supports configuring the following options:
* `azure.keyvault.uri`: The Azure Key Vault endpoint to retrieve certificates.
* `azure.keyvault.tenant-id`: The Microsoft Entra ID tenant ID required for authentication.
* `azure.keyvault.client-id`: The client/application ID used for authentication.
* `azure.keyvault.client-secret`: The client secret for authentication when using client credentials.
* `azure.keyvault.managed-identity`: Indicates whether Managed Identity authentication is enabled.
* `azure.keyvault.uri`: **(Required)** The Azure Key Vault endpoint to retrieve certificates.
* `azure.keyvault.tenant-id`: The Microsoft Entra ID tenant ID (required for Service Principal authentication).
* `azure.keyvault.client-id`: The client/application ID (required for Service Principal authentication).
* `azure.keyvault.client-secret`: The client secret (required for Service Principal authentication).
* `azure.keyvault.managed-identity`: The user-assigned managed identity object ID (optional, for user-assigned managed identity).
* `azure.cert-path.well-known`: The path where the well-known certificate is stored.
* `azure.cert-path.custom`: The path where the custom certificate is stored.
* `azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate`: Indicates whether to refresh certificates when have untrusted certificate.
Expand Down Expand Up @@ -139,7 +177,10 @@ while (true) {
}
```

Note if you want to use Azure Managed Identity, you should set the value of `azure.keyvault.uri`, and the rest of the parameters would be `null`.
**Note:**
- **For Service Principal authentication**: Set all four properties (`uri`, `tenant-id`, `client-id`, `client-secret`)
- **For Managed Identity authentication**: Only set `azure.keyvault.uri` (and optionally `managed-identity` for user-assigned identity)
- **For Workload Identity authentication (AKS)**: Only set `azure.keyvault.uri` - the provider automatically detects Workload Identity environment variables

#### Client side SSL
If you are looking to integrate the JCA provider for client side socket connections, see the Apache HTTP client example below.
Expand Down Expand Up @@ -187,7 +228,10 @@ try (CloseableHttpClient client = HttpClients.custom().setConnectionManager(mana
System.out.println(result);
```

Note if you want to use Azure managed identity, you should set the value of `azure.keyvault.uri`, and the rest of the parameters would be `null`.
**Note:**
- **For Service Principal authentication**: Set all four properties (`uri`, `tenant-id`, `client-id`, `client-secret`)
- **For Managed Identity authentication**: Only set `azure.keyvault.uri` (and optionally `managed-identity` for user-assigned identity)
- **For Workload Identity authentication (AKS)**: Only set `azure.keyvault.uri` - the provider automatically detects Workload Identity environment variables

### mTLS
#### Server side mTLS
Expand Down Expand Up @@ -237,7 +281,10 @@ while (true) {
}
```

Note if you want to use Azure Managed Identity, you should set the value of `azure.keyvault.uri`, and the rest of the parameters would be `null`.
**Note:**
- **For Service Principal authentication**: Set all four properties (`uri`, `tenant-id`, `client-id`, `client-secret`)
- **For Managed Identity authentication**: Only set `azure.keyvault.uri` (and optionally `managed-identity` for user-assigned identity)
- **For Workload Identity authentication (AKS)**: Only set `azure.keyvault.uri` - the provider automatically detects Workload Identity environment variables

#### Client side mTLS
If you are looking to integrate the JCA provider for client side socket connections, see the Apache HTTP client example below.
Expand Down Expand Up @@ -291,7 +338,10 @@ try (CloseableHttpClient client = HttpClients.custom().setConnectionManager(mana
System.out.println(result);
```

Note if you want to use Azure managed identity, you should set the value of `azure.keyvault.uri`, and the rest of the parameters would be `null`.
**Note:**
- **For Service Principal authentication**: Set all four properties (`uri`, `tenant-id`, `client-id`, `client-secret`)
- **For Managed Identity authentication**: Only set `azure.keyvault.uri` (and optionally `managed-identity` for user-assigned identity)
- **For Workload Identity authentication (AKS)**: Only set `azure.keyvault.uri` - the provider automatically detects Workload Identity environment variables

### Jarsigner
You can use the JCA provider to sign JAR files using certificates stored in Azure Key Vault by the following commands:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public final class AccessTokenUtil {

private static final String PROPERTY_IDENTITY_ENDPOINT = "IDENTITY_ENDPOINT";
private static final String PROPERTY_IDENTITY_HEADER = "IDENTITY_HEADER";
private static final String PROPERTY_AZURE_FEDERATED_TOKEN_FILE = "AZURE_FEDERATED_TOKEN_FILE";
private static final String PROPERTY_AZURE_CLIENT_ID = "AZURE_CLIENT_ID";
private static final String PROPERTY_AZURE_TENANT_ID = "AZURE_TENANT_ID";
private static final String PROPERTY_AZURE_AUTHORITY_HOST = "AZURE_AUTHORITY_HOST";
private static final String DEFAULT_AUTHORITY_HOST = "https://login.microsoftonline.com/";

/**
* Get an access token for a managed identity.
Expand All @@ -90,11 +95,14 @@ public static AccessToken getAccessToken(String resource, String identity) {
AccessToken result;

/*
* Azure Workload Identity (AKS): AZURE_FEDERATED_TOKEN_FILE, AZURE_CLIENT_ID, AZURE_TENANT_ID
* App Service 2017-09-01: MSI_ENDPOINT, MSI_SECRET
* Azure Container App 2019-08-01: IDENTITY_ENDPOINT, IDENTITY_HEADER, see more from https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=cli%2Chttp#rest-endpoint-reference
* Azure Virtual Machine 2018-02-01, see more from https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
*/
if (System.getenv("WEBSITE_SITE_NAME") != null && !System.getenv("WEBSITE_SITE_NAME").isEmpty()) {
if (isWorkloadIdentityAvailable()) {
result = getAccessTokenWithWorkloadIdentity(resource);
} else if (System.getenv("WEBSITE_SITE_NAME") != null && !System.getenv("WEBSITE_SITE_NAME").isEmpty()) {
result = getAccessTokenOnAppService(resource, identity);
} else if (System.getenv(PROPERTY_IDENTITY_ENDPOINT) != null
&& !System.getenv(PROPERTY_IDENTITY_ENDPOINT).isEmpty()) {
Expand Down Expand Up @@ -168,6 +176,108 @@ public static AccessToken getAccessToken(String resource, String aadAuthenticati
return result;
}

/**
* Check if Azure Workload Identity environment is available.
*
* @return true if Workload Identity environment variables are present, false otherwise.
*/
private static boolean isWorkloadIdentityAvailable() {
return System.getenv(PROPERTY_AZURE_FEDERATED_TOKEN_FILE) != null
&& !System.getenv(PROPERTY_AZURE_FEDERATED_TOKEN_FILE).isEmpty()
&& System.getenv(PROPERTY_AZURE_CLIENT_ID) != null
&& !System.getenv(PROPERTY_AZURE_CLIENT_ID).isEmpty()
&& System.getenv(PROPERTY_AZURE_TENANT_ID) != null
&& !System.getenv(PROPERTY_AZURE_TENANT_ID).isEmpty();
}

/**
* Get the access token using Azure Workload Identity (AKS federated token).
*
* @param resource The resource.
* @return The authorization token.
*/
private static AccessToken getAccessTokenWithWorkloadIdentity(String resource) {
LOGGER.entering("AccessTokenUtil", "getAccessTokenWithWorkloadIdentity", resource);
LOGGER.info("Getting access token using Azure Workload Identity (federated token)");

AccessToken result = null;

try {
String tokenFilePath = System.getenv(PROPERTY_AZURE_FEDERATED_TOKEN_FILE);
String clientId = System.getenv(PROPERTY_AZURE_CLIENT_ID);
String tenantId = System.getenv(PROPERTY_AZURE_TENANT_ID);
String authorityHost = System.getenv(PROPERTY_AZURE_AUTHORITY_HOST);

if (authorityHost == null || authorityHost.isEmpty()) {
authorityHost = DEFAULT_AUTHORITY_HOST;
}

LOGGER.log(INFO, "Using Workload Identity with client ID: {0}", clientId);
LOGGER.log(INFO, "Using federated token file: {0}", tokenFilePath);

// Read the federated token from the file
String federatedToken = readTokenFromFile(tokenFilePath);

if (federatedToken == null || federatedToken.isEmpty()) {
LOGGER.log(WARNING, "Failed to read federated token from file: {0}", tokenFilePath);
return null;
}

// Build the OAuth2 token endpoint URL
StringBuilder oauth2Url = new StringBuilder();
oauth2Url.append(addTrailingSlashIfRequired(authorityHost))
.append(tenantId)
.append("/oauth2/v2.0/token");

// Build the request body for client assertion flow
StringBuilder requestBody = new StringBuilder();
requestBody.append("grant_type=client_credentials")
.append("&client_id=").append(clientId)
.append("&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
.append("&client_assertion=").append(federatedToken)
.append("&scope=").append(resource);

// If resource doesn't end with /.default, append it
if (!resource.endsWith("/.default")) {
requestBody.append("/.default");
}

String body = HttpUtil.post(oauth2Url.toString(), requestBody.toString(), "application/x-www-form-urlencoded");

if (body != null) {
try {
result = JsonConverterUtil.fromJson(AccessToken::fromJson, body);
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to parse access token response.", e);
}
}
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to read federated token file for Workload Identity.", e);
}

LOGGER.exiting("AccessTokenUtil", "getAccessTokenWithWorkloadIdentity", result);

return result;
}

/**
* Read the federated token from the specified file.
*
* @param tokenFilePath The path to the token file.
* @return The token content.
* @throws IOException If the file cannot be read.
*/
private static String readTokenFromFile(String tokenFilePath) throws IOException {
LOGGER.entering("AccessTokenUtil", "readTokenFromFile", tokenFilePath);

java.nio.file.Path path = java.nio.file.Paths.get(tokenFilePath);
String token = new String(java.nio.file.Files.readAllBytes(path), java.nio.charset.StandardCharsets.UTF_8).trim();

LOGGER.exiting("AccessTokenUtil", "readTokenFromFile", "[token read successfully]");

return token;
}

/**
* Get the access token on Azure App Service.
*
Expand Down
Loading