diff --git a/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java b/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java index 842b49cba2..8d5806bf09 100644 --- a/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java +++ b/cloudplatform/connectivity-ztis/src/main/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityService.java @@ -10,6 +10,7 @@ import java.time.Duration; import java.time.Instant; import java.util.Date; +import java.util.List; import java.util.Objects; import javax.annotation.Nonnull; @@ -23,6 +24,7 @@ import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.exception.X509SvidException; +import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.TrustDomain; import io.spiffe.svid.x509svid.X509Svid; import io.spiffe.workloadapi.DefaultX509Source; @@ -109,16 +111,57 @@ X509Source initX509Source() final String socketPath = Option.of(System.getenv(SOCKET_ENVIRONMENT_VARIABLE)).getOrElse(DEFAULT_SOCKET_PATH); log.info("Using socket path {} for ZTIS agent.", socketPath); - final X509SourceOptions x509SourceOptions = - X509SourceOptions.builder().spiffeSocketPath(socketPath).initTimeout(DEFAULT_SOCKET_TIMEOUT).build(); + // The SPIRE agent may return multiple SVIDs when overlapping registration selectors exist + // (e.g. a second service key on the same ZTIS instance, or a co-located workload). We must pick + // the one whose SPIFFE ID matches our binding — see pickSvid. + final SpiffeId expectedSpiffeId; try { - return DefaultX509Source.newSource(x509SourceOptions); + expectedSpiffeId = SpiffeId.parse(mapView.getMapView("workload").getString("spiffeID")); + } + catch( Exception e ) { + throw new CloudPlatformException("Invalid SPIFFE ID in Zero Trust Identity Service binding.", e); + } + try { + return DefaultX509Source.newSource(buildX509SourceOptions(socketPath, expectedSpiffeId)); } catch( final Exception e ) { throw new CloudPlatformException("Failed to load the certificate from the unix socket: " + socketPath, e); } } + X509SourceOptions + buildX509SourceOptions( @Nonnull final String socketPath, @Nonnull final SpiffeId expectedSpiffeId ) + { + return X509SourceOptions + .builder() + .spiffeSocketPath(socketPath) + .initTimeout(DEFAULT_SOCKET_TIMEOUT) + .svidPicker(svids -> pickSvid(svids, expectedSpiffeId)) + .build(); + } + + /** + * Selects the {@link X509Svid} whose SPIFFE ID equals {@code expectedSpiffeId} from the given list. Used as the + * {@code svidPicker} for {@link DefaultX509Source} to avoid non-deterministic selection when the SPIRE agent + * returns multiple SVIDs (e.g. after a second service key is created for the same ZTIS instance). + */ + @Nonnull + static X509Svid pickSvid( @Nonnull final List svids, @Nonnull final SpiffeId expectedSpiffeId ) + { + log.debug("SPIRE agent returned {} SVID(s); selecting the one matching '{}'.", svids.size(), expectedSpiffeId); + return svids + .stream() + .filter(svid -> expectedSpiffeId.equals(svid.getSpiffeId())) + .findFirst() + .orElseThrow( + () -> new CloudPlatformException( + String + .format( + "No SVID matching SPIFFE ID '%s' among %d returned by the SPIRE agent.", + expectedSpiffeId, + svids.size()))); + } + @Nonnull X509Svid getX509Svid() { diff --git a/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java b/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java index b9e064853d..36a4c7365a 100644 --- a/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java +++ b/cloudplatform/connectivity-ztis/src/test/java/com/sap/cloud/sdk/cloudplatform/connectivity/ZeroTrustIdentityServiceTest.java @@ -15,7 +15,9 @@ import java.security.cert.X509Certificate; import java.time.Instant; import java.util.Date; +import java.util.List; import java.util.Map; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +28,7 @@ import com.sap.cloud.environment.servicebinding.api.exception.ServiceBindingAccessException; import com.sap.cloud.sdk.cloudplatform.exception.CloudPlatformException; +import io.spiffe.spiffeid.SpiffeId; import io.spiffe.svid.x509svid.X509Svid; class ZeroTrustIdentityServiceTest @@ -152,6 +155,25 @@ void testCertOnFileSystem() .isInstanceOf(IllegalStateException.class); } + @Test + void testSvidPickerIsWiredAndSelectsMatchingSvid() + { + final SpiffeId expected = SpiffeId.parse("spiffe://example.org/my-app"); + final X509Svid own = mockSvidWithSpiffeId(expected); + final X509Svid other = mockSvidWithSpiffeId(SpiffeId.parse("spiffe://example.org/other-app")); + + final Function, X509Svid> picker = + sut.buildX509SourceOptions("unix:///tmp/test.sock", expected).getSvidPicker(); + + assertThat(picker).isNotNull(); + assertThat(picker.apply(List.of(own))).isSameAs(own); + assertThat(picker.apply(List.of(other, own))).isSameAs(own); + assertThatThrownBy(() -> picker.apply(List.of(other))) + .isInstanceOf(CloudPlatformException.class) + .hasMessageContaining("spiffe://example.org/my-app"); + assertThatThrownBy(() -> picker.apply(List.of())).isInstanceOf(CloudPlatformException.class); + } + private void mockSvid( Instant notAfter ) { final X509Svid svid = mock(X509Svid.class); @@ -162,6 +184,13 @@ private void mockSvid( Instant notAfter ) svidMock = svid; } + private static X509Svid mockSvidWithSpiffeId( final SpiffeId spiffeId ) + { + final X509Svid svid = mock(X509Svid.class); + doReturn(spiffeId).when(svid).getSpiffeId(); + return svid; + } + private static ServiceBinding mockBinding() { return new DefaultServiceBindingBuilder() diff --git a/release_notes.md b/release_notes.md index 8fb3414093..78264dbc2b 100644 --- a/release_notes.md +++ b/release_notes.md @@ -20,4 +20,4 @@ ### 🐛 Fixed Issues -- +- [Connectivity] Fixed a bug in `ZeroTrustIdentityService` where a non-deterministic SVID could be selected when the SPIRE agent returns multiple SVIDs to the workload (e.g. after creating a second service key for the same ZTIS instance).