+ * // 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/11] 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/11] 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/11] 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: