Learn how to securely manage identities and access Azure resources from Java applications running in Kubernetes using Azure AD Pod Identity.
Table of Contents
- Overview of Azure AD Pod Identity
- Architecture & Components
- Setup & Configuration
- Java Implementation
- Azure SDK Integration
- Security Best Practices
- 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.