Azure AD Pod Identity in Java: Complete Implementation Guide

Learn how to securely manage identities and access Azure resources from Java applications running in Kubernetes using Azure AD Pod Identity.

Table of Contents

  1. Overview of Azure AD Pod Identity
  2. Architecture & Components
  3. Setup & Configuration
  4. Java Implementation
  5. Azure SDK Integration
  6. Security Best Practices
  7. Troubleshooting

Overview

What is Azure AD Pod Identity?

Azure AD Pod Identity allows Kubernetes pods to access Azure resources securely using Azure Active Directory identities without storing credentials in code or environment variables.

Key Benefits

  • Eliminates secret management - No need for connection strings or keys
  • Role-based access control - Leverage Azure RBAC
  • Automatic token rotation - Managed by Azure
  • Fine-grained permissions - Specific resource access

Architecture & Components

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Java Pod      │───▶│  MIC (Managed    │───▶│  Azure Resource │
│                 │    │   Identity       │    │                 │
│ - App           │    │   Controller)    │    │ - Key Vault     │
│ - NMI (Node     │    │                  │    │ - Storage       │
│    Managed      │    │                  │    │ - SQL DB        │
│    Identity)    │    │                  │    │ - etc.          │
└─────────────────┘    └──────────────────┘    └─────────────────┘
│                       │                       │
└───────────────────────┼───────────────────────┘
│
┌───────────┴──────────┐
│   Azure AD           │
│   (AAD)              │
└──────────────────────┘

Components:

  • Managed Identity Controller (MIC): Watches for pod changes and manages Azure Identity binding
  • Node Managed Identity (NMI): DaemonSet that handles token requests from pods
  • AzureIdentity: Custom resource defining the Azure AD identity
  • AzureIdentityBinding: Custom resource linking pods to identities

Setup & Configuration

1. Prerequisites

# prerequisites-check.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: prerequisites-check
data:
# Verify these before proceeding
requirements: |
- Azure Kubernetes Service (AKS) cluster
- Azure CLI installed and configured
- kubectl configured for AKS cluster
- Helm for installation (optional)
- Azure AD permissions for identity management

2. Install Azure AD Pod Identity

Using Helm (Recommended):

helm repo add aad-pod-identity https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts
helm install aad-pod-identity aad-pod-identity/aad-pod-identity

Using kubectl:

# aad-pod-identity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mic
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: mic
template:
metadata:
labels:
app: mic
spec:
containers:
- name: mic
image: "mcr.microsoft.com/oss/azure/aad-pod-identity/mic:v1.8.13"
args:
- "--cloudconfig=/etc/kubernetes/azure.json"
- "--cloud=AzurePublicCloud"
env:
- name: MIC_VERSION
value: "v1.8.13"
volumeMounts:
- name: kubernetes-config
mountPath: /etc/kubernetes/
readOnly: true
volumes:
- name: kubernetes-config
hostPath:
path: /etc/kubernetes/
type: Directory
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nmi
namespace: default
spec:
selector:
matchLabels:
app: nmi
template:
metadata:
labels:
app: nmi
spec:
hostNetwork: true
containers:
- name: nmi
image: "mcr.microsoft.com/oss/azure/aad-pod-identity/nmi:v1.8.13"
args:
- "--host-ip=$(HOST_IP)"
- "--node=$(NODE_NAME)"
env:
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
ports:
- containerPort: 2579
name: http

3. Create Azure Identity Resources

# azure-identity.yaml
apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentity
metadata:
name: java-app-identity
namespace: default
spec:
type: 0  # 0: User-Assigned MSI, 1: Service Principal
resourceID: /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name}
clientID: {client-id}  # The client ID of the Azure AD Identity
---
apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentityBinding
metadata:
name: java-app-identity-binding
namespace: default
spec:
azureIdentity: java-app-identity
selector: java-app  # This matches the pod's aadpodidbinding label

Java Implementation

1. Core Dependencies

<!-- pom.xml -->
<properties>
<azure-identity.version>1.10.0</azure-identity.version>
<azure-security-keyvault-secrets.version>4.6.0</azure-security-keyvault-secrets.version>
<azure-storage-blob.version>12.22.0</azure-storage-blob.version>
<azure-data-appconfiguration.version>1.4.0</azure-data-appconfiguration.version>
</properties>
<dependencies>
<!-- Azure Identity -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>${azure-identity.version}</version>
</dependency>
<!-- Azure Key Vault -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-secrets</artifactId>
<version>${azure-security-keyvault-secrets.version}</version>
</dependency>
<!-- Azure Storage -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-storage-blob</artifactId>
<version>${azure-storage-blob.version}</version>
</dependency>
<!-- App Configuration -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-data-appconfiguration</artifactId>
<version>${azure-data-appconfiguration.version}</version>
</dependency>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Health checks -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

2. Azure Identity Service

@Service
public class AzureIdentityService {
private static final Logger logger = LoggerFactory.getLogger(AzureIdentityService.class);
private final TokenCredential tokenCredential;
public AzureIdentityService() {
this.tokenCredential = new DefaultAzureCredentialBuilder().build();
}
/**
* Get access token for specific resource
*/
public String getAccessToken(String resource) {
try {
AccessToken token = tokenCredential
.getToken(new TokenRequestContext().addScopes(resource + "/.default"))
.block();
logger.info("Successfully acquired token for resource: {}", resource);
return token.getToken();
} catch (Exception e) {
logger.error("Failed to acquire access token for resource: {}", resource, e);
throw new AzureIdentityException("Token acquisition failed", e);
}
}
/**
* Get token for multiple scopes
*/
public AccessToken getToken(TokenRequestContext requestContext) {
try {
return tokenCredential.getToken(requestContext).block();
} catch (Exception e) {
logger.error("Failed to acquire token for scopes: {}", requestContext.getScopes(), e);
throw new AzureIdentityException("Token acquisition failed", e);
}
}
/**
* Validate identity is working
*/
public boolean validateIdentity() {
try {
// Try to get token for Azure Resource Manager
getAccessToken("https://management.azure.com");
return true;
} catch (Exception e) {
logger.warn("Identity validation failed", e);
return false;
}
}
}

3. Azure Service Clients Factory

@Component
public class AzureServiceClientFactory {
private final TokenCredential tokenCredential;
public AzureServiceClientFactory() {
this.tokenCredential = new DefaultAzureCredentialBuilder().build();
}
/**
* Create Key Vault Secret Client
*/
public SecretClient createSecretClient(String keyVaultUrl) {
return new SecretClientBuilder()
.vaultUrl(keyVaultUrl)
.credential(tokenCredential)
.buildClient();
}
/**
* Create Blob Service Client
*/
public BlobServiceClient createBlobServiceClient(String storageAccountUrl) {
return new BlobServiceClientBuilder()
.endpoint(storageAccountUrl)
.credential(tokenCredential)
.buildClient();
}
/**
* Create App Configuration Client
*/
public ConfigurationClient createConfigurationClient(String configurationUrl) {
return new ConfigurationClientBuilder()
.endpoint(configurationUrl)
.credential(tokenCredential)
.buildClient();
}
/**
* Create Event Hubs Client
*/
public EventHubProducerClient createEventHubProducerClient(
String namespace, String eventHubName) {
return new EventHubClientBuilder()
.credential(namespace, eventHubName, tokenCredential)
.buildProducerClient();
}
}

4. Key Vault Integration Service

@Service
public class KeyVaultService {
private static final Logger logger = LoggerFactory.getLogger(KeyVaultService.class);
private final SecretClient secretClient;
private final AzureServiceClientFactory clientFactory;
public KeyVaultService(AzureServiceClientFactory clientFactory,
@Value("${azure.key-vault.url}") String keyVaultUrl) {
this.clientFactory = clientFactory;
this.secretClient = clientFactory.createSecretClient(keyVaultUrl);
}
/**
* Get secret from Key Vault
*/
public String getSecret(String secretName) {
try {
KeyVaultSecret secret = secretClient.getSecret(secretName);
logger.debug("Retrieved secret: {}", secretName);
return secret.getValue();
} catch (ResourceNotFoundException e) {
logger.warn("Secret not found: {}", secretName);
throw new SecretNotFoundException("Secret not found: " + secretName, e);
} catch (Exception e) {
logger.error("Failed to retrieve secret: {}", secretName, e);
throw new SecretAccessException("Failed to access secret: " + secretName, e);
}
}
/**
* Set secret in Key Vault
*/
public void setSecret(String secretName, String secretValue) {
try {
secretClient.setSecret(secretName, secretValue);
logger.info("Successfully set secret: {}", secretName);
} catch (Exception e) {
logger.error("Failed to set secret: {}", secretName, e);
throw new SecretUpdateException("Failed to set secret: " + secretName, e);
}
}
/**
* List all secrets (names only)
*/
public List<String> listSecrets() {
List<String> secretNames = new ArrayList<>();
try {
secretClient.listPropertiesOfSecrets().forEach(secretProperties -> {
secretNames.add(secretProperties.getName());
});
return secretNames;
} catch (Exception e) {
logger.error("Failed to list secrets", e);
throw new SecretAccessException("Failed to list secrets", e);
}
}
/**
* Health check for Key Vault connectivity
*/
public boolean isHealthy() {
try {
// Try to list secrets (limited to 1 for performance)
secretClient.listPropertiesOfSecrets().stream().limit(1).count();
return true;
} catch (Exception e) {
logger.warn("Key Vault health check failed", e);
return false;
}
}
}

5. Storage Service with Managed Identity

@Service
public class AzureStorageService {
private static final Logger logger = LoggerFactory.getLogger(AzureStorageService.class);
private final BlobServiceClient blobServiceClient;
private final AzureServiceClientFactory clientFactory;
public AzureStorageService(AzureServiceClientFactory clientFactory,
@Value("${azure.storage.account-url}") String storageAccountUrl) {
this.clientFactory = clientFactory;
this.blobServiceClient = clientFactory.createBlobServiceClient(storageAccountUrl);
}
/**
* Upload file to blob storage
*/
public void uploadFile(String containerName, String blobName, InputStream data, long length) {
try {
BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
if (!containerClient.exists()) {
containerClient.create();
logger.info("Created container: {}", containerName);
}
BlobClient blobClient = containerClient.getBlobClient(blobName);
blobClient.upload(data, length, true);
logger.info("Successfully uploaded blob: {}/{}", containerName, blobName);
} catch (Exception e) {
logger.error("Failed to upload blob: {}/{}", containerName, blobName, e);
throw new StorageException("Upload failed for blob: " + blobName, e);
}
}
/**
* Download file from blob storage
*/
public byte[] downloadFile(String containerName, String blobName) {
try {
BlobClient blobClient = blobServiceClient
.getBlobContainerClient(containerName)
.getBlobClient(blobName);
if (!blobClient.exists()) {
throw new BlobNotFoundException("Blob not found: " + blobName);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
blobClient.downloadStream(outputStream);
logger.debug("Successfully downloaded blob: {}/{}", containerName, blobName);
return outputStream.toByteArray();
} catch (BlobNotFoundException e) {
throw e;
} catch (Exception e) {
logger.error("Failed to download blob: {}/{}", containerName, blobName, e);
throw new StorageException("Download failed for blob: " + blobName, e);
}
}
/**
* List blobs in container
*/
public List<String> listBlobs(String containerName) {
List<String> blobNames = new ArrayList<>();
try {
BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
containerClient.listBlobs().forEach(blobItem -> {
blobNames.add(blobItem.getName());
});
return blobNames;
} catch (Exception e) {
logger.error("Failed to list blobs in container: {}", containerName, e);
throw new StorageException("Failed to list blobs", e);
}
}
}

Spring Boot Configuration

1. Application Properties

# application.yml
spring:
application:
name: java-pod-identity-app
azure:
key-vault:
url: https://${AZURE_KEY_VAULT_NAME}.vault.azure.net/
storage:
account-url: https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net
app-config:
url: https://${AZURE_APP_CONFIG_NAME}.azconfig.io
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
logging:
level:
com.azure: DEBUG
com.yourcompany.azure: DEBUG

2. Configuration Class

@Configuration
@EnableConfigurationProperties(AzureProperties.class)
public class AzureConfig {
@Bean
@ConditionalOnMissingBean
public AzureServiceClientFactory azureServiceClientFactory() {
return new AzureServiceClientFactory();
}
@Bean
public KeyVaultService keyVaultService(AzureServiceClientFactory clientFactory,
AzureProperties azureProperties) {
return new KeyVaultService(clientFactory, azureProperties.getKeyVault().getUrl());
}
@Bean
public AzureStorageService azureStorageService(AzureServiceClientFactory clientFactory,
AzureProperties azureProperties) {
return new AzureStorageService(clientFactory, azureProperties.getStorage().getAccountUrl());
}
@Bean
public AzureIdentityService azureIdentityService() {
return new AzureIdentityService();
}
}
@ConfigurationProperties(prefix = "azure")
@Data
public class AzureProperties {
private KeyVault keyVault = new KeyVault();
private Storage storage = new Storage();
private AppConfig appConfig = new AppConfig();
@Data
public static class KeyVault {
private String url;
}
@Data
public static class Storage {
private String accountUrl;
}
@Data
public static class AppConfig {
private String url;
}
}

3. Health Indicators

@Component
public class AzureServicesHealthIndicator implements HealthIndicator {
private final KeyVaultService keyVaultService;
private final AzureIdentityService identityService;
public AzureServicesHealthIndicator(KeyVaultService keyVaultService,
AzureIdentityService identityService) {
this.keyVaultService = keyVaultService;
this.identityService = identityService;
}
@Override
public Health health() {
Map<String, Object> details = new HashMap<>();
// Check Key Vault connectivity
boolean keyVaultHealthy = keyVaultService.isHealthy();
details.put("keyVault", keyVaultHealthy ? "UP" : "DOWN");
// Check identity
boolean identityHealthy = identityService.validateIdentity();
details.put("azureIdentity", identityHealthy ? "UP" : "DOWN");
Health.Builder status = keyVaultHealthy && identityHealthy ? 
Health.up() : Health.down();
return status.withDetails(details).build();
}
}

Kubernetes Deployment

1. Deployment Manifest

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-pod-identity-app
namespace: default
labels:
app: java-pod-identity-app
spec:
replicas: 3
selector:
matchLabels:
app: java-pod-identity-app
template:
metadata:
labels:
app: java-pod-identity-app
aadpodidbinding: java-app  # Matches AzureIdentityBinding selector
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
containers:
- name: java-app
image: your-registry/java-pod-identity-app:latest
ports:
- containerPort: 8080
env:
- name: AZURE_KEY_VAULT_NAME
value: "your-keyvault-name"
- name: AZURE_STORAGE_ACCOUNT
value: "your-storage-account"
- name: AZURE_APP_CONFIG_NAME
value: "your-app-config"
- name: JAVA_OPTS
value: "-Xmx512m -Djava.security.egd=file:/dev/./urandom"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 20
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
securityContext:
runAsNonRoot: true
runAsUser: 1000
---
apiVersion: v1
kind: Service
metadata:
name: java-pod-identity-service
namespace: default
spec:
selector:
app: java-pod-identity-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP

2. Service Account (Optional)

# service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: java-app-service-account
namespace: default
labels:
app: java-pod-identity-app

Security Best Practices

1. Network Policies

# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: java-app-network-policy
namespace: default
spec:
podSelector:
matchLabels:
app: java-pod-identity-app
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: default
ports:
- protocol: TCP
port: 8080
egress:
- to:
- ipBlock:
cidr: 169.254.169.254/32 # Azure Instance Metadata Service
ports:
- protocol: TCP
port: 80
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443

2. Pod Security Context

# pod-security.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: java-app-psp
spec:
privileged: false
allowPrivilegeEscalation: false
requiredDropCapabilities:
- ALL
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'MustRunAsNonRoot'
seLinux:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'

Troubleshooting & Monitoring

1. Diagnostic Service

@Service
public class AzureIdentityDiagnosticService {
private static final Logger logger = LoggerFactory.getLogger(AzureIdentityDiagnosticService.class);
private final AzureIdentityService identityService;
public AzureIdentityDiagnosticService(AzureIdentityService identityService) {
this.identityService = identityService;
}
/**
* Comprehensive diagnostic check
*/
public Map<String, Object> runDiagnostics() {
Map<String, Object> diagnostics = new HashMap<>();
try {
// Test token acquisition for different resources
diagnostics.put("managementToken", testTokenAcquisition("https://management.azure.com"));
diagnostics.put("keyVaultToken", testTokenAcquisition("https://vault.azure.net"));
diagnostics.put("storageToken", testTokenAcquisition("https://storage.azure.com"));
diagnostics.put("overallStatus", "HEALTHY");
} catch (Exception e) {
diagnostics.put("overallStatus", "UNHEALTHY");
diagnostics.put("error", e.getMessage());
}
return diagnostics;
}
private Map<String, Object> testTokenAcquisition(String resource) {
Map<String, Object> result = new HashMap<>();
try {
String token = identityService.getAccessToken(resource);
result.put("status", "SUCCESS");
result.put("tokenLength", token.length());
result.put("resource", resource);
} catch (Exception e) {
result.put("status", "FAILED");
result.put("error", e.getMessage());
result.put("resource", resource);
}
return result;
}
}

2. REST Controller for Management

@RestController
@RequestMapping("/api/azure")
public class AzureIdentityController {
private final AzureIdentityDiagnosticService diagnosticService;
private final KeyVaultService keyVaultService;
private final AzureStorageService storageService;
public AzureIdentityController(AzureIdentityDiagnosticService diagnosticService,
KeyVaultService keyVaultService,
AzureStorageService storageService) {
this.diagnosticService = diagnosticService;
this.keyVaultService = keyVaultService;
this.storageService = storageService;
}
@GetMapping("/diagnostics")
public Map<String, Object> getDiagnostics() {
return diagnosticService.runDiagnostics();
}
@GetMapping("/secrets")
public List<String> listSecrets() {
return keyVaultService.listSecrets();
}
@GetMapping("/secrets/{secretName}")
public String getSecret(@PathVariable String secretName) {
return keyVaultService.getSecret(secretName);
}
@PostMapping("/secrets/{secretName}")
public ResponseEntity<Void> setSecret(@PathVariable String secretName,
@RequestBody String secretValue) {
keyVaultService.setSecret(secretName, secretValue);
return ResponseEntity.ok().build();
}
@GetMapping("/storage/{container}/blobs")
public List<String> listBlobs(@PathVariable String container) {
return storageService.listBlobs(container);
}
}

3. Common Issues and Solutions

@Component
public class TroubleshootingGuide {
/**
* Common issues and their solutions
*/
public Map<String, String> getCommonIssues() {
return Map.of(
"401 Unauthorized", 
"Check AzureIdentityBinding selector matches pod label",
"403 Forbidden", 
"Verify Azure Identity has proper RBAC assignments",
"Token acquisition failed", 
"Check NMI pod logs and Azure Identity resource configuration",
"Network connectivity issues",
"Verify network policies allow access to 169.254.169.254:80"
);
}
/**
* Validate pod identity configuration
*/
public boolean validatePodConfiguration() {
// Check environment variables
String hostIp = System.getenv("HOST_IP");
String nodeName = System.getenv("NODE_NAME");
if (hostIp == null || nodeName == null) {
logger.warn("Pod identity environment variables not set");
return false;
}
// Check IMDS endpoint accessibility
return canReachImds();
}
private boolean canReachImds() {
try {
URL url = new URL("http://169.254.169.254/metadata/instance?api-version=2021-02-01");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Metadata", "true");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
int responseCode = connection.getResponseCode();
return responseCode == 200;
} catch (Exception e) {
logger.warn("Cannot reach IMDS endpoint", e);
return false;
}
}
}

Migration from Connection Strings

1. Legacy Configuration Support

@Configuration
public class LegacyMigrationConfig {
@Bean
@ConditionalOnProperty(name = "azure.use-managed-identity", havingValue = "false")
public TokenCredential connectionStringCredential(
@Value("${azure.connection-string:}") String connectionString) {
// Fallback to connection string for local development
return new ConnectionStringCredential(connectionString);
}
@Bean
@Primary
@ConditionalOnProperty(name = "azure.use-managed-identity", havingValue = "true", matchIfMissing = true)
public TokenCredential managedIdentityCredential() {
return new DefaultAzureCredentialBuilder().build();
}
}

This comprehensive implementation provides a secure, production-ready approach to using Azure AD Pod Identity with Java applications, eliminating the need for managing secrets while maintaining robust security and operational excellence.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper