Workload Identity is the modern paradigm for securely granting Kubernetes applications access to cloud services without storing static credentials. By leveraging native Kubernetes and cloud provider identity systems, it eliminates the security risks of hardcoded secrets while providing fine-grained, auditable access control.
Understanding Workload Identity Concepts
Traditional vs. Workload Identity Approach:
| Aspect | Traditional (Secrets) | Workload Identity |
|---|---|---|
| Credentials | Static API keys/credentials | Dynamic, short-lived tokens |
| Security | Risk of exposure, hard to rotate | Automatic rotation, minimal exposure |
| Management | Manual secret rotation | Automated lifecycle management |
| Auditing | Difficult to trace | Clear identity mapping |
| Granularity | Often over-privileged | Fine-grained IAM policies |
Key Technologies:
- AWS: IAM Roles for Service Accounts (IRSA)
- GCP: Workload Identity Federation
- Azure: Azure AD Pod Identity / Workload Identity
- Kubernetes: Service Accounts, TokenRequest API
AWS: IAM Roles for Service Accounts (IRSA)
1. Cluster Configuration for IRSA
// IRSA-enabled Kubernetes service account manifest
public class IRSAConfiguration {
public static String createIRSAServiceAccount(String namespace, String serviceAccountName,
String roleArn) {
return String.format("""
apiVersion: v1
kind: ServiceAccount
metadata:
name: %s
namespace: %s
annotations:
eks.amazonaws.com/role-arn: %s
""", serviceAccountName, namespace, roleArn);
}
}
2. Java Application with AWS IRSA
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest;
public class AWSWorkloadIdentity {
private final AwsCredentialsProvider credentialsProvider;
private final String region;
public AWSWorkloadIdentity(String region) {
this.region = region;
this.credentialsProvider = createIRSAProvider();
}
private AwsCredentialsProvider createIRSAProvider() {
// WebIdentityTokenFileCredentialsProvider automatically handles:
// - Token file location (/var/run/secrets/eks.amazonaws.com/serviceaccount/token)
// - Token refresh
// - STS AssumeRoleWithWebIdentity calls
return WebIdentityTokenFileCredentialsProvider.builder()
.roleSessionName("my-java-app")
.build();
}
public S3Client createS3Client() {
return S3Client.builder()
.credentialsProvider(credentialsProvider)
.region(Region.of(region))
.build();
}
// Manual token validation and role assumption
public void demonstrateManualWorkflow() {
try {
// Read the service account token
String tokenPath = "/var/run/secrets/eks.amazonaws.com/serviceaccount/token";
String token = new String(Files.readAllBytes(Paths.get(tokenPath)), StandardCharsets.UTF_8);
// Create STS client with initial credentials (if needed)
StsClient stsClient = StsClient.builder()
.region(Region.of(region))
.build();
// Assume role with web identity
AssumeRoleWithWebIdentityRequest request = AssumeRoleWithWebIdentityRequest.builder()
.webIdentityToken(token)
.roleArn("arn:aws:iam::123456789012:role/my-app-role")
.roleSessionName("my-java-app-session")
.durationSeconds(3600)
.build();
var response = stsClient.assumeRoleWithWebIdentity(request);
System.out.println("Assumed role: " + response.assumedRoleUser().arn());
System.out.println("Credentials expire: " + response.credentials().expiration());
} catch (Exception e) {
throw new RuntimeException("Workload identity assumption failed", e);
}
}
// Verify the current identity
public void verifyIdentity() {
try {
StsClient stsClient = StsClient.builder()
.credentialsProvider(credentialsProvider)
.region(Region.of(region))
.build();
var identity = stsClient.getCallerIdentity();
System.out.println("Current identity: " + identity.arn());
System.out.println("User ID: " + identity.userId());
System.out.println("Account: " + identity.account());
} catch (Exception e) {
throw new RuntimeException("Identity verification failed", e);
}
}
}
3. Spring Boot Configuration for AWS IRSA
@Configuration
@EnableConfigurationProperties(AWSProperties.class)
public class AWSIRSAConfiguration {
@Bean
@ConditionalOnProperty(name = "aws.irsa.enabled", havingValue = "true")
public AwsCredentialsProvider irsaCredentialsProvider() {
return WebIdentityTokenFileCredentialsProvider.create();
}
@Bean
public S3Client s3Client(AwsCredentialsProvider credentialsProvider,
AWSProperties properties) {
return S3Client.builder()
.credentialsProvider(credentialsProvider)
.region(Region.of(properties.getRegion()))
.build();
}
@Bean
public SqsClient sqsClient(AwsCredentialsProvider credentialsProvider,
AWSProperties properties) {
return SqsClient.builder()
.credentialsProvider(credentialsProvider)
.region(Region.of(properties.getRegion()))
.build();
}
}
@ConfigurationProperties(prefix = "aws")
@Data
public class AWSProperties {
private String region = "us-east-1";
private boolean irsaEnabled = true;
private String s3Bucket;
private String sqsQueueUrl;
}
Google Cloud: Workload Identity Federation
1. GCP Workload Identity Setup
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ExternalAccountCredentials;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
public class GCPWorkloadIdentity {
private final GoogleCredentials credentials;
public GCPWorkloadIdentity() {
this.credentials = createWorkloadIdentityCredentials();
}
private GoogleCredentials createWorkloadIdentityCredentials() {
try {
// Automatic credential resolution for:
// - GKE Workload Identity
// - Cloud Run
// - Compute Engine
return GoogleCredentials.getApplicationDefault();
} catch (IOException e) {
throw new RuntimeException("Failed to initialize GCP credentials", e);
}
}
public Storage createStorageClient() {
return StorageOptions.newBuilder()
.setCredentials(credentials)
.build()
.getService();
}
// For explicit configuration (e.g., cross-cloud scenarios)
public GoogleCredentials createExternalAccountCredentials() {
try {
String config = """
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"file": "/var/run/secrets/tokens/gcp-ksa"
}
}
""";
return ExternalAccountCredentials.fromJson(new StringReader(config));
} catch (IOException e) {
throw new RuntimeException("Failed to create external account credentials", e);
}
}
public void verifyCredentials() {
try {
credentials.refreshIfExpired();
System.out.println("Credentials are valid");
System.out.println("Project ID: " + ((ExternalAccountCredentials) credentials).getAccount());
} catch (IOException e) {
throw new RuntimeException("Credential verification failed", e);
}
}
}
2. Spring Boot GCP Configuration
@Configuration
@EnableConfigurationProperties(GCPProperties.class)
public class GCPWorkloadIdentityConfig {
@Bean
@ConditionalOnMissingBean
public GoogleCredentials googleCredentials() throws IOException {
return GoogleCredentials.getApplicationDefault();
}
@Bean
public Storage storage(GoogleCredentials credentials, GCPProperties properties) {
return StorageOptions.newBuilder()
.setCredentials(credentials)
.setProjectId(properties.getProjectId())
.build()
.getService();
}
@Bean
public PubSub pubSub(GoogleCredentials credentials, GCPProperties properties) {
return PubSubOptions.newBuilder()
.setCredentials(credentials)
.setProjectId(properties.getProjectId())
.build()
.getService();
}
}
@ConfigurationProperties(prefix = "gcp")
@Data
public class GCPProperties {
private String projectId;
private String location = "us-central1";
}
Azure: Workload Identity
1. Azure Workload Identity Implementation
import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
public class AzureWorkloadIdentity {
private final DefaultAzureCredential credential;
public AzureWorkloadIdentity() {
this.credential = createWorkloadIdentityCredential();
}
private DefaultAzureCredential createWorkloadIdentityCredential() {
// DefaultAzureCredential automatically tries:
// 1. Environment variables
// 2. Managed Identity (App Service, VM, AKS)
// 3. Azure CLI
// 4. Azure PowerShell
return new DefaultAzureCredentialBuilder()
.managedIdentityClientId(System.getenv("AZURE_CLIENT_ID")) // Optional: specific MI
.build();
}
public SecretClient createKeyVaultClient(String keyVaultUrl) {
return new SecretClientBuilder()
.vaultUrl(keyVaultUrl)
.credential(credential)
.buildClient();
}
public BlobServiceClient createBlobServiceClient(String storageAccountUrl) {
return new BlobServiceClientBuilder()
.endpoint(storageAccountUrl)
.credential(credential)
.buildClient();
}
public void verifyToken() {
try {
// The credential will automatically refresh tokens as needed
System.out.println("Azure credential initialized successfully");
} catch (Exception e) {
throw new RuntimeException("Azure credential initialization failed", e);
}
}
}
Cross-Cloud Workload Identity Solution
1. Abstract Workload Identity Provider
public interface WorkloadIdentityProvider {
String getAccessToken() throws WorkloadIdentityException;
String getIdentityType();
boolean isTokenExpired();
void refreshToken() throws WorkloadIdentityException;
}
public enum CloudProvider {
AWS, GCP, AZURE, KUBERNETES
}
public class WorkloadIdentityManager {
private final WorkloadIdentityProvider identityProvider;
private final CloudProvider cloudProvider;
public WorkloadIdentityManager(CloudProvider cloudProvider) {
this.cloudProvider = cloudProvider;
this.identityProvider = createProvider(cloudProvider);
}
private WorkloadIdentityProvider createProvider(CloudProvider provider) {
switch (provider) {
case AWS:
return new AWSWorkloadIdentityProvider();
case GCP:
return new GCPWorkloadIdentityProvider();
case AZURE:
return new AzureWorkloadIdentityProvider();
default:
throw new IllegalArgumentException("Unsupported cloud provider: " + provider);
}
}
public <T> T createCloudClient(Class<T> clientClass, Map<String, String> config) {
// Factory method to create cloud-specific clients
// using workload identity
return CloudClientFactory.createClient(clientClass, identityProvider, config);
}
public void validatePermissions(List<String> requiredPermissions) {
// Validate that the workload has required permissions
identityProvider.validatePermissions(requiredPermissions);
}
}
2. Generic Token-Based Identity Provider
public class KubernetesServiceAccountProvider implements WorkloadIdentityProvider {
private static final String TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
private static final String NAMESPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
private String currentToken;
private Instant tokenExpiry;
private final String audience;
public KubernetesServiceAccountProvider(String audience) {
this.audience = audience;
refreshToken();
}
@Override
public String getAccessToken() throws WorkloadIdentityException {
if (isTokenExpired()) {
refreshToken();
}
return currentToken;
}
@Override
public void refreshToken() throws WorkloadIdentityException {
try {
// Read the service account token
String token = readTokenFile(TOKEN_PATH);
// For projected tokens, we might need to request a new token
if (isTokenProjected()) {
token = requestNewToken();
}
this.currentToken = token;
this.tokenExpiry = extractExpiryFromToken(token);
} catch (IOException e) {
throw new WorkloadIdentityException("Failed to refresh token", e);
}
}
private String requestNewToken() throws IOException {
// Use TokenRequest API to get a new token
String namespace = readTokenFile(NAMESPACE_PATH);
// In production, you'd use the Kubernetes API
// This is a simplified example
return requestTokenFromAPI(namespace);
}
private String readTokenFile(String path) throws IOException {
return Files.readString(Paths.get(path), StandardCharsets.UTF_8).trim();
}
private Instant extractExpiryFromToken(String token) {
try {
String[] parts = token.split("\\.");
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
JsonObject jsonPayload = JsonParser.parseString(payload).getAsJsonObject();
long exp = jsonPayload.get("exp").getAsLong();
return Instant.ofEpochSecond(exp);
} catch (Exception e) {
// If we can't parse, assume short expiry
return Instant.now().plus(Duration.ofMinutes(5));
}
}
@Override
public boolean isTokenExpired() {
return tokenExpiry == null || Instant.now().isAfter(tokenExpiry.minus(Duration.ofMinutes(1)));
}
private boolean isTokenProjected() {
return Files.exists(Paths.get("/var/run/secrets/tokens"));
}
}
Spring Boot Auto-Configuration
1. Workload Identity Auto-Configuration
@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@AutoConfigureAfter(CloudAutoConfiguration.class)
public class WorkloadIdentityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public WorkloadIdentityDetector workloadIdentityDetector() {
return new WorkloadIdentityDetector();
}
@Bean
@ConditionalOnBean(WorkloadIdentityDetector.class)
@ConditionalOnAws
public AwsCredentialsProvider awsCredentialsProvider(WorkloadIdentityDetector detector) {
if (detector.isWorkloadIdentityEnabled()) {
return WebIdentityTokenFileCredentialsProvider.create();
}
return DefaultCredentialsProvider.create();
}
@Bean
@ConditionalOnBean(WorkloadIdentityDetector.class)
@ConditionalOnGcp
public GoogleCredentials googleCredentials(WorkloadIdentityDetector detector) throws IOException {
if (detector.isWorkloadIdentityEnabled()) {
return GoogleCredentials.getApplicationDefault();
}
// Fallback to other credential methods
return GoogleCredentials.fromStream(getCredentialsStream());
}
}
@Component
public class WorkloadIdentityDetector {
public boolean isWorkloadIdentityEnabled() {
// Check for workload identity indicators
return isRunningInKubernetes() &&
(hasServiceAccountToken() || hasAwsIRSAAnnotation() || hasGcpWorkloadIdentity());
}
private boolean isRunningInKubernetes() {
return Files.exists(Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/token"));
}
private boolean hasServiceAccountToken() {
return Files.exists(Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/token"));
}
private boolean hasAwsIRSAAnnotation() {
// Check for IRSA annotation in service account
String namespace = readNamespace();
// In practice, you'd need to query the Kubernetes API
return Files.exists(Paths.get("/var/run/secrets/eks.amazonaws.com/serviceaccount"));
}
private String readNamespace() {
try {
return Files.readString(
Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
).trim();
} catch (IOException e) {
return "default";
}
}
}
2. Application Configuration
# application.yml
workload-identity:
enabled: true
cloud-provider: auto-detect
token-refresh: 300s # 5 minutes
aws:
irsa:
enabled: true
role-arn: ${AWS_ROLE_ARN:}
gcp:
workload-identity:
enabled: true
pool: ${GCP_POOL_ID:}
azure:
workload-identity:
enabled: true
client-id: ${AZURE_CLIENT_ID:}
Security and Best Practices
1. Security Validation
@Component
public class WorkloadIdentitySecurityValidator {
private static final Duration MAX_TOKEN_LIFETIME = Duration.ofHours(1);
public void validateTokenSecurity(String token) {
try {
Jwt jwt = parseJwt(token);
validateExpiry(jwt);
validateAudience(jwt);
validateIssuer(jwt);
validateTokenLifetime(jwt);
} catch (Exception e) {
throw new SecurityException("Token security validation failed", e);
}
}
private Jwt parseJwt(String token) {
// Use a JWT library like jjwt or nimbus-jose-jwt
return Jwts.parserBuilder()
.setSigningKeyResolver(getSigningKeyResolver())
.build()
.parseClaimsJws(token);
}
private void validateExpiry(Jwt jwt) {
if (jwt.getBody().getExpiration().before(new Date())) {
throw new SecurityException("Token has expired");
}
}
private void validateTokenLifetime(Jwt jwt) {
Date issuedAt = jwt.getBody().getIssuedAt();
Date expiration = jwt.getBody().getExpiration();
long lifetime = expiration.getTime() - issuedAt.getTime();
if (lifetime > MAX_TOKEN_LIFETIME.toMillis()) {
throw new SecurityException("Token lifetime exceeds maximum allowed");
}
}
}
2. IAM Policy Validation
@Service
public class IAMPolicyValidator {
public void validateLeastPrivilege(String serviceAccount, List<String> actions, List<String> resources) {
// This would integrate with cloud provider's IAM policy simulator
// For AWS: IAM Policy Simulator API
// For GCP: Policy Troubleshooter API
// For Azure: Azure Policy API
System.out.println("Validating IAM policies for: " + serviceAccount);
System.out.println("Required actions: " + actions);
System.out.println("Required resources: " + resources);
// In production, make API calls to cloud provider's policy validation service
}
public List<String> detectExcessivePermissions(String serviceAccount) {
// Detect and report excessive permissions
List<String> excessivePermissions = new ArrayList<>();
// Implementation would vary by cloud provider
// This is a conceptual example
return excessivePermissions;
}
}
Monitoring and Observability
1. Workload Identity Metrics
@Component
public class WorkloadIdentityMetrics {
private final MeterRegistry meterRegistry;
private final Counter tokenRefreshCounter;
private final Counter tokenValidationCounter;
private final Gauge tokenExpiryGauge;
public WorkloadIdentityMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.tokenRefreshCounter = Counter.builder("workload_identity_token_refresh")
.description("Number of token refresh operations")
.tag("cloud_provider", getCloudProvider())
.register(meterRegistry);
this.tokenValidationCounter = Counter.builder("workload_identity_token_validation")
.description("Number of token validation operations")
.tag("cloud_provider", getCloudProvider())
.register(meterRegistry);
this.tokenExpiryGauge = Gauge.builder("workload_identity_token_expiry_seconds")
.description("Seconds until token expiry")
.tag("cloud_provider", getCloudProvider())
.register(meterRegistry);
}
public void recordTokenRefresh(boolean success) {
tokenRefreshCounter.increment();
meterRegistry.counter("workload_identity_token_refresh_result",
"success", String.valueOf(success))
.increment();
}
public void updateTokenExpiry(Instant expiry) {
long secondsUntilExpiry = Duration.between(Instant.now(), expiry).getSeconds();
tokenExpiryGauge.set(secondsUntilExpiry);
}
private String getCloudProvider() {
// Detect cloud provider from environment
if (System.getenv("AWS_EXECUTION_ENV") != null) return "aws";
if (System.getenv("K_SERVICE") != null) return "gcp";
if (System.getenv("WEBSITE_SITE_NAME") != null) return "azure";
return "unknown";
}
}
Troubleshooting and Debugging
1. Identity Debugging Tool
@Component
public class WorkloadIdentityDebugger {
public void diagnoseIdentityIssues() {
System.out.println("=== Workload Identity Diagnostics ===");
checkKubernetesEnvironment();
checkServiceAccount();
checkCloudCredentials();
checkTokenFiles();
checkNetworkConnectivity();
}
private void checkKubernetesEnvironment() {
System.out.println("\n1. Kubernetes Environment:");
System.out.println(" Running in Kubernetes: " + isRunningInKubernetes());
System.out.println(" Namespace: " + getCurrentNamespace());
System.out.println(" Service Account: " + getServiceAccountName());
}
private void checkServiceAccount() {
System.out.println("\n2. Service Account:");
String tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
boolean tokenExists = Files.exists(Paths.get(tokenPath));
System.out.println(" Token exists: " + tokenExists);
if (tokenExists) {
try {
String token = Files.readString(Paths.get(tokenPath));
System.out.println(" Token length: " + token.length());
System.out.println(" Token preview: " + token.substring(0, Math.min(50, token.length())) + "...");
} catch (IOException e) {
System.out.println(" Error reading token: " + e.getMessage());
}
}
}
private void checkCloudCredentials() {
System.out.println("\n3. Cloud Credentials:");
// Check AWS
if (Files.exists(Paths.get("/var/run/secrets/eks.amazonaws.com/serviceaccount"))) {
System.out.println(" AWS IRSA: Enabled");
}
// Check GCP
try {
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
System.out.println(" GCP Workload Identity: Enabled");
} catch (IOException e) {
System.out.println(" GCP Workload Identity: Not available");
}
// Check Azure
try {
DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();
System.out.println(" Azure Workload Identity: Enabled");
} catch (Exception e) {
System.out.println(" Azure Workload Identity: Not available");
}
}
private boolean isRunningInKubernetes() {
return Files.exists(Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/namespace"));
}
private String getCurrentNamespace() {
try {
return Files.readString(
Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
).trim();
} catch (IOException e) {
return "unknown";
}
}
}
Migration Strategy from Static Credentials
1. Credential Migration Helper
@Service
public class CredentialMigrationService {
@Value("${migration.mode:workload-identity}")
private String migrationMode;
public AwsCredentialsProvider getAwsCredentialsProvider() {
switch (migrationMode) {
case "workload-identity":
return WebIdentityTokenFileCredentialsProvider.create();
case "static":
return StaticCredentialsProvider.create(
AwsBasicCredentials.create(
System.getenv("AWS_ACCESS_KEY_ID"),
System.getenv("AWS_SECRET_ACCESS_KEY")
)
);
case "hybrid":
// Try workload identity first, fall back to static
try {
return WebIdentityTokenFileCredentialsProvider.create();
} catch (Exception e) {
return StaticCredentialsProvider.create(
AwsBasicCredentials.create(
System.getenv("AWS_ACCESS_KEY_ID"),
System.getenv("AWS_SECRET_ACCESS_KEY")
)
);
}
default:
throw new IllegalArgumentException("Unknown migration mode: " + migrationMode);
}
}
}
Conclusion
Workload Identity provides significant advantages for Java applications running in Kubernetes:
Key Benefits:
- Enhanced Security: No long-lived credentials stored in containers
- Automatic Rotation: Tokens are automatically refreshed
- Fine-Grained Access: IAM roles tied to specific service accounts
- Audit Trail: Clear identity mapping for compliance
- Simplified Management: No manual secret rotation
Implementation Checklist:
- [ ] Configure Kubernetes service accounts with cloud provider annotations
- [ ] Set up IAM roles/policies with least privilege
- [ ] Update Java applications to use cloud SDKs with workload identity
- [ ] Implement proper token refresh logic
- [ ] Add monitoring and metrics for identity operations
- [ ] Create automated testing for identity workflows
- [ ] Establish security validation and auditing
By adopting workload identity patterns, Java applications achieve production-grade security while maintaining the operational simplicity of cloud-native deployments.