+ * // Aspire automatically sets SSL_CERT_DIR or mounts certificates at /usr/lib/ssl/aspire
+ * // This configuration will automatically detect and trust those certificates
+ *
+ */
+@Configuration
+public class SslTrustConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(SslTrustConfiguration.class);
+
+ @Bean
+ public X509TrustManager sslTrustManager() {
+ try {
+ TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ defaultTmf.init((KeyStore) null);
+ javax.net.ssl.TrustManager[] trustManagers = defaultTmf.getTrustManagers();
+ if (trustManagers == null || trustManagers.length == 0 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("No X509TrustManager available from default TrustManagerFactory");
+ }
+ X509TrustManager defaultTrustManager = (X509TrustManager) trustManagers[0];
+
+ List devCerts = loadDevelopmentCertificates();
+ if (devCerts.isEmpty()) {
+ logger.info("SSL trust: Using default trust manager (no development certificates found)");
+ return defaultTrustManager;
+ }
+
+ logger.info("SSL trust: Loaded {} development certificate(s)", devCerts.size());
+ return new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ defaultTrustManager.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ try {
+ defaultTrustManager.checkServerTrusted(chain, authType);
+ } catch (CertificateException e) {
+ // If default validation fails, check if any certificate in the chain
+ // is signed by or matches a development certificate
+ logger.debug("Default trust validation failed, checking development certificates...");
+ for (X509Certificate cert : chain) {
+ X500Principal certSubject = cert.getSubjectX500Principal();
+ X500Principal certIssuer = cert.getIssuerX500Principal();
+ logger.trace("Checking certificate: {}", certSubject);
+
+ // Check if this certificate matches or is signed by a dev cert
+ for (X509Certificate devCert : devCerts) {
+ X500Principal devCertSubject = devCert.getSubjectX500Principal();
+ X500Principal devCertIssuer = devCert.getIssuerX500Principal();
+
+ // Check if certificate matches dev cert (same serial/issuer or exact match)
+ if (cert.getSerialNumber().equals(devCert.getSerialNumber()) ||
+ certIssuer.equals(devCertIssuer) ||
+ cert.equals(devCert)) {
+ logger.debug("Trusting certificate signed by development CA: {}", certSubject);
+ return; // Trusted by development CA
+ }
+
+ // Check if this certificate's issuer matches a dev cert's subject
+ // (meaning the dev cert is the CA that signed this cert)
+ if (certIssuer.equals(devCertSubject)) {
+ logger.debug("Trusting certificate signed by development CA: {}", certSubject);
+ return; // Trusted by development CA
+ }
+ }
+ }
+ // If we get here, the certificate chain doesn't include any development certificates
+ logger.warn("Certificate validation failed and no development certificate found in chain");
+ throw e;
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ X509Certificate[] defaultCerts = defaultTrustManager.getAcceptedIssuers();
+ X509Certificate[] allCerts = new X509Certificate[defaultCerts.length + devCerts.size()];
+ System.arraycopy(defaultCerts, 0, allCerts, 0, defaultCerts.length);
+ System.arraycopy(devCerts.toArray(new X509Certificate[0]), 0, allCerts, defaultCerts.length, devCerts.size());
+ return allCerts;
+ }
+ };
+ } catch (Exception e) {
+ logger.error("Failed to create SSL trust manager, using default", e);
+ try {
+ TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ defaultTmf.init((KeyStore) null);
+ javax.net.ssl.TrustManager[] trustManagers = defaultTmf.getTrustManagers();
+ if (trustManagers == null || trustManagers.length == 0 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("No X509TrustManager available from default TrustManagerFactory");
+ }
+ return (X509TrustManager) trustManagers[0];
+ } catch (Exception ex) {
+ logger.error("Failed to create default trust manager", ex);
+ throw new RuntimeException("Failed to create trust manager", ex);
+ }
+ }
+ }
+
+ private List loadDevelopmentCertificates() {
+ List certificates = new ArrayList<>();
+ logger.debug("Loading development certificates...");
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ String[] certPaths = {
+ System.getenv("SSL_CERT_DIR"),
+ "/usr/lib/ssl/aspire",
+ System.getenv("SSL_CERT_FILE")
+ };
+
+ logger.trace("Checking certificate paths: SSL_CERT_DIR={}, /usr/lib/ssl/aspire, SSL_CERT_FILE={}",
+ System.getenv("SSL_CERT_DIR"), System.getenv("SSL_CERT_FILE"));
+
+ for (String certPath : certPaths) {
+ if (certPath == null) continue;
+ Path path = Paths.get(certPath);
+ if (Files.isDirectory(path)) {
+ logger.debug("Scanning directory for certificates: {}", certPath);
+ try (var stream = Files.walk(path)) {
+ stream.filter(Files::isRegularFile)
+ .filter(p -> p.toString().matches(".*\\.(pem|crt|cer)$"))
+ .forEach(p -> {
+ try {
+ try (var inputStream = Files.newInputStream(p)) {
+ certificates.add((X509Certificate) certFactory.generateCertificate(inputStream));
+ logger.debug("Loaded certificate: {}", p);
+ }
+ } catch (Exception e) {
+ logger.warn("Failed to load certificate {}: {}", p, e.getMessage());
+ }
+ });
+ }
+ } else if (Files.isRegularFile(path)) {
+ try (var inputStream = Files.newInputStream(path)) {
+ certificates.add((X509Certificate) certFactory.generateCertificate(inputStream));
+ logger.debug("Loaded certificate: {}", path);
+ } catch (Exception e) {
+ logger.warn("Failed to load certificate {}: {}", path, e.getMessage());
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.error("Error loading development certificates", e);
+ }
+ return certificates;
+ }
+}
diff --git a/spring-boot-admin/metadata/IMAGE_VERSION b/spring-boot-admin/metadata/IMAGE_VERSION
index c492825..3cf5751 100644
--- a/spring-boot-admin/metadata/IMAGE_VERSION
+++ b/spring-boot-admin/metadata/IMAGE_VERSION
@@ -1 +1 @@
-3.5.6
+3.5.7
diff --git a/spring-boot-admin/patches/application.properties.patch b/spring-boot-admin/patches/application.properties.patch
index 4963c92..82ef4d5 100644
--- a/spring-boot-admin/patches/application.properties.patch
+++ b/spring-boot-admin/patches/application.properties.patch
@@ -1,5 +1,6 @@
--- ./src/main/resources/application.properties 2025-10-01 14:13:49.968047867 -0500
-+++ ./src/main/resources/application.properties 2025-10-01 14:13:24.727639700 -0500
-@@ -0,0 +1,2 @@
++++ ./src/main/resources/application.properties 2026-01-27 00:00:00.000000000 -0500
+@@ -0,0 +1,3 @@
+server.port=9099
+spring.thymeleaf.check-template-location=false
++logging.level.io.steeltoe.docker.ssl=INFO
diff --git a/spring-boot-admin/patches/build.gradle.patch b/spring-boot-admin/patches/build.gradle.patch
index afc78c3..3b19e03 100644
--- a/spring-boot-admin/patches/build.gradle.patch
+++ b/spring-boot-admin/patches/build.gradle.patch
@@ -1,6 +1,6 @@
--- ./build.gradle 2025-09-22 14:48:20.000000000 -0500
+++ ./build.gradle 2026-01-27 00:00:00.000000000 -0500
-@@ -38,3 +38,10 @@
+@@ -38,3 +38,11 @@
tasks.named('test') {
useJUnitPlatform()
}
@@ -8,6 +8,7 @@
+bootBuildImage {
+ createdDate = "now"
+ environment = [
-+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true"
++ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
++ "BP_NATIVE_IMAGE_BUILD_ARGUMENTS": "-H:+UnlockExperimentalVMOptions"
+ ]
+}
diff --git a/spring-boot-admin/patches/enable-springbootadmin.patch b/spring-boot-admin/patches/enable-springbootadmin.patch
index ebd9f14..877a062 100644
--- a/spring-boot-admin/patches/enable-springbootadmin.patch
+++ b/spring-boot-admin/patches/enable-springbootadmin.patch
@@ -1,13 +1,22 @@
--- ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2024-09-20 12:49:35.099908129 -0500
-+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2024-09-20 12:49:59.410273961 -0500
-@@ -2,8 +2,10 @@
++++ ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2026-01-27 00:00:00.000000000 -0500
+@@ -1,13 +1,18 @@
+ package io.steeltoe.docker.springbootadmin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import de.codecentric.boot.admin.server.config.EnableAdminServer;
++import org.springframework.context.annotation.ComponentScan;
++import org.springframework.context.annotation.Import;
- @SpringBootApplication
+@EnableAdminServer
++@ComponentScan(basePackages = { "io.steeltoe.docker.springbootadmin", "io.steeltoe.docker.ssl" })
++@Import(SteeltoeAdminConfiguration.class)
+ @SpringBootApplication
public class SpringBootAdmin {
public static void main(String[] args) {
+ SpringApplication.run(SpringBootAdmin.class, args);
+ }
+-
+ }
diff --git a/spring-boot-admin/patches/spring-boot-admin-ssl-config.patch b/spring-boot-admin/patches/spring-boot-admin-ssl-config.patch
new file mode 100644
index 0000000..2aec452
--- /dev/null
+++ b/spring-boot-admin/patches/spring-boot-admin-ssl-config.patch
@@ -0,0 +1,82 @@
+--- /dev/null
++++ ./src/main/java/io/steeltoe/docker/ssl/SpringBootAdminSslConfiguration.java 2026-01-27 00:00:00.000000000 +0000
+@@ -0,0 +1,79 @@
++package io.steeltoe.docker.ssl;
++
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import org.springframework.beans.factory.ObjectProvider;
++import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
++import org.springframework.context.annotation.Bean;
++import org.springframework.context.annotation.Configuration;
++import org.springframework.http.client.reactive.ClientHttpConnector;
++import org.springframework.http.client.reactive.ReactorClientHttpConnector;
++import io.netty.handler.ssl.SslContext;
++import reactor.netty.http.client.HttpClient;
++import reactor.netty.tcp.SslProvider;
++import reactor.netty.tcp.TcpSslContextSpec;
++
++import javax.net.ssl.X509TrustManager;
++
++/**
++ * Spring Boot Admin SSL Configuration
++ *
++ * Configures Spring Boot Admin's WebClient to use the shared SSL trust manager
++ * for trusting development certificates (e.g., Aspire development certificates).
++ *
++ * Uses ObjectProvider for AOT compatibility - the trust manager is resolved at runtime
++ * when the bean method is called, not at configuration class construction time.
++ */
++@Configuration
++public class SpringBootAdminSslConfiguration {
++
++ private static final Logger logger = LoggerFactory.getLogger(SpringBootAdminSslConfiguration.class);
++ private final ObjectProvider trustManagerProvider;
++
++ public SpringBootAdminSslConfiguration(ObjectProvider trustManagerProvider) {
++ this.trustManagerProvider = trustManagerProvider;
++ }
++
++ /**
++ * Provides a ClientHttpConnector with custom SSL trust for Spring Boot Admin's WebClient.
++ *
++ * Uses ObjectProvider to defer trust manager resolution until runtime, making this
++ * AOT-compatible. The trust manager is resolved when this bean method is called,
++ * not during AOT processing at build time.
++ */
++ @Bean
++ @ConditionalOnMissingBean(ClientHttpConnector.class)
++ public ClientHttpConnector clientHttpConnector() {
++ logger.info("Configuring Spring Boot Admin WebClient with SSL trust support");
++ X509TrustManager trustManager = trustManagerProvider.getIfAvailable();
++
++ if (trustManager == null) {
++ logger.debug("No custom X509TrustManager available, using default SSL configuration");
++ return new ReactorClientHttpConnector(HttpClient.create());
++ }
++
++ try {
++ logger.info("Using custom X509TrustManager for Spring Boot Admin WebClient");
++ // Build SslContext first to avoid deprecated sslContext(ProtocolSslContextSpec) method
++ SslContext sslContext = TcpSslContextSpec.forClient()
++ .configure(sslContextBuilder -> {
++ sslContextBuilder.trustManager(trustManager);
++ })
++ .sslContext();
++
++ SslProvider sslProvider = SslProvider.builder()
++ .sslContext(sslContext)
++ .build();
++
++ HttpClient httpClient = HttpClient.create()
++ .secure(sslProvider);
++
++ logger.debug("Configured Spring Boot Admin WebClient with custom SSL trust");
++ return new ReactorClientHttpConnector(httpClient);
++ } catch (Exception e) {
++ logger.error("Failed to configure SSL trust for Spring Boot Admin WebClient, using default", e);
++ // Fall back to default connector if SSL configuration fails
++ return new ReactorClientHttpConnector(HttpClient.create());
++ }
++ }
++}
diff --git a/spring-boot-admin/patches/steeltoe-admin-config.patch b/spring-boot-admin/patches/steeltoe-admin-config.patch
new file mode 100644
index 0000000..783b16c
--- /dev/null
+++ b/spring-boot-admin/patches/steeltoe-admin-config.patch
@@ -0,0 +1,129 @@
+--- /dev/null
++++ ./src/main/java/io/steeltoe/docker/springbootadmin/SteeltoeAdminConfiguration.java 2026-01-27 00:00:00.000000000 +0000
+@@ -0,0 +1,126 @@
++package io.steeltoe.docker.springbootadmin;
++
++import com.fasterxml.jackson.core.JsonParser;
++import com.fasterxml.jackson.databind.DeserializationContext;
++import com.fasterxml.jackson.databind.JsonDeserializer;
++import com.fasterxml.jackson.databind.JsonNode;
++import com.fasterxml.jackson.databind.ObjectMapper;
++import com.fasterxml.jackson.databind.module.SimpleModule;
++import de.codecentric.boot.admin.server.web.client.InstanceWebClientCustomizer;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import org.springframework.beans.factory.ObjectProvider;
++import org.springframework.context.annotation.Bean;
++import org.springframework.context.annotation.Configuration;
++import org.springframework.core.annotation.Order;
++import org.springframework.http.client.reactive.ClientHttpConnector;
++import org.springframework.http.codec.json.Jackson2JsonDecoder;
++import org.springframework.http.codec.json.Jackson2JsonEncoder;
++import org.springframework.web.reactive.function.client.ExchangeStrategies;
++import org.springframework.web.reactive.function.client.WebClient;
++
++import java.io.IOException;
++import java.util.HashMap;
++import java.util.Map;
++
++/**
++ * Configuration to make Spring Boot Admin compatible with Steeltoe actuator responses.
++ *
++ * Steeltoe adds a "type":"Steeltoe" property to its actuator index response, which causes
++ * deserialization failures in AOT-compiled Spring Boot Admin. This configuration provides
++ * a custom WebClient with a manual deserializer that only extracts the _links field.
++ *
++ * @see GraalVM Reflection Metadata
++ */
++@Configuration(proxyBeanMethods = false)
++public class SteeltoeAdminConfiguration {
++
++ private static final Logger log = LoggerFactory.getLogger(SteeltoeAdminConfiguration.class);
++
++ @Bean
++ @Order(-100)
++ public InstanceWebClientCustomizer steeltoeInstanceWebClientCustomizer(
++ ObjectMapper objectMapper,
++ ObjectProvider clientHttpConnectorProvider) {
++
++ log.info("Configuring Spring Boot Admin WebClient for Steeltoe compatibility");
++
++ return (builder) -> {
++ try {
++ Class> responseClass = Class.forName(
++ "de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy$Response");
++ Class> endpointRefClass = Class.forName(
++ "de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy$Response$EndpointRef");
++
++ // Copy the base ObjectMapper to preserve other configuration (date formats, etc.)
++ // while adding our custom deserializer for the actuator index response
++ ObjectMapper mapper = objectMapper.copy();
++ SimpleModule module = new SimpleModule("SteeltoeCompatibility");
++ @SuppressWarnings({"unchecked", "rawtypes"})
++ JsonDeserializer deserializer = new ActuatorIndexResponseDeserializer(responseClass, endpointRefClass);
++ module.addDeserializer((Class) responseClass, deserializer);
++ mapper.registerModule(module);
++
++ ExchangeStrategies strategies = ExchangeStrategies.builder()
++ .codecs(configurer -> {
++ configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
++ configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));
++ })
++ .build();
++
++ WebClient.Builder webClientBuilder = WebClient.builder().exchangeStrategies(strategies);
++ clientHttpConnectorProvider.ifAvailable(connector -> {
++ webClientBuilder.clientConnector(connector);
++ log.info("Using custom ClientHttpConnector for SSL trust");
++ });
++
++ builder.webClient(webClientBuilder);
++ log.info("Steeltoe-compatible WebClient configuration complete");
++
++ } catch (ClassNotFoundException e) {
++ log.error("Failed to load SBA Response classes: {}", e.getMessage());
++ }
++ };
++ }
++
++ @SuppressWarnings("rawtypes")
++ public static class ActuatorIndexResponseDeserializer extends JsonDeserializer {
++ private static final Logger log = LoggerFactory.getLogger(ActuatorIndexResponseDeserializer.class);
++ private final Class> responseClass;
++ private final Class> endpointRefClass;
++
++ public ActuatorIndexResponseDeserializer(Class> responseClass, Class> endpointRefClass) {
++ this.responseClass = responseClass;
++ this.endpointRefClass = endpointRefClass;
++ }
++
++ @Override
++ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
++ JsonNode node = p.getCodec().readTree(p);
++ try {
++ Object response = responseClass.getDeclaredConstructor().newInstance();
++ Map
-+ * // Aspire automatically sets SSL_CERT_DIR or mounts certificates at /usr/lib/ssl/aspire
-+ * // This configuration will automatically detect and trust those certificates
-+ *
++ *
Supported certificate formats: .pem, .crt, .cer
+ */
+@Configuration
+public class SslTrustConfiguration {
@@ -84,7 +77,7 @@
+ for (X509Certificate cert : chain) {
+ X500Principal certSubject = cert.getSubjectX500Principal();
+ logger.trace("Checking certificate: {}", certSubject);
-+
++
+ // Check if this certificate matches or is signed by a dev cert
+ for (X509Certificate devCert : devCerts) {
+ // First check for exact match
@@ -92,7 +85,7 @@
+ logger.debug("Trusting certificate (exact match with development cert): {}", certSubject);
+ return;
+ }
-+
++
+ // Then verify cryptographic signature
+ // Only trust certs signed by dev CAs if the dev cert is actually a CA
+ try {
@@ -102,14 +95,14 @@
+ logger.trace("Development cert is not a CA, skipping signature verification: {}", devCert.getSubjectX500Principal());
+ continue;
+ }
-+
++
+ // Verify that the cert was signed by the dev cert
+ cert.verify(devCert.getPublicKey());
+ logger.debug("Trusting certificate signed by development CA: {}", certSubject);
+ return; // Trusted by development CA
+ } catch (Exception verifyException) {
+ // Signature verification failed, continue checking other dev certs
-+ logger.trace("Signature verification failed for cert {} with dev cert {}: {}",
++ logger.trace("Signature verification failed for cert {} with dev cert {}: {}",
+ certSubject, devCert.getSubjectX500Principal(), verifyException.getMessage());
+ }
+ }
@@ -151,17 +144,27 @@
+ logger.debug("Loading development certificates...");
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
-+ String[] certPaths = {
-+ System.getenv("SSL_CERT_DIR"),
-+ "/usr/lib/ssl/aspire",
-+ System.getenv("SSL_CERT_FILE")
-+ };
++ List certPaths = new ArrayList<>();
++
++ // SSL_CERT_DIR can be colon-separated list of directories (OpenSSL standard)
++ String sslCertDir = System.getenv("SSL_CERT_DIR");
++ if (sslCertDir != null && !sslCertDir.isEmpty()) {
++ for (String dir : sslCertDir.split(":")) {
++ if (!dir.isEmpty()) {
++ certPaths.add(dir);
++ }
++ }
++ }
++
++ // SSL_CERT_FILE for single certificate file
++ String sslCertFile = System.getenv("SSL_CERT_FILE");
++ if (sslCertFile != null && !sslCertFile.isEmpty()) {
++ certPaths.add(sslCertFile);
++ }
+
-+ logger.trace("Checking certificate paths: SSL_CERT_DIR={}, /usr/lib/ssl/aspire, SSL_CERT_FILE={}",
-+ System.getenv("SSL_CERT_DIR"), System.getenv("SSL_CERT_FILE"));
++ logger.debug("Checking certificate paths: {}", certPaths);
+
+ for (String certPath : certPaths) {
-+ if (certPath == null) continue;
+ Path path = Paths.get(certPath);
+ if (Files.isDirectory(path)) {
+ logger.debug("Scanning directory for certificates: {}", certPath);
@@ -186,6 +189,8 @@
+ } catch (Exception e) {
+ logger.warn("Failed to load certificate {}: {}", path, e.getMessage());
+ }
++ } else {
++ logger.trace("Path does not exist or is not accessible: {}", certPath);
+ }
+ }
+ } catch (Exception e) {
diff --git a/spring-boot-admin/patches/steeltoe-admin-config.patch b/spring-boot-admin/patches/steeltoe-admin-config.patch
index 783b16c..b4f74cd 100644
--- a/spring-boot-admin/patches/steeltoe-admin-config.patch
+++ b/spring-boot-admin/patches/steeltoe-admin-config.patch
@@ -81,7 +81,7 @@
+ log.info("Steeltoe-compatible WebClient configuration complete");
+
+ } catch (ClassNotFoundException e) {
-+ log.error("Failed to load SBA Response classes: {}", e.getMessage());
++ log.error("Failed to load SBA Response class: {}", e.getMessage());
+ }
+ };
+ }
diff --git a/uaa-server/README.md b/uaa-server/README.md
index 5cd53ce..3a12b70 100644
--- a/uaa-server/README.md
+++ b/uaa-server/README.md
@@ -7,13 +7,13 @@ This directory contains resources for building a [CloudFoundry User Account and
To run this image locally:
```shell
-docker run -it -p 8080:8080 --name steeltoe-uaa steeltoe.azurecr.io/uaa-server:78
+docker run -it -p 8080:8080 --name steeltoe-uaa steeltoe.azurecr.io/uaa-server
```
To run this image locally, overwriting the included `uaa.yml` file:
```shell
-docker run -it -p 8080:8080 --name steeltoe-uaa -v $pwd/uaa.yml:/uaa/uaa.yml steeltoe.azurecr.io/uaa-server:78
+docker run -it -p 8080:8080 --name steeltoe-uaa -v $pwd/uaa.yml:/uaa/uaa.yml steeltoe.azurecr.io/uaa-server
```
## Customizing for your Cloud Foundry environment
@@ -27,6 +27,6 @@ These instructions will help you build and deploy a custom image to use as an id
1. `.\build.ps1 uaa-server`.
1. Push the image to an image repository accessible from your Cloud Foundry environment.
1. Deploy the image with a command similar to this:
- * `cf push steeltoe-uaa --docker-image steeltoe.azurecr.io/uaa-server:78`
-1. (Operator task) [Add the new identity provider with OpenID Connect](https://techdocs.broadcom.com/us/en/vmware-tanzu/platform-services/single-sign-on-for-tanzu/1-16/sso-tanzu/operator-guide.html#config-ext-id)
+ * `cf push steeltoe-uaa --docker-image steeltoe.azurecr.io/uaa-server`
+1. (Operator task) [Add the new identity provider with OpenID Connect](https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/single-sign-on/1-16/sso/configure-external-id.html#config-ext-prov)
* Use the `ssotile` credentials from uaa.yml
From 2956bc1ebac4f2adae6aa745020c93685b127a9f Mon Sep 17 00:00:00 2001
From: Tim Hess
Date: Fri, 6 Feb 2026 11:49:34 -0600
Subject: [PATCH 09/16] enable class data sharing for non-native images (.5
second faster start times)
---
config-server/patches/build.gradle.patch | 5 +++--
eureka-server/patches/build.gradle.patch | 5 +++--
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/config-server/patches/build.gradle.patch b/config-server/patches/build.gradle.patch
index b6d01c6..fa07a86 100644
--- a/config-server/patches/build.gradle.patch
+++ b/config-server/patches/build.gradle.patch
@@ -1,6 +1,6 @@
--- ./build.gradle 2025-09-30 14:48:20.000000000 -0500
+++ ./build.gradle 2025-09-30 14:49:16.584226000 -0500
-@@ -41,3 +41,10 @@
+@@ -41,3 +41,11 @@
tasks.named('test') {
useJUnitPlatform()
}
@@ -8,6 +8,7 @@
+bootBuildImage {
+ createdDate = "now"
+ environment = [
-+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true"
++ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
++ "BP_JVM_CDS_ENABLED": "true"
+ ]
+}
diff --git a/eureka-server/patches/build.gradle.patch b/eureka-server/patches/build.gradle.patch
index b6d01c6..fa07a86 100644
--- a/eureka-server/patches/build.gradle.patch
+++ b/eureka-server/patches/build.gradle.patch
@@ -1,6 +1,6 @@
--- ./build.gradle 2025-09-30 14:48:20.000000000 -0500
+++ ./build.gradle 2025-09-30 14:49:16.584226000 -0500
-@@ -41,3 +41,10 @@
+@@ -41,3 +41,11 @@
tasks.named('test') {
useJUnitPlatform()
}
@@ -8,6 +8,7 @@
+bootBuildImage {
+ createdDate = "now"
+ environment = [
-+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true"
++ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
++ "BP_JVM_CDS_ENABLED": "true"
+ ]
+}
From 8c9534c224079dc16c3f23b945eea8a07f5ff700 Mon Sep 17 00:00:00 2001
From: Tim Hess
Date: Fri, 6 Feb 2026 19:43:29 -0600
Subject: [PATCH 10/16] BP_JVM_CDS_ENABLED => BP_JVM_AOTCACHE_ENABLED
- remove last tag from readme
- un-reorder lines for sba
---
config-server/README.md | 4 ++--
config-server/patches/build.gradle.patch | 2 +-
eureka-server/patches/build.gradle.patch | 2 +-
spring-boot-admin/patches/enable-springbootadmin.patch | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/config-server/README.md b/config-server/README.md
index 91ac6f0..269e66d 100644
--- a/config-server/README.md
+++ b/config-server/README.md
@@ -20,7 +20,7 @@ docker run --publish 8888:8888 steeltoe.azurecr.io/config-server \
Local file system configuration:
```shell
-docker run --publish 8888:8888 --volume /path/to/my/config:/config steeltoe.azurecr.io/config-server:4 \
+docker run --publish 8888:8888 --volume /path/to/my/config:/config steeltoe.azurecr.io/config-server \
--spring.profiles.active=native \
--spring.cloud.config.server.native.searchLocations=file:///config
```
@@ -40,6 +40,6 @@ docker run --publish 8888:8888 steeltoe.azurecr.io/config-server \
| ---- | ----------- |
| /_{app}_/_{profile}_ | Configuration data for app in Spring profile |
| /_{app}_/_{profile}_/_{label}_ | Add a git label |
-| /_{app}_/_{profiles}/{label}_/_{path}_ | Environment-specific plain text config file at _{path}_|
+| /_{app}_/_{profiles}/{label}_/_{path}_ | Environment-specific plain text config file at _{path}_ |
_Example:_
diff --git a/config-server/patches/build.gradle.patch b/config-server/patches/build.gradle.patch
index fa07a86..e72ffbe 100644
--- a/config-server/patches/build.gradle.patch
+++ b/config-server/patches/build.gradle.patch
@@ -9,6 +9,6 @@
+ createdDate = "now"
+ environment = [
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
-+ "BP_JVM_CDS_ENABLED": "true"
++ "BP_JVM_AOTCACHE_ENABLED": "true"
+ ]
+}
diff --git a/eureka-server/patches/build.gradle.patch b/eureka-server/patches/build.gradle.patch
index fa07a86..e72ffbe 100644
--- a/eureka-server/patches/build.gradle.patch
+++ b/eureka-server/patches/build.gradle.patch
@@ -9,6 +9,6 @@
+ createdDate = "now"
+ environment = [
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
-+ "BP_JVM_CDS_ENABLED": "true"
++ "BP_JVM_AOTCACHE_ENABLED": "true"
+ ]
+}
diff --git a/spring-boot-admin/patches/enable-springbootadmin.patch b/spring-boot-admin/patches/enable-springbootadmin.patch
index c561e71..38e5c2a 100644
--- a/spring-boot-admin/patches/enable-springbootadmin.patch
+++ b/spring-boot-admin/patches/enable-springbootadmin.patch
@@ -7,7 +7,7 @@
+import org.springframework.context.annotation.Import;
+import de.codecentric.boot.admin.server.config.EnableAdminServer;
+ @SpringBootApplication
+@EnableAdminServer
+@Import(SteeltoeAdminConfiguration.class)
- @SpringBootApplication
public class SpringBootAdmin {
From 9821cd259da7d03628272b507644cddd361f5fd2 Mon Sep 17 00:00:00 2001
From: Tim Hess
Date: Mon, 9 Feb 2026 11:51:55 -0600
Subject: [PATCH 11/16] Build on PR to any branch, but with more restictive
path filter
---
.github/workflows/build_config_server.yaml | 8 ++++----
.github/workflows/build_eureka_server.yaml | 8 ++++----
.github/workflows/build_springboot_admin_server.yaml | 8 ++++----
.github/workflows/build_uaa_server.yaml | 12 ++++++++----
4 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/build_config_server.yaml b/.github/workflows/build_config_server.yaml
index 995323b..998dc2f 100644
--- a/.github/workflows/build_config_server.yaml
+++ b/.github/workflows/build_config_server.yaml
@@ -2,18 +2,18 @@ name: Build Config Server
on:
pull_request:
- branches:
- - main
paths:
- '.github/workflows/build_config_server.yaml'
- - 'config-server/**'
+ - 'config-server/metadata/*'
+ - 'config-server/patches/*'
- 'build.ps1'
push:
branches:
- main
paths:
- '.github/workflows/build_config_server.yaml'
- - 'config-server/**'
+ - 'config-server/metadata/*'
+ - 'config-server/patches/*'
- 'build.ps1'
concurrency:
diff --git a/.github/workflows/build_eureka_server.yaml b/.github/workflows/build_eureka_server.yaml
index 5777ecc..6ac942a 100644
--- a/.github/workflows/build_eureka_server.yaml
+++ b/.github/workflows/build_eureka_server.yaml
@@ -2,18 +2,18 @@ name: Build Eureka Server
on:
pull_request:
- branches:
- - main
paths:
- '.github/workflows/build_eureka_server.yaml'
- - 'eureka-server/**'
+ - 'eureka-server/metadata/*'
+ - 'eureka-server/patches/*'
- 'build.ps1'
push:
branches:
- main
paths:
- '.github/workflows/build_eureka_server.yaml'
- - 'eureka-server/**'
+ - 'eureka-server/metadata/*'
+ - 'eureka-server/patches/*'
- 'build.ps1'
concurrency:
diff --git a/.github/workflows/build_springboot_admin_server.yaml b/.github/workflows/build_springboot_admin_server.yaml
index e47e85b..d2ba5cf 100644
--- a/.github/workflows/build_springboot_admin_server.yaml
+++ b/.github/workflows/build_springboot_admin_server.yaml
@@ -2,18 +2,18 @@ name: Build Spring Boot Admin Server
on:
pull_request:
- branches:
- - main
paths:
- '.github/workflows/build_springboot_admin_server.yaml'
- - 'spring-boot-admin/**'
+ - 'spring-boot-admin/metadata/*'
+ - 'spring-boot-admin/patches/*'
- 'build.ps1'
push:
branches:
- main
paths:
- '.github/workflows/build_springboot_admin_server.yaml'
- - 'spring-boot-admin/**'
+ - 'spring-boot-admin/metadata/*'
+ - 'spring-boot-admin/patches/*'
- 'build.ps1'
concurrency:
diff --git a/.github/workflows/build_uaa_server.yaml b/.github/workflows/build_uaa_server.yaml
index faff82c..eb7504a 100644
--- a/.github/workflows/build_uaa_server.yaml
+++ b/.github/workflows/build_uaa_server.yaml
@@ -2,18 +2,22 @@ name: Build UAA Server
on:
pull_request:
- branches:
- - main
paths:
- '.github/workflows/build_uaa_server.yaml'
- - 'uaa-server/**'
+ - 'uaa-server/Dockerfile'
+ - 'uaa-server/metadata/*'
+ - 'uaa-server/*.yml'
+ - 'uaa-server/*.properties'
- 'build.ps1'
push:
branches:
- main
paths:
- '.github/workflows/build_uaa_server.yaml'
- - 'uaa-server/**'
+ - 'uaa-server/Dockerfile'
+ - 'uaa-server/metadata/*'
+ - 'uaa-server/*.yml'
+ - 'uaa-server/*.properties'
- 'build.ps1'
concurrency:
From 5be24baa71c38a524a41fc0beb33c33e244cbcb9 Mon Sep 17 00:00:00 2001
From: Tim Hess
Date: Fri, 6 Feb 2026 19:47:34 -0600
Subject: [PATCH 12/16] switch from native to aot
---
build.ps1 | 2 +-
.../patches/application.properties.patch | 3 +-
spring-boot-admin/patches/build.gradle.patch | 2 +-
.../patches/enable-springbootadmin.patch | 5 +-
.../spring-boot-admin-ssl-config.patch | 82 -------
.../patches/ssl-trust-config.patch | 201 ------------------
.../patches/steeltoe-admin-config.patch | 129 -----------
7 files changed, 5 insertions(+), 419 deletions(-)
delete mode 100644 spring-boot-admin/patches/spring-boot-admin-ssl-config.patch
delete mode 100644 spring-boot-admin/patches/ssl-trust-config.patch
delete mode 100644 spring-boot-admin/patches/steeltoe-admin-config.patch
diff --git a/build.ps1 b/build.ps1
index a2eb292..2745fd6 100755
--- a/build.ps1
+++ b/build.ps1
@@ -164,7 +164,7 @@ try {
}
"spring-boot-admin" {
$appName = "SpringBootAdmin"
- $dependencies = "codecentric-spring-boot-admin-server,native"
+ $dependencies = "codecentric-spring-boot-admin-server"
}
Default {
Write-Host "$Name is not currently supported by this script"
diff --git a/spring-boot-admin/patches/application.properties.patch b/spring-boot-admin/patches/application.properties.patch
index 6b737a6..2a76cc2 100644
--- a/spring-boot-admin/patches/application.properties.patch
+++ b/spring-boot-admin/patches/application.properties.patch
@@ -1,6 +1,5 @@
--- ./src/main/resources/application.properties 2025-10-01 14:13:49.968047867 -0500
+++ ./src/main/resources/application.properties 2026-01-27 00:00:00.000000000 -0500
-@@ -0,0 +1,3 @@
+@@ -0,0 +1,2 @@
+server.port=9099
+spring.thymeleaf.check-template-location=false
-+logging.level.io.steeltoe.docker=INFO
diff --git a/spring-boot-admin/patches/build.gradle.patch b/spring-boot-admin/patches/build.gradle.patch
index 3b19e03..05413d3 100644
--- a/spring-boot-admin/patches/build.gradle.patch
+++ b/spring-boot-admin/patches/build.gradle.patch
@@ -9,6 +9,6 @@
+ createdDate = "now"
+ environment = [
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
-+ "BP_NATIVE_IMAGE_BUILD_ARGUMENTS": "-H:+UnlockExperimentalVMOptions"
++ "BP_JVM_AOTCACHE_ENABLED": "true"
+ ]
+}
diff --git a/spring-boot-admin/patches/enable-springbootadmin.patch b/spring-boot-admin/patches/enable-springbootadmin.patch
index 38e5c2a..1208c3f 100644
--- a/spring-boot-admin/patches/enable-springbootadmin.patch
+++ b/spring-boot-admin/patches/enable-springbootadmin.patch
@@ -1,13 +1,12 @@
--- ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2024-09-20 12:49:35.099908129 -0500
+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2026-01-27 00:00:00.000000000 -0500
-@@ -2,7 +2,11 @@
+@@ -2,7 +2,9 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-+import org.springframework.context.annotation.Import;
+import de.codecentric.boot.admin.server.config.EnableAdminServer;
@SpringBootApplication
+@EnableAdminServer
-+@Import(SteeltoeAdminConfiguration.class)
public class SpringBootAdmin {
+
diff --git a/spring-boot-admin/patches/spring-boot-admin-ssl-config.patch b/spring-boot-admin/patches/spring-boot-admin-ssl-config.patch
deleted file mode 100644
index a161228..0000000
--- a/spring-boot-admin/patches/spring-boot-admin-ssl-config.patch
+++ /dev/null
@@ -1,82 +0,0 @@
---- /dev/null
-+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdminSslConfiguration.java 2026-01-27 00:00:00.000000000 +0000
-@@ -0,0 +1,79 @@
-+package io.steeltoe.docker.springbootadmin;
-+
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import org.springframework.beans.factory.ObjectProvider;
-+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-+import org.springframework.context.annotation.Bean;
-+import org.springframework.context.annotation.Configuration;
-+import org.springframework.http.client.reactive.ClientHttpConnector;
-+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
-+import io.netty.handler.ssl.SslContext;
-+import reactor.netty.http.client.HttpClient;
-+import reactor.netty.tcp.SslProvider;
-+import reactor.netty.tcp.TcpSslContextSpec;
-+
-+import javax.net.ssl.X509TrustManager;
-+
-+/**
-+ * Spring Boot Admin SSL Configuration
-+ *
-+ * Configures Spring Boot Admin's WebClient to use the shared SSL trust manager
-+ * for trusting development certificates (e.g., ASP.NET Core development certificates).
-+ *
-+ * Uses ObjectProvider for AOT compatibility - the trust manager is resolved at runtime
-+ * when the bean method is called, not at configuration class construction time.
-+ */
-+@Configuration
-+public class SpringBootAdminSslConfiguration {
-+
-+ private static final Logger logger = LoggerFactory.getLogger(SpringBootAdminSslConfiguration.class);
-+ private final ObjectProvider trustManagerProvider;
-+
-+ public SpringBootAdminSslConfiguration(ObjectProvider trustManagerProvider) {
-+ this.trustManagerProvider = trustManagerProvider;
-+ }
-+
-+ /**
-+ * Provides a ClientHttpConnector with custom SSL trust for Spring Boot Admin's WebClient.
-+ *
-+ * Uses ObjectProvider to defer trust manager resolution until runtime, making this
-+ * AOT-compatible. The trust manager is resolved when this bean method is called,
-+ * not during AOT processing at build time.
-+ */
-+ @Bean
-+ @ConditionalOnMissingBean(ClientHttpConnector.class)
-+ public ClientHttpConnector clientHttpConnector() {
-+ logger.info("Configuring Spring Boot Admin WebClient with SSL trust support");
-+ X509TrustManager trustManager = trustManagerProvider.getIfAvailable();
-+
-+ if (trustManager == null) {
-+ logger.debug("No custom X509TrustManager available, using default SSL configuration");
-+ return new ReactorClientHttpConnector(HttpClient.create());
-+ }
-+
-+ try {
-+ logger.info("Using custom X509TrustManager for Spring Boot Admin WebClient");
-+ // Build SslContext first to avoid deprecated sslContext(ProtocolSslContextSpec) method
-+ SslContext sslContext = TcpSslContextSpec.forClient()
-+ .configure(sslContextBuilder -> {
-+ sslContextBuilder.trustManager(trustManager);
-+ })
-+ .sslContext();
-+
-+ SslProvider sslProvider = SslProvider.builder()
-+ .sslContext(sslContext)
-+ .build();
-+
-+ HttpClient httpClient = HttpClient.create()
-+ .secure(sslProvider);
-+
-+ logger.debug("Configured Spring Boot Admin WebClient with custom SSL trust");
-+ return new ReactorClientHttpConnector(httpClient);
-+ } catch (Exception e) {
-+ logger.error("Failed to configure SSL trust for Spring Boot Admin WebClient, using default", e);
-+ // Fall back to default connector if SSL configuration fails
-+ return new ReactorClientHttpConnector(HttpClient.create());
-+ }
-+ }
-+}
diff --git a/spring-boot-admin/patches/ssl-trust-config.patch b/spring-boot-admin/patches/ssl-trust-config.patch
deleted file mode 100644
index 6b061ab..0000000
--- a/spring-boot-admin/patches/ssl-trust-config.patch
+++ /dev/null
@@ -1,201 +0,0 @@
---- /dev/null
-+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SslTrustConfiguration.java 2026-01-27 00:00:00.000000000 +0000
-@@ -0,0 +1,198 @@
-+package io.steeltoe.docker.springbootadmin;
-+
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import org.springframework.context.annotation.Bean;
-+import org.springframework.context.annotation.Configuration;
-+
-+import javax.net.ssl.TrustManagerFactory;
-+import javax.net.ssl.X509TrustManager;
-+import java.nio.file.Files;
-+import java.nio.file.Path;
-+import java.nio.file.Paths;
-+import java.security.KeyStore;
-+import java.security.cert.CertificateException;
-+import java.security.cert.CertificateFactory;
-+import java.security.cert.X509Certificate;
-+import java.util.ArrayList;
-+import java.util.List;
-+import javax.security.auth.x500.X500Principal;
-+
-+/**
-+ * SSL Trust Configuration
-+ *
-+ *
This configuration class provides SSL certificate trust support for development environments.
-+ * It automatically loads certificates from environment variables or standard locations and
-+ * creates a custom TrustManager that trusts both standard CA certificates and development
-+ * certificates (e.g., ASP.NET Core development certificates).
-+ *
-+ *
Certificate locations checked:
-+ *
-+ *
{@code SSL_CERT_DIR} environment variable - colon-separated list of directories containing certificates
-+ *
{@code SSL_CERT_FILE} environment variable - path to a single certificate file
-+ *
-+ *
-+ *
Supported certificate formats: .pem, .crt, .cer
-+ */
-+@Configuration
-+public class SslTrustConfiguration {
-+
-+ private static final Logger logger = LoggerFactory.getLogger(SslTrustConfiguration.class);
-+
-+ @Bean
-+ public X509TrustManager sslTrustManager() {
-+ try {
-+ TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-+ defaultTmf.init((KeyStore) null);
-+ javax.net.ssl.TrustManager[] trustManagers = defaultTmf.getTrustManagers();
-+ if (trustManagers == null || trustManagers.length == 0 || !(trustManagers[0] instanceof X509TrustManager)) {
-+ throw new IllegalStateException("No X509TrustManager available from default TrustManagerFactory");
-+ }
-+ X509TrustManager defaultTrustManager = (X509TrustManager) trustManagers[0];
-+
-+ List devCerts = loadDevelopmentCertificates();
-+ if (devCerts.isEmpty()) {
-+ logger.info("SSL trust: Using default trust manager (no development certificates found)");
-+ return defaultTrustManager;
-+ }
-+
-+ logger.info("SSL trust: Loaded {} development certificate(s)", devCerts.size());
-+ return new X509TrustManager() {
-+ @Override
-+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-+ defaultTrustManager.checkClientTrusted(chain, authType);
-+ }
-+
-+ @Override
-+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-+ try {
-+ defaultTrustManager.checkServerTrusted(chain, authType);
-+ } catch (CertificateException e) {
-+ // If default validation fails, check if any certificate in the chain
-+ // is signed by or matches a development certificate
-+ logger.debug("Default trust validation failed, checking development certificates...");
-+ for (X509Certificate cert : chain) {
-+ X500Principal certSubject = cert.getSubjectX500Principal();
-+ logger.trace("Checking certificate: {}", certSubject);
-+
-+ // Check if this certificate matches or is signed by a dev cert
-+ for (X509Certificate devCert : devCerts) {
-+ // First check for exact match
-+ if (cert.equals(devCert)) {
-+ logger.debug("Trusting certificate (exact match with development cert): {}", certSubject);
-+ return;
-+ }
-+
-+ // Then verify cryptographic signature
-+ // Only trust certs signed by dev CAs if the dev cert is actually a CA
-+ try {
-+ // Check if dev cert has CA basic constraints
-+ boolean isCA = devCert.getBasicConstraints() != -1;
-+ if (!isCA) {
-+ logger.trace("Development cert is not a CA, skipping signature verification: {}", devCert.getSubjectX500Principal());
-+ continue;
-+ }
-+
-+ // Verify that the cert was signed by the dev cert
-+ cert.verify(devCert.getPublicKey());
-+ logger.debug("Trusting certificate signed by development CA: {}", certSubject);
-+ return; // Trusted by development CA
-+ } catch (Exception verifyException) {
-+ // Signature verification failed, continue checking other dev certs
-+ logger.trace("Signature verification failed for cert {} with dev cert {}: {}",
-+ certSubject, devCert.getSubjectX500Principal(), verifyException.getMessage());
-+ }
-+ }
-+ }
-+ // If we get here, the certificate chain doesn't include any development certificates
-+ logger.warn("Certificate validation failed and no development certificate found in chain");
-+ throw e;
-+ }
-+ }
-+
-+ @Override
-+ public X509Certificate[] getAcceptedIssuers() {
-+ X509Certificate[] defaultCerts = defaultTrustManager.getAcceptedIssuers();
-+ X509Certificate[] allCerts = new X509Certificate[defaultCerts.length + devCerts.size()];
-+ System.arraycopy(defaultCerts, 0, allCerts, 0, defaultCerts.length);
-+ System.arraycopy(devCerts.toArray(new X509Certificate[0]), 0, allCerts, defaultCerts.length, devCerts.size());
-+ return allCerts;
-+ }
-+ };
-+ } catch (Exception e) {
-+ logger.error("Failed to create SSL trust manager, using default", e);
-+ try {
-+ TrustManagerFactory defaultTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-+ defaultTmf.init((KeyStore) null);
-+ javax.net.ssl.TrustManager[] trustManagers = defaultTmf.getTrustManagers();
-+ if (trustManagers == null || trustManagers.length == 0 || !(trustManagers[0] instanceof X509TrustManager)) {
-+ throw new IllegalStateException("No X509TrustManager available from default TrustManagerFactory");
-+ }
-+ return (X509TrustManager) trustManagers[0];
-+ } catch (Exception ex) {
-+ logger.error("Failed to create default trust manager", ex);
-+ throw new RuntimeException("Failed to create trust manager", ex);
-+ }
-+ }
-+ }
-+
-+ private List loadDevelopmentCertificates() {
-+ List certificates = new ArrayList<>();
-+ logger.debug("Loading development certificates...");
-+ try {
-+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
-+ List certPaths = new ArrayList<>();
-+
-+ // SSL_CERT_DIR can be colon-separated list of directories (OpenSSL standard)
-+ String sslCertDir = System.getenv("SSL_CERT_DIR");
-+ if (sslCertDir != null && !sslCertDir.isEmpty()) {
-+ for (String dir : sslCertDir.split(":")) {
-+ if (!dir.isEmpty()) {
-+ certPaths.add(dir);
-+ }
-+ }
-+ }
-+
-+ // SSL_CERT_FILE for single certificate file
-+ String sslCertFile = System.getenv("SSL_CERT_FILE");
-+ if (sslCertFile != null && !sslCertFile.isEmpty()) {
-+ certPaths.add(sslCertFile);
-+ }
-+
-+ logger.debug("Checking certificate paths: {}", certPaths);
-+
-+ for (String certPath : certPaths) {
-+ Path path = Paths.get(certPath);
-+ if (Files.isDirectory(path)) {
-+ logger.debug("Scanning directory for certificates: {}", certPath);
-+ try (var stream = Files.walk(path)) {
-+ stream.filter(Files::isRegularFile)
-+ .filter(p -> p.toString().matches(".*\\.(pem|crt|cer)$"))
-+ .forEach(p -> {
-+ try {
-+ try (var inputStream = Files.newInputStream(p)) {
-+ certificates.add((X509Certificate) certFactory.generateCertificate(inputStream));
-+ logger.debug("Loaded certificate: {}", p);
-+ }
-+ } catch (Exception e) {
-+ logger.warn("Failed to load certificate {}: {}", p, e.getMessage());
-+ }
-+ });
-+ }
-+ } else if (Files.isRegularFile(path)) {
-+ try (var inputStream = Files.newInputStream(path)) {
-+ certificates.add((X509Certificate) certFactory.generateCertificate(inputStream));
-+ logger.debug("Loaded certificate: {}", path);
-+ } catch (Exception e) {
-+ logger.warn("Failed to load certificate {}: {}", path, e.getMessage());
-+ }
-+ } else {
-+ logger.trace("Path does not exist or is not accessible: {}", certPath);
-+ }
-+ }
-+ } catch (Exception e) {
-+ logger.error("Error loading development certificates", e);
-+ }
-+ return certificates;
-+ }
-+}
diff --git a/spring-boot-admin/patches/steeltoe-admin-config.patch b/spring-boot-admin/patches/steeltoe-admin-config.patch
deleted file mode 100644
index b4f74cd..0000000
--- a/spring-boot-admin/patches/steeltoe-admin-config.patch
+++ /dev/null
@@ -1,129 +0,0 @@
---- /dev/null
-+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SteeltoeAdminConfiguration.java 2026-01-27 00:00:00.000000000 +0000
-@@ -0,0 +1,126 @@
-+package io.steeltoe.docker.springbootadmin;
-+
-+import com.fasterxml.jackson.core.JsonParser;
-+import com.fasterxml.jackson.databind.DeserializationContext;
-+import com.fasterxml.jackson.databind.JsonDeserializer;
-+import com.fasterxml.jackson.databind.JsonNode;
-+import com.fasterxml.jackson.databind.ObjectMapper;
-+import com.fasterxml.jackson.databind.module.SimpleModule;
-+import de.codecentric.boot.admin.server.web.client.InstanceWebClientCustomizer;
-+import org.slf4j.Logger;
-+import org.slf4j.LoggerFactory;
-+import org.springframework.beans.factory.ObjectProvider;
-+import org.springframework.context.annotation.Bean;
-+import org.springframework.context.annotation.Configuration;
-+import org.springframework.core.annotation.Order;
-+import org.springframework.http.client.reactive.ClientHttpConnector;
-+import org.springframework.http.codec.json.Jackson2JsonDecoder;
-+import org.springframework.http.codec.json.Jackson2JsonEncoder;
-+import org.springframework.web.reactive.function.client.ExchangeStrategies;
-+import org.springframework.web.reactive.function.client.WebClient;
-+
-+import java.io.IOException;
-+import java.util.HashMap;
-+import java.util.Map;
-+
-+/**
-+ * Configuration to make Spring Boot Admin compatible with Steeltoe actuator responses.
-+ *
-+ * Steeltoe adds a "type":"Steeltoe" property to its actuator index response, which causes
-+ * deserialization failures in AOT-compiled Spring Boot Admin. This configuration provides
-+ * a custom WebClient with a manual deserializer that only extracts the _links field.
-+ *
-+ * @see GraalVM Reflection Metadata
-+ */
-+@Configuration(proxyBeanMethods = false)
-+public class SteeltoeAdminConfiguration {
-+
-+ private static final Logger log = LoggerFactory.getLogger(SteeltoeAdminConfiguration.class);
-+
-+ @Bean
-+ @Order(-100)
-+ public InstanceWebClientCustomizer steeltoeInstanceWebClientCustomizer(
-+ ObjectMapper objectMapper,
-+ ObjectProvider clientHttpConnectorProvider) {
-+
-+ log.info("Configuring Spring Boot Admin WebClient for Steeltoe compatibility");
-+
-+ return (builder) -> {
-+ try {
-+ Class> responseClass = Class.forName(
-+ "de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy$Response");
-+ Class> endpointRefClass = Class.forName(
-+ "de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy$Response$EndpointRef");
-+
-+ // Copy the base ObjectMapper to preserve other configuration (date formats, etc.)
-+ // while adding our custom deserializer for the actuator index response
-+ ObjectMapper mapper = objectMapper.copy();
-+ SimpleModule module = new SimpleModule("SteeltoeCompatibility");
-+ @SuppressWarnings({"unchecked", "rawtypes"})
-+ JsonDeserializer deserializer = new ActuatorIndexResponseDeserializer(responseClass, endpointRefClass);
-+ module.addDeserializer((Class) responseClass, deserializer);
-+ mapper.registerModule(module);
-+
-+ ExchangeStrategies strategies = ExchangeStrategies.builder()
-+ .codecs(configurer -> {
-+ configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
-+ configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));
-+ })
-+ .build();
-+
-+ WebClient.Builder webClientBuilder = WebClient.builder().exchangeStrategies(strategies);
-+ clientHttpConnectorProvider.ifAvailable(connector -> {
-+ webClientBuilder.clientConnector(connector);
-+ log.info("Using custom ClientHttpConnector for SSL trust");
-+ });
-+
-+ builder.webClient(webClientBuilder);
-+ log.info("Steeltoe-compatible WebClient configuration complete");
-+
-+ } catch (ClassNotFoundException e) {
-+ log.error("Failed to load SBA Response class: {}", e.getMessage());
-+ }
-+ };
-+ }
-+
-+ @SuppressWarnings("rawtypes")
-+ public static class ActuatorIndexResponseDeserializer extends JsonDeserializer {
-+ private static final Logger log = LoggerFactory.getLogger(ActuatorIndexResponseDeserializer.class);
-+ private final Class> responseClass;
-+ private final Class> endpointRefClass;
-+
-+ public ActuatorIndexResponseDeserializer(Class> responseClass, Class> endpointRefClass) {
-+ this.responseClass = responseClass;
-+ this.endpointRefClass = endpointRefClass;
-+ }
-+
-+ @Override
-+ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
-+ JsonNode node = p.getCodec().readTree(p);
-+ try {
-+ Object response = responseClass.getDeclaredConstructor().newInstance();
-+ Map