Eliminating Service Account Keys for Cross-Cloud Workloads
Managing identities across cloud providers is often more challenging than managing infrastructure.
A common integration pattern is allowing workloads running on Amazon Web Services (AWS) to access Google Cloud resources such as Google Cloud Storage (GCS). The traditional approach typically involves generating long-lived Google service account keys, distributing them to workloads, and rotating them periodically.
This creates several security and operational challenges:
Long-lived credentials increase the attack surface.
Secret rotation is often manual and error-prone.
CI/CD pipelines require secure secret management.
Compromised keys can provide broad access for extended periods.
Google Cloud Workload Identity Federation (WIF) eliminates these challenges by enabling AWS workloads to exchange trusted identities for short-lived Google access tokens without storing service account keys.
This project demonstrates how to implement AWS → Google Cloud federation using Terraform.
Supported Authentication Patterns
A single Workload Identity Pool supports two authentication paths.
The EKS cluster's OIDC issuer signs the token, and Google Cloud validates the workload identity through an OIDC provider.
Key characteristics:
Uses Kubernetes ServiceAccount JWTs
Preserves pod-level identity
Supports namespace and ServiceAccount-level authorization
Does not require AWS IAM credentials inside the pod
EC2 / AWS IAM Role → Google Cloud Storage
For workloads running on EC2 or other compute services that authenticate with AWS IAM roles, authentication uses AWS temporary credentials obtained through AWS STS.
Google Cloud validates the signed AWS request through an AWS workload identity provider and exchanges it for a short-lived Google access token.
Key characteristics:
Uses AWS STS temporary credentials
Supports EC2 instances and non-Kubernetes workloads
Uses IAM role-based authorization
Eliminates Google service account keys
Architecture Overview
AWS Workload
│
├── EKS Pod → Kubernetes ServiceAccount JWT
│
└── EC2 Instance → AWS STS Temporary Credentials
│
▼
Google Cloud Workload Identity Federation
│
▼
Google Service Account Impersonation
│
▼
Google Cloud Storage
Important Design Consideration for Amazon EKS
One of the most common mistakes is attempting to use the AWS IAM provider path from inside EKS pods.
This approach relies on the AWS Instance Metadata Service (IMDS), which returns the IAM role attached to the worker node—not the identity of the pod itself.
As a result, authorization becomes tied to node-level permissions rather than workload identity.
For EKS workloads, the recommended approach is Kubernetes OIDC federation using projected ServiceAccount tokens.
In short:
EKS pods → Kubernetes OIDC provider
EC2 instances → AWS IAM provider
Choosing the correct identity source is critical.
What the Repository Includes
Terraform configuration for Workload Identity Pools and providers
Secretless authentication is becoming a foundational requirement for modern cloud platforms.
Workload Identity Federation enables secure, short-lived, and auditable access between cloud providers without the operational burden of managing service account keys.
Import Changes, API Adjustments & Compatibility Results
Migration summary from Spring Boot 3.5 → 4.0, including breaking changes, dependency updates, and fixes across Web, JPA, Jackson 3, GCP, Pub/Sub, Thymeleaf, and Testing.
Alternative (DirectObjectMapper with builder-style API)
When you need a plain ObjectMapper without JsonMapper.builder(), use the mutate-style API:
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.JsonNode;
var mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
📌 In Jackson 3, ObjectMapper is still valid for simple use cases. The disable(...) / enable(...) methods work directly on the instance — no need to use configure(feature, false) as in Jackson 2.
👉Important:Annotations (@JsonProperty, etc.) remain unchanged → ✔️ No changes required in model/POJO classes
new HttpMessageNotReadableException(message, cause);
✅ New
new HttpMessageNotReadableException(message, (HttpInputMessage) null);
📌ReasonSpring Framework 7 removed the Throwable constructor.
✅ ResponseEntity with Status Codes
Spring Boot 4 also introduces cleaner static factory methods for common HTTP status codes. The new ResponseEntity<>(null, HttpStatus.XXX) pattern should be replaced with the dedicated builder methods.
Spring Boot 3
ResponseEntity<List<ApplyBillCreditResponseVO>> responseEntity =
new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
Springdoc (3.0.2) → still usesJackson 2 (com.fasterxml.*) internally
1️⃣4️⃣ JavaTimeModule in Jackson 3
⚠️ Important Clarification
InJackson 3, JavaTimeModule no longer needs to be registered explicitly. The Jackson 3 migration guide states that the former Java 8 modules are now built into jackson-databind, including jackson-datatype-jsr310 for java.time support.
JavaTimeModule isnot requiredanymore in Jackson 3.
java.time support remains available out of the box.
This specifically applies to: LocalDate LocalDateTime Instant other Java 8 date/time types.
💡 Practical Note
If your DTOs already serialize time values as String (for example Instant.now().toString()), then removing new JavaTimeModule() does not change that payload behavior.
📌 Jackson 3 embeds the former Java 8 modules directly into jackson-databind, including JSR-310 support for java.time types, so JavaTimeModule no longer needs to be registered explicitly.
1️⃣5️⃣ HttpHeaders Change in Spring Framework 7
⚠️ Breaking Change
InSpring Framework 7, HttpHeaders no longer implements the MultiValueMap contract. The official Javadoc and Spring Framework 7 release notes both call this out explicitly.
❌ Old style
HashMap<String, List<String>> customHeaders = new HashMap<>(request.getHeaders());
builder.append(" Custom Headers: ").append(customHeaders);
Since Spring Framework 7, HttpHeaders is no longer map-like in the same way as before, and several Map / MultiValueMap style usages are no longer valid or are discouraged. Spring’s Javadoc also notes that asMultiValueMap() is now only for backward compatibility and should generally be avoided.
✅ Other common fixes
Check existence
headers.containsHeader("My-Header")
instead of:
headers.containsKey("My-Header")
Spring Framework 7 introduced header-focused alternatives because HttpHeaders no longer extends MultiValueMap.
📌 Spring Framework 7 changed HttpHeaders so it no longer implements MultiValueMap directly. Map-style usage such as new HashMap<>(request.getHeaders()) may fail and should be replaced with header-focused access patterns like request.getHeaders().forEach(...) or containsHeader(...).
1️⃣6️⃣ Logback — Transitive via spring-boot-starter-webmvc
📌 Declaring these explicitly adds no value and may cause version conflicts if the explicit version differs from the one managed by the Spring Boot BOM. Let Spring Boot manage the Logback version automatically.
1️⃣7️⃣ Spring Retry → Spring Resilience (Spring Framework 7 / Spring Boot 4)
⚠️Breaking Change
✅ Dependencies — Safe to Remove
The following dependencies areno longer needed(Spring Boot 4 includes resilience support built-in):
<!-- ❌ Remove — replaced by Spring Framework 7 built-in resilience -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
✅ Configuration Class Migration
Spring Boot 3 (spring-retry)
import org.springframework.retry.annotation.EnableRetry;
@Configuration
@EnableRetry
public class RetryConfig {
}
Spring Boot 4 (Spring Resilience)
import org.springframework.context.annotation.Configuration;
import org.springframework.resilience.annotation.EnableResilientMethods;
@Configuration
@EnableResilientMethods
public class RetryConfig {
}
Spring removed multiple confusing factory methods (fromHttpUrl, fromHttpRequest, etc.) and unified everything into fromUriString(...), which works for all URI types — HTTP, HTTPS, and generic URIs.
// Additional configuration to accept with and without forward slash /
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true);
}
wrapRequest() — wraps the incoming request to strip the trailing slash, behaving closest to the legacy setUseTrailingSlashMatch(true) without issuing HTTP redirects
redirect() — would send a 301/308 redirect to the client, which changes the HTTP method and adds a round-trip
📌 Summary
✅ Final Conclusion
✔️ The full platform isfully functional on Spring Boot 4.0:
Web (Spring MVC)
JPA / Hibernate
Jackson 3
Pub/Sub + Spring Integration
GCP Secret Manager
Testcontainers
Lombok (compile-time only)
Observability / Actuator
👉 After applying all fixes →no runtime issues
💡 Final Insight
Spring Boot 4 is not just an upgrade — it’s aplatform evolution: