google-site-verification: google61fe8ba583a51912.html
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