This is an automated email from the ASF dual-hosted git repository.

dhuo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 793a1188f SigV4 Auth Support for Catalog Federation - Part 3: Service 
Identity Info Injection (#2523)
793a1188f is described below

commit 793a1188f53658d17e032f2548a38088a8045650
Author: Rulin Xing <[email protected]>
AuthorDate: Fri Oct 3 19:17:41 2025 -0700

    SigV4 Auth Support for Catalog Federation - Part 3: Service Identity Info 
Injection (#2523)
    
    This PR introduces service identity management for SigV4 Auth Support for 
Catalog Federation. Unlike user-supplied parameters, the service identity 
represents the identity of the Polaris service itself and should be managed by 
Polaris.
    
    * Service Identity Injection
    
    * Return injected service identity info in response
    
    * Use AwsCredentialsProvider to retrieve the credentials
    
    * Move some logic to ServiceIdentityConfiguration
    
    * Rename ServiceIdentityRegistry to ServiceIdentityProvider
    
    * Rename ResolvedServiceIdentity to ServiceIdentityCredential
    
    * Simplify the logic and add more tests
    
    * Use SecretReference and fix some small issues
    
    * Disable Catalog Federation
---
 .../polaris/core/config/FeatureConfiguration.java  |   3 +-
 .../core/connection/ConnectionConfigInfoDpo.java   |   6 +-
 .../hadoop/HadoopConnectionConfigInfoDpo.java      |   8 +-
 .../hive/HiveConnectionConfigInfoDpo.java          |  11 +-
 .../IcebergRestConnectionConfigInfoDpo.java        |   8 +-
 .../apache/polaris/core/entity/CatalogEntity.java  |  25 +-
 .../AwsIamServiceIdentityCredential.java           | 100 ++++++
 .../credential/ServiceIdentityCredential.java      |  91 +++++
 .../identity/dpo/AwsIamServiceIdentityInfoDpo.java |  29 +-
 .../core/identity/dpo/ServiceIdentityInfoDpo.java  |  30 +-
 .../identity/provider/ServiceIdentityProvider.java | 103 ++++++
 .../connection/ConnectionConfigInfoDpoTest.java    |  37 ++-
 .../AwsIamServiceIdentityCredentialTest.java       | 164 +++++++++
 .../polaris/service/admin/PolarisAdminService.java |  30 +-
 .../polaris/service/admin/PolarisServiceImpl.java  |  18 +-
 .../polaris/service/config/ServiceProducers.java   |   4 +-
 .../AwsIamServiceIdentityConfiguration.java        | 141 ++++++++
 .../RealmServiceIdentityConfiguration.java         |  51 +++
 .../ResolvableServiceIdentityConfiguration.java    |  75 +++++
 .../identity/ServiceIdentityConfiguration.java     | 114 +++++++
 .../provider/DefaultServiceIdentityProvider.java   | 155 +++++++++
 .../service/admin/ManagementServiceTest.java       |   2 +
 .../admin/PolarisAdminServiceAuthzTest.java        |  13 +-
 .../service/admin/PolarisAdminServiceTest.java     |   3 +
 .../service/admin/PolarisAuthzTestBase.java        |   5 +-
 .../service/admin/PolarisServiceImplTest.java      |  10 +-
 .../AbstractPolarisGenericTableCatalogTest.java    |   5 +-
 .../iceberg/AbstractIcebergCatalogTest.java        |  15 +-
 .../iceberg/AbstractIcebergCatalogViewTest.java    |   5 +-
 .../iceberg/IcebergCatalogHandlerAuthzTest.java    |   2 +-
 .../catalog/policy/AbstractPolicyCatalogTest.java  |   5 +-
 .../polaris/service/entity/CatalogEntityTest.java  |  92 ++++-
 .../DefaultServiceIdentityProviderTest.java        | 369 +++++++++++++++++++++
 .../org/apache/polaris/service/TestServices.java   |   7 +-
 34 files changed, 1663 insertions(+), 73 deletions(-)

diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
index 6eda66a23..5bd9f7257 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
@@ -303,7 +303,8 @@ public class FeatureConfiguration<T> extends 
PolarisConfiguration<T> {
               .defaultValue(
                   List.of(
                       
AuthenticationParameters.AuthenticationTypeEnum.OAUTH.name(),
-                      
AuthenticationParameters.AuthenticationTypeEnum.BEARER.name()))
+                      
AuthenticationParameters.AuthenticationTypeEnum.BEARER.name(),
+                      
AuthenticationParameters.AuthenticationTypeEnum.SIGV4.name()))
               .buildFeatureConfiguration();
 
   public static final FeatureConfiguration<Integer> ICEBERG_COMMIT_MAX_RETRIES 
=
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java
index bb95dc442..555a4008d 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java
@@ -41,6 +41,7 @@ import 
org.apache.polaris.core.connection.hive.HiveConnectionConfigInfoDpo;
 import 
org.apache.polaris.core.connection.iceberg.IcebergCatalogPropertiesProvider;
 import 
org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo;
 import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.secrets.SecretReference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -225,9 +226,10 @@ public abstract class ConnectionConfigInfoDpo implements 
IcebergCatalogPropertie
       @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo);
 
   /**
-   * Produces the correponding API-model ConnectionConfigInfo for this 
persistence object; many
+   * Produces the corresponding API-model ConnectionConfigInfo for this 
persistence object; many
    * fields are one-to-one direct mappings, but some fields, such as 
secretReferences, might only be
    * applicable/present in the persistence object, but not the API model 
object.
    */
-  public abstract ConnectionConfigInfo asConnectionConfigInfoModel();
+  public abstract ConnectionConfigInfo asConnectionConfigInfoModel(
+      ServiceIdentityProvider serviceIdentityProvider);
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
index 05104dd20..66fe8bb70 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java
@@ -32,6 +32,7 @@ import 
org.apache.polaris.core.connection.AuthenticationParametersDpo;
 import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
 import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
@@ -88,7 +89,8 @@ public class HadoopConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo {
   }
 
   @Override
-  public ConnectionConfigInfo asConnectionConfigInfoModel() {
+  public ConnectionConfigInfo asConnectionConfigInfoModel(
+      ServiceIdentityProvider serviceIdentityProvider) {
     return HadoopConnectionConfigInfo.builder()
         .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HADOOP)
         .setUri(getUri())
@@ -97,7 +99,9 @@ public class HadoopConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo {
             getAuthenticationParameters().asAuthenticationParametersModel())
         .setServiceIdentity(
             Optional.ofNullable(getServiceIdentity())
-                .map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel)
+                .map(
+                    serviceIdentityInfoDpo ->
+                        
serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider))
                 .orElse(null))
         .build();
   }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
index 1f9027f2b..22b067a6c 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java
@@ -24,6 +24,7 @@ import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 import org.apache.iceberg.CatalogProperties;
 import org.apache.polaris.core.admin.model.ConnectionConfigInfo;
 import org.apache.polaris.core.admin.model.HiveConnectionConfigInfo;
@@ -31,6 +32,7 @@ import 
org.apache.polaris.core.connection.AuthenticationParametersDpo;
 import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
 import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
@@ -88,13 +90,20 @@ public class HiveConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo {
   }
 
   @Override
-  public ConnectionConfigInfo asConnectionConfigInfoModel() {
+  public ConnectionConfigInfo asConnectionConfigInfoModel(
+      ServiceIdentityProvider serviceIdentityProvider) {
     return HiveConnectionConfigInfo.builder()
         .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HIVE)
         .setUri(getUri())
         .setWarehouse(getWarehouse())
         .setAuthenticationParameters(
             getAuthenticationParameters().asAuthenticationParametersModel())
+        .setServiceIdentity(
+            Optional.ofNullable(getServiceIdentity())
+                .map(
+                    serviceIdentityInfoDpo ->
+                        
serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider))
+                .orElse(null))
         .build();
   }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
index 0a6e870b7..ee01691b9 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java
@@ -32,6 +32,7 @@ import 
org.apache.polaris.core.connection.AuthenticationParametersDpo;
 import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
 import org.apache.polaris.core.connection.ConnectionType;
 import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.secrets.UserSecretsManager;
 
 /**
@@ -80,7 +81,8 @@ public class IcebergRestConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo
   }
 
   @Override
-  public ConnectionConfigInfo asConnectionConfigInfoModel() {
+  public ConnectionConfigInfo asConnectionConfigInfoModel(
+      ServiceIdentityProvider serviceIdentityProvider) {
     return IcebergRestConnectionConfigInfo.builder()
         
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST)
         .setUri(getUri())
@@ -89,7 +91,9 @@ public class IcebergRestConnectionConfigInfoDpo extends 
ConnectionConfigInfoDpo
             getAuthenticationParameters().asAuthenticationParametersModel())
         .setServiceIdentity(
             Optional.ofNullable(getServiceIdentity())
-                .map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel)
+                .map(
+                    serviceIdentityInfoDpo ->
+                        
serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider))
                 .orElse(null))
         .build();
   }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java 
b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
index a31299504..055ccd895 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
@@ -43,6 +43,8 @@ import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.config.BehaviorChangeConfiguration;
 import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.connection.ConnectionConfigInfoDpo;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.secrets.SecretReference;
 import org.apache.polaris.core.storage.FileStorageConfigurationInfo;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
@@ -108,6 +110,10 @@ public class CatalogEntity extends PolarisEntity 
implements LocationBasedEntity
   }
 
   public Catalog asCatalog() {
+    return this.asCatalog(null);
+  }
+
+  public Catalog asCatalog(ServiceIdentityProvider serviceIdentityProvider) {
     Map<String, String> internalProperties = getInternalPropertiesAsMap();
     Catalog.TypeEnum catalogType =
         Optional.ofNullable(internalProperties.get(CATALOG_TYPE_PROPERTY))
@@ -118,6 +124,12 @@ public class CatalogEntity extends PolarisEntity 
implements LocationBasedEntity
         CatalogProperties.builder(propertiesMap.get(DEFAULT_BASE_LOCATION_KEY))
             .putAll(propertiesMap)
             .build();
+
+    // Right now, only external catalog may use ServiceIdentityProvider to 
resolve identity
+    Preconditions.checkState(
+        catalogType != Catalog.TypeEnum.EXTERNAL || serviceIdentityProvider != 
null,
+        "%s catalog needs ServiceIdentityProvider to resolve service 
identities",
+        Catalog.TypeEnum.EXTERNAL);
     return catalogType == Catalog.TypeEnum.EXTERNAL
         ? ExternalCatalog.builder()
             .setType(Catalog.TypeEnum.EXTERNAL)
@@ -127,7 +139,7 @@ public class CatalogEntity extends PolarisEntity implements 
LocationBasedEntity
             .setLastUpdateTimestamp(getLastUpdateTimestamp())
             .setEntityVersion(getEntityVersion())
             .setStorageConfigInfo(getStorageInfo(internalProperties))
-            .setConnectionConfigInfo(getConnectionInfo(internalProperties))
+            .setConnectionConfigInfo(getConnectionInfo(internalProperties, 
serviceIdentityProvider))
             .build()
         : PolarisCatalog.builder()
             .setType(Catalog.TypeEnum.INTERNAL)
@@ -187,11 +199,12 @@ public class CatalogEntity extends PolarisEntity 
implements LocationBasedEntity
     return null;
   }
 
-  private ConnectionConfigInfo getConnectionInfo(Map<String, String> 
internalProperties) {
+  private ConnectionConfigInfo getConnectionInfo(
+      Map<String, String> internalProperties, ServiceIdentityProvider 
serviceIdentityProvider) {
     if (internalProperties.containsKey(
         PolarisEntityConstants.getConnectionConfigInfoPropertyName())) {
       ConnectionConfigInfoDpo configInfo = getConnectionConfigInfoDpo();
-      return configInfo.asConnectionConfigInfoModel();
+      return configInfo.asConnectionConfigInfoModel(serviceIdentityProvider);
     }
     return null;
   }
@@ -352,11 +365,13 @@ public class CatalogEntity extends PolarisEntity 
implements LocationBasedEntity
 
     public Builder setConnectionConfigInfoDpoWithSecrets(
         ConnectionConfigInfo connectionConfigurationModel,
-        Map<String, SecretReference> secretReferences) {
+        Map<String, SecretReference> secretReferences,
+        ServiceIdentityInfoDpo serviceIdentityInfoDpo) {
       if (connectionConfigurationModel != null) {
         ConnectionConfigInfoDpo config =
             ConnectionConfigInfoDpo.fromConnectionConfigInfoModelWithSecrets(
-                connectionConfigurationModel, secretReferences);
+                    connectionConfigurationModel, secretReferences)
+                .withServiceIdentity(serviceIdentityInfoDpo);
         internalProperties.put(
             PolarisEntityConstants.getConnectionConfigInfoPropertyName(), 
config.serialize());
       }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java
new file mode 100644
index 000000000..ae85cbbe1
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.identity.credential;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.identity.ServiceIdentityType;
+import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.secrets.SecretReference;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+
+/**
+ * Represents an AWS IAM service identity credential used by Polaris to 
authenticate to AWS
+ * services.
+ *
+ * <p>This credential encapsulates:
+ *
+ * <ul>
+ *   <li>The IAM ARN (role or user) representing the Polaris service identity
+ *   <li>An {@link AwsCredentialsProvider} that supplies AWS credentials 
(access key, secret key,
+ *       and optional session token)
+ * </ul>
+ *
+ * <p>Polaris uses this identity to assume customer-provided IAM roles when 
accessing remote
+ * catalogs with SigV4 authentication. The {@link AwsCredentialsProvider} can 
be configured to use
+ * either:
+ *
+ * <ul>
+ *   <li>Static credentials (for testing or single-tenant deployments)
+ *   <li>DefaultCredentialsProvider (which chains through various AWS 
credential sources)
+ *   <li>Custom credential providers (for vendor-specific secret management)
+ * </ul>
+ */
+public class AwsIamServiceIdentityCredential extends ServiceIdentityCredential 
{
+
+  /** IAM role or user ARN representing the Polaris service identity. */
+  private final String iamArn;
+
+  /** AWS credentials provider for accessing AWS services. */
+  private final AwsCredentialsProvider awsCredentialsProvider;
+
+  public AwsIamServiceIdentityCredential(@Nullable String iamArn) {
+    this(null, iamArn, DefaultCredentialsProvider.builder().build());
+  }
+
+  public AwsIamServiceIdentityCredential(
+      @Nullable String iamArn, @Nonnull AwsCredentialsProvider 
awsCredentialsProvider) {
+    this(null, iamArn, awsCredentialsProvider);
+  }
+
+  public AwsIamServiceIdentityCredential(
+      @Nullable SecretReference secretReference,
+      @Nullable String iamArn,
+      @Nonnull AwsCredentialsProvider awsCredentialsProvider) {
+    super(ServiceIdentityType.AWS_IAM, secretReference);
+    this.iamArn = iamArn;
+    this.awsCredentialsProvider = awsCredentialsProvider;
+  }
+
+  public @Nullable String getIamArn() {
+    return iamArn;
+  }
+
+  public @Nonnull AwsCredentialsProvider getAwsCredentialsProvider() {
+    return awsCredentialsProvider;
+  }
+
+  @Override
+  public @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo() {
+    return new AwsIamServiceIdentityInfoDpo(getIdentityInfoReference());
+  }
+
+  @Override
+  public @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel() {
+    return AwsIamServiceIdentityInfo.builder()
+        .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM)
+        .setIamArn(getIamArn())
+        .build();
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java
new file mode 100644
index 000000000..635c6fcd6
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.identity.credential;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.identity.ServiceIdentityType;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.secrets.SecretReference;
+import software.amazon.awssdk.annotations.NotNull;
+
+/**
+ * Represents a service identity credential used by Polaris to authenticate to 
external systems.
+ *
+ * <p>This class encapsulates both the service identity metadata (e.g., AWS 
IAM ARN) and the
+ * associated credentials (e.g., AWS access keys) needed to authenticate as 
the Polaris service when
+ * accessing external catalog services.
+ *
+ * <p>The credential contains:
+ *
+ * <ul>
+ *   <li>Identity type (e.g., AWS_IAM)
+ *   <li>A {@link SecretReference} that serves as a unique identifier for this 
service identity
+ *       instance (used for lookups and persistence)
+ *   <li>The actual authentication credentials (implementation-specific, e.g.,
+ *       AwsCredentialsProvider)
+ * </ul>
+ */
+public abstract class ServiceIdentityCredential {
+  private final ServiceIdentityType identityType;
+  private SecretReference identityInfoReference;
+
+  public ServiceIdentityCredential(@Nonnull ServiceIdentityType identityType) {
+    this(identityType, null);
+  }
+
+  public ServiceIdentityCredential(
+      @Nonnull ServiceIdentityType identityType, @Nullable SecretReference 
identityInfoReference) {
+    this.identityType = identityType;
+    this.identityInfoReference = identityInfoReference;
+  }
+
+  public @NotNull ServiceIdentityType getIdentityType() {
+    return identityType;
+  }
+
+  public @Nonnull SecretReference getIdentityInfoReference() {
+    return identityInfoReference;
+  }
+
+  public void setIdentityInfoReference(@NotNull SecretReference 
identityInfoReference) {
+    this.identityInfoReference = identityInfoReference;
+  }
+
+  /**
+   * Converts this service identity credential into its corresponding 
persisted form (DPO).
+   *
+   * <p>The DPO contains only a reference to the credential, not the 
credential itself, as the
+   * actual secrets are managed externally.
+   *
+   * @return The persistence object representation
+   */
+  public abstract @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo();
+
+  /**
+   * Converts this service identity credential into its API model 
representation.
+   *
+   * <p>The model contains identity information (e.g., IAM ARN) but excludes 
sensitive credentials
+   * such as access keys or session tokens.
+   *
+   * @return The API model representation for client responses
+   */
+  public abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel();
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java
index cddad3a42..1e8d77c60 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java
@@ -21,26 +21,26 @@ package org.apache.polaris.core.identity.dpo;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.MoreObjects;
-import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
 import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
-import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
 import org.apache.polaris.core.identity.ServiceIdentityType;
+import 
org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential;
 import org.apache.polaris.core.secrets.SecretReference;
 
 /**
  * Persistence-layer representation of an AWS IAM service identity used by 
Polaris.
  *
- * <p>This class models an AWS IAM identity (either a user or role) and 
extends {@link
- * ServiceIdentityInfoDpo}. It is typically used internally to store a 
reference to the actual
- * credential (e.g., via {@link SecretReference}).
+ * <p>This class stores only the identity type and a {@link SecretReference} 
that serves as a unique
+ * identifier for this service identity instance. The reference is used to 
look up the identity's
+ * configuration at runtime. The actual credentials (AWS access keys) and 
metadata (IAM ARN) are not
+ * persisted in this object.
  *
- * <p>During the runtime, it will be resolved to an actual 
ResolvedAwsIamServiceIdentityInfo object
- * which contains the actual service identity info (e.g., the IAM user arn) 
and the corresponding
- * credential.
+ * <p>At runtime, a ServiceIdentityProvider uses the reference to look up the 
configuration and
+ * retrieve the full {@link AwsIamServiceIdentityCredential} which contains 
both the identity
+ * metadata (e.g., IAM ARN) and the actual AWS credentials needed for 
authentication.
  *
- * <p>Instances of this class are convertible to the public API model {@link
- * AwsIamServiceIdentityInfo}.
+ * <p>Instances of this class can be converted to the public API model {@link
+ * AwsIamServiceIdentityInfo} via a ServiceIdentityProvider.
  */
 public class AwsIamServiceIdentityInfoDpo extends ServiceIdentityInfoDpo {
 
@@ -51,15 +51,6 @@ public class AwsIamServiceIdentityInfoDpo extends 
ServiceIdentityInfoDpo {
     super(ServiceIdentityType.AWS_IAM.getCode(), identityInfoReference);
   }
 
-  @Override
-  public @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel() {
-    return AwsIamServiceIdentityInfo.builder()
-        .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM)
-        // TODO: inject service identity info
-        .setIamArn("")
-        .build();
-  }
-
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java
index 22f25dd70..de30bed30 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java
@@ -23,18 +23,22 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.google.common.base.MoreObjects;
-import jakarta.annotation.Nonnull;
+import com.google.common.base.Preconditions;
 import jakarta.annotation.Nullable;
 import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
 import org.apache.polaris.core.identity.ServiceIdentityType;
+import org.apache.polaris.core.identity.credential.ServiceIdentityCredential;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.secrets.SecretReference;
 
 /**
  * The internal persistence-object counterpart to ServiceIdentityInfo defined 
in the API model.
  * Important: JsonSubTypes must be kept in sync with {@link 
ServiceIdentityType}.
  *
- * <p>During the runtime, it will be resolved to an actual 
ResolvedServiceIdentityInfo object which
- * contains the actual service identity info and the corresponding credential.
+ * <p>This DPO stores only the identity type and a {@link SecretReference} 
that serves as a unique
+ * identifier for the service identity instance. The reference is used at 
runtime by a {@link
+ * ServiceIdentityProvider} to look up the configuration and retrieve the full 
{@link
+ * ServiceIdentityCredential} with credentials and metadata.
  */
 @JsonTypeInfo(
     use = JsonTypeInfo.Id.NAME,
@@ -72,10 +76,24 @@ public abstract class ServiceIdentityInfoDpo {
   }
 
   /**
-   * Converts this persistence object to the corresponding API model. During 
the conversion, some
-   * fields will be dropped, e.g. the reference to the service identity's 
credential
+   * Converts this persistence object to the corresponding API model.
+   *
+   * <p>The conversion uses the provided {@link ServiceIdentityProvider} to 
retrieve the user-facing
+   * identity information (e.g., AWS IAM ARN) without exposing sensitive 
credentials. The credential
+   * reference stored in this DPO is not included in the API model.
+   *
+   * @param serviceIdentityProvider the service identity provider used to 
retrieve display
+   *     information
+   * @return the API model representation, or null if the provider is null or 
cannot resolve the
+   *     identity
    */
-  public abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel();
+  public @Nullable ServiceIdentityInfo asServiceIdentityInfoModel(
+      ServiceIdentityProvider serviceIdentityProvider) {
+    Preconditions.checkNotNull(
+        serviceIdentityProvider,
+        "Need ServiceIdentityProvider to inject service identity info, should 
not be null");
+    return serviceIdentityProvider.getServiceIdentityInfo(this).orElse(null);
+  }
 
   @Override
   public String toString() {
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java
new file mode 100644
index 000000000..c652ddbe1
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.core.identity.provider;
+
+import jakarta.annotation.Nonnull;
+import java.util.Optional;
+import org.apache.polaris.core.admin.model.ConnectionConfigInfo;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.identity.credential.ServiceIdentityCredential;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+
+/**
+ * A provider interface for managing and resolving service identities in 
Polaris.
+ *
+ * <p>In a multi-tenant Polaris deployment, each catalog or tenant may be 
associated with a distinct
+ * service identity that represents the Polaris service itself when accessing 
external systems
+ * (e.g., cloud services like AWS or GCP). This provider offers a central 
mechanism to allocate
+ * service identities to catalog entities and resolve them at runtime.
+ *
+ * <p>The provider helps abstract the configuration and retrieval of 
service-managed credentials
+ * from the logic that uses them. It ensures a consistent and secure way to 
handle identity
+ * resolution across different deployment models, including SaaS and 
self-managed environments.
+ *
+ * <p>Key responsibilities:
+ *
+ * <ul>
+ *   <li><b>Allocation</b>: Assign a service identity to a catalog entity 
during creation. The
+ *       actual credentials are not stored in the entity; only a reference is 
persisted.
+ *   <li><b>Resolution (with credentials)</b>: Retrieve the full identity 
including credentials for
+ *       authentication purposes (e.g., signing requests with SigV4).
+ *   <li><b>Resolution (without credentials)</b>: Retrieve the identity 
information for display in
+ *       API responses without exposing sensitive credentials.
+ * </ul>
+ */
+public interface ServiceIdentityProvider {
+  /**
+   * Allocates a {@link ServiceIdentityInfoDpo} for the given connection 
configuration. This method
+   * is typically invoked during catalog entity creation to associate a 
service identity with the
+   * catalog for accessing external services.
+   *
+   * <p>The allocation strategy is implementation-specific:
+   *
+   * <ul>
+   *   <li>A vendor may choose to use the same service identity across all 
entities in an account.
+   *   <li>Alternatively, different service identities can be assigned per 
catalog entity.
+   *   <li>The associated DPO stores only a reference to the service identity, 
not the credentials
+   *       themselves.
+   * </ul>
+   *
+   * @param connectionConfig The connection configuration for which a service 
identity should be
+   *     allocated.
+   * @return An {@link Optional} containing the allocated {@link 
ServiceIdentityInfoDpo}, or empty
+   *     if no service identity is available or applicable for this connection.
+   */
+  Optional<ServiceIdentityInfoDpo> allocateServiceIdentity(
+      @Nonnull ConnectionConfigInfo connectionConfig);
+
+  /**
+   * Retrieves the user-facing {@link ServiceIdentityInfo} model for the given 
service identity
+   * reference, without exposing sensitive credentials.
+   *
+   * <p>This method is used when generating API responses (e.g., {@code 
getCatalog}) to return
+   * identity details such as the AWS IAM user ARN, but not the actual 
credentials.
+   *
+   * @param serviceIdentityInfo The service identity metadata to resolve.
+   * @return An {@link Optional} containing the {@link ServiceIdentityInfo} 
model for API responses,
+   *     or empty if the identity cannot be resolved.
+   */
+  Optional<ServiceIdentityInfo> getServiceIdentityInfo(
+      @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo);
+
+  /**
+   * Retrieves the service identity credential by resolving the actual 
credential or secret
+   * referenced by the given service identity info, typically from a secret 
manager or internal
+   * credential store.
+   *
+   * <p>This method is used when Polaris needs to authenticate to external 
systems using the service
+   * identity, such as when signing requests with SigV4 authentication.
+   *
+   * @param serviceIdentityInfo The service identity metadata to resolve.
+   * @return An {@link Optional} containing a {@link 
ServiceIdentityCredential} with credentials, or
+   *     empty if the identity cannot be resolved.
+   */
+  Optional<ServiceIdentityCredential> getServiceIdentityCredential(
+      @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo);
+}
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java
index 727fac0d0..5d8dbc7d4 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java
@@ -22,9 +22,16 @@ import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Optional;
+import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
 import org.apache.polaris.core.admin.model.ConnectionConfigInfo;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import 
org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 public class ConnectionConfigInfoDpoTest {
   private static final ObjectMapper objectMapper = new ObjectMapper();
@@ -33,6 +40,24 @@ public class ConnectionConfigInfoDpoTest {
     objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
   }
 
+  private ServiceIdentityProvider serviceIdentityProvider;
+
+  @BeforeEach
+  void setUp() {
+    serviceIdentityProvider = Mockito.mock(ServiceIdentityProvider.class);
+    Mockito.when(serviceIdentityProvider.getServiceIdentityInfo(Mockito.any()))
+        .thenReturn(
+            Optional.of(
+                AwsIamServiceIdentityInfo.builder()
+                    
.setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM)
+                    .setIamArn("arn:aws:iam::123456789012:user/test-user")
+                    .build()));
+    
Mockito.when(serviceIdentityProvider.getServiceIdentityCredential(Mockito.any()))
+        .thenReturn(
+            Optional.of(
+                new 
AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user")));
+  }
+
   @Test
   void testOAuthClientCredentialsParameters() throws JsonProcessingException {
     // Test deserialization and reserialization of the persistence JSON.
@@ -64,7 +89,7 @@ public class ConnectionConfigInfoDpoTest {
 
     // Test conversion into API model JSON.
     ConnectionConfigInfo connectionConfigInfoApiModel =
-        connectionConfigInfoDpo.asConnectionConfigInfoModel();
+        
connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider);
     String expectedApiModelJson =
         ""
             + "{"
@@ -111,7 +136,7 @@ public class ConnectionConfigInfoDpoTest {
 
     // Test conversion into API model JSON.
     ConnectionConfigInfo connectionConfigInfoApiModel =
-        connectionConfigInfoDpo.asConnectionConfigInfoModel();
+        
connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider);
     String expectedApiModelJson =
         ""
             + "{"
@@ -148,7 +173,7 @@ public class ConnectionConfigInfoDpoTest {
 
     // Test conversion into API model JSON.
     ConnectionConfigInfo connectionConfigInfoApiModel =
-        connectionConfigInfoDpo.asConnectionConfigInfoModel();
+        
connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider);
     String expectedApiModelJson =
         ""
             + "{"
@@ -184,7 +209,7 @@ public class ConnectionConfigInfoDpoTest {
             + "  \"serviceIdentity\": {"
             + "    \"identityTypeCode\": 1,"
             + "    \"identityInfoReference\": {"
-            + "      \"urn\": 
\"urn:polaris-secret:default-identity-registry:my-realm:AWS_IAM\","
+            + "      \"urn\": 
\"urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM\","
             + "      \"referencePayload\": {"
             + "        \"key\": \"value\""
             + "      }"
@@ -200,7 +225,7 @@ public class ConnectionConfigInfoDpoTest {
 
     // Test conversion into API model JSON.
     ConnectionConfigInfo connectionConfigInfoApiModel =
-        connectionConfigInfoDpo.asConnectionConfigInfoModel();
+        
connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider);
     String expectedApiModelJson =
         ""
             + "{"
@@ -217,7 +242,7 @@ public class ConnectionConfigInfoDpoTest {
             + "  },"
             + "  \"serviceIdentity\": {"
             + "    \"identityType\": \"AWS_IAM\","
-            + "    \"iamArn\": \"\""
+            + "    \"iamArn\": \"arn:aws:iam::123456789012:user/test-user\""
             + "  }"
             + "}";
     Assertions.assertEquals(
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java
new file mode 100644
index 000000000..ce69a4f08
--- /dev/null
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.identity.credential;
+
+import java.util.Map;
+import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.identity.ServiceIdentityType;
+import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.secrets.SecretReference;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+
+public class AwsIamServiceIdentityCredentialTest {
+
+  @Test
+  void testConstructorWithIamArnOnly() {
+    AwsIamServiceIdentityCredential credential =
+        new 
AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user");
+
+    Assertions.assertThat(credential.getIamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/test-user");
+    
Assertions.assertThat(credential.getIdentityType()).isEqualTo(ServiceIdentityType.AWS_IAM);
+    Assertions.assertThat(credential.getAwsCredentialsProvider())
+        .isInstanceOf(DefaultCredentialsProvider.class);
+  }
+
+  @Test
+  void testConstructorWithIamArnAndCredentialsProvider() {
+    StaticCredentialsProvider credProvider =
+        
StaticCredentialsProvider.create(AwsBasicCredentials.create("access-key", 
"secret-key"));
+
+    AwsIamServiceIdentityCredential credential =
+        new AwsIamServiceIdentityCredential(
+            "arn:aws:iam::123456789012:role/test-role", credProvider);
+
+    Assertions.assertThat(credential.getIamArn())
+        .isEqualTo("arn:aws:iam::123456789012:role/test-role");
+    
Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider);
+  }
+
+  @Test
+  void testConstructorWithAllParameters() {
+    SecretReference ref = new SecretReference("urn:polaris-secret:test:ref", 
Map.of());
+    StaticCredentialsProvider credProvider =
+        StaticCredentialsProvider.create(
+            AwsSessionCredentials.create("access-key", "secret-key", 
"session-token"));
+
+    AwsIamServiceIdentityCredential credential =
+        new AwsIamServiceIdentityCredential(
+            ref, "arn:aws:iam::123456789012:user/test-user", credProvider);
+
+    Assertions.assertThat(credential.getIamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/test-user");
+    
Assertions.assertThat(credential.getIdentityInfoReference()).isEqualTo(ref);
+    
Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider);
+  }
+
+  @Test
+  void testConversionToDpo() {
+    SecretReference ref = new 
SecretReference("urn:polaris-secret:test:reference", Map.of());
+    AwsIamServiceIdentityCredential credential =
+        new AwsIamServiceIdentityCredential(
+            ref,
+            "arn:aws:iam::123456789012:user/test-user",
+            DefaultCredentialsProvider.builder().build());
+
+    ServiceIdentityInfoDpo dpo = credential.asServiceIdentityInfoDpo();
+
+    Assertions.assertThat(dpo).isNotNull();
+    
Assertions.assertThat(dpo).isInstanceOf(AwsIamServiceIdentityInfoDpo.class);
+    
Assertions.assertThat(dpo.getIdentityType()).isEqualTo(ServiceIdentityType.AWS_IAM);
+    Assertions.assertThat(dpo.getIdentityInfoReference()).isEqualTo(ref);
+  }
+
+  @Test
+  void testConversionToModel() {
+    AwsIamServiceIdentityCredential credential =
+        new 
AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/polaris-service");
+
+    ServiceIdentityInfo model = credential.asServiceIdentityInfoModel();
+
+    Assertions.assertThat(model).isNotNull();
+    Assertions.assertThat(model).isInstanceOf(AwsIamServiceIdentityInfo.class);
+    Assertions.assertThat(model.getIdentityType())
+        .isEqualTo(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM);
+
+    AwsIamServiceIdentityInfo awsModel = (AwsIamServiceIdentityInfo) model;
+    Assertions.assertThat(awsModel.getIamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-service");
+  }
+
+  @Test
+  void testCredentialsProviderWithStaticBasicCredentials() {
+    StaticCredentialsProvider credProvider =
+        
StaticCredentialsProvider.create(AwsBasicCredentials.create("my-key-id", 
"my-secret"));
+
+    AwsIamServiceIdentityCredential credential =
+        new 
AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test", 
credProvider);
+
+    
Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider);
+    AwsBasicCredentials creds = (AwsBasicCredentials) 
credProvider.resolveCredentials();
+    Assertions.assertThat(creds.accessKeyId()).isEqualTo("my-key-id");
+    Assertions.assertThat(creds.secretAccessKey()).isEqualTo("my-secret");
+  }
+
+  @Test
+  void testCredentialsProviderWithSessionCredentials() {
+    StaticCredentialsProvider credProvider =
+        StaticCredentialsProvider.create(
+            AwsSessionCredentials.create("access-key", "secret-key", 
"session-token"));
+
+    AwsIamServiceIdentityCredential credential =
+        new 
AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:role/test", 
credProvider);
+
+    
Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider);
+    AwsSessionCredentials creds = (AwsSessionCredentials) 
credProvider.resolveCredentials();
+    Assertions.assertThat(creds.accessKeyId()).isEqualTo("access-key");
+    Assertions.assertThat(creds.secretAccessKey()).isEqualTo("secret-key");
+    Assertions.assertThat(creds.sessionToken()).isEqualTo("session-token");
+  }
+
+  @Test
+  void testModelDoesNotExposeCredentials() {
+    // Verify that the API model contains identity info but not credentials
+    StaticCredentialsProvider credProvider =
+        StaticCredentialsProvider.create(
+            AwsBasicCredentials.create("secret-access-key-id", 
"secret-access-key"));
+
+    AwsIamServiceIdentityCredential credential =
+        new 
AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/service", 
credProvider);
+
+    ServiceIdentityInfo model = credential.asServiceIdentityInfoModel();
+    AwsIamServiceIdentityInfo awsModel = (AwsIamServiceIdentityInfo) model;
+
+    // Model should have the ARN
+    
Assertions.assertThat(awsModel.getIamArn()).isEqualTo("arn:aws:iam::123456789012:user/service");
+
+    // Model should NOT have access keys or secrets (they're not in the 
AwsIamServiceIdentityInfo
+    // class)
+    // This is by design - credentials are never exposed in API responses
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
index ad7995925..9adf88fd0 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
@@ -100,6 +100,8 @@ import org.apache.polaris.core.entity.PrincipalRoleEntity;
 import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
 import org.apache.polaris.core.entity.table.federated.FederatedEntities;
 import org.apache.polaris.core.exceptions.CommitConflictException;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
 import org.apache.polaris.core.persistence.dao.entity.BaseResult;
@@ -149,6 +151,7 @@ public class PolarisAdminService {
   private final PolarisAuthorizer authorizer;
   private final PolarisMetaStoreManager metaStoreManager;
   private final UserSecretsManager userSecretsManager;
+  private final ServiceIdentityProvider serviceIdentityProvider;
   private final ReservedProperties reservedProperties;
 
   // Initialized in the authorize methods.
@@ -161,6 +164,7 @@ public class PolarisAdminService {
       @Nonnull ResolutionManifestFactory resolutionManifestFactory,
       @Nonnull PolarisMetaStoreManager metaStoreManager,
       @Nonnull UserSecretsManager userSecretsManager,
+      @Nonnull ServiceIdentityProvider serviceIdentityProvider,
       @Nonnull SecurityContext securityContext,
       @Nonnull PolarisAuthorizer authorizer,
       @Nonnull ReservedProperties reservedProperties) {
@@ -179,6 +183,7 @@ public class PolarisAdminService {
     this.polarisPrincipal = (PolarisPrincipal) 
securityContext.getUserPrincipal();
     this.authorizer = authorizer;
     this.userSecretsManager = userSecretsManager;
+    this.serviceIdentityProvider = serviceIdentityProvider;
     this.reservedProperties = reservedProperties;
   }
 
@@ -190,6 +195,10 @@ public class PolarisAdminService {
     return userSecretsManager;
   }
 
+  private ServiceIdentityProvider getServiceIdentityProvider() {
+    return serviceIdentityProvider;
+  }
+
   private PolarisResolutionManifest newResolutionManifest(@Nullable String 
catalogName) {
     return resolutionManifestFactory.createResolutionManifest(securityContext, 
catalogName);
   }
@@ -671,6 +680,12 @@ public class PolarisAdminService {
                   
AuthenticationParametersDpo.INLINE_BEARER_TOKEN_REFERENCE_KEY, secretReference);
               break;
             }
+          case SIGV4:
+            {
+              // SigV4 authentication is not based on users provided secrets 
but based on the
+              // service identity managed by Polaris. Nothing to do here.
+              break;
+            }
           default:
             throw new IllegalStateException(
                 "Unsupported authentication type: "
@@ -750,10 +765,19 @@ public class PolarisAdminService {
                   
AuthenticationParameters.AuthenticationTypeEnum.IMPLICIT.name()),
               "Implicit authentication based catalog federation is not 
supported.");
         }
+
+        // Allocate service identity if needed for the authentication type.
+        // The provider will determine if a service identity is required based 
on the connection
+        // config.
+        Optional<ServiceIdentityInfoDpo> serviceIdentityInfoDpoOptional =
+            
serviceIdentityProvider.allocateServiceIdentity(connectionConfigInfo);
+
         entity =
             new CatalogEntity.Builder(entity)
                 .setConnectionConfigInfoDpoWithSecrets(
-                    connectionConfigInfo, processedSecretReferences)
+                    connectionConfigInfo,
+                    processedSecretReferences,
+                    serviceIdentityInfoDpoOptional.orElse(null))
                 .build();
       }
     }
@@ -929,7 +953,9 @@ public class PolarisAdminService {
   /** List all catalogs after checking for permission. */
   public List<Catalog> listCatalogs() {
     
authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation.LIST_CATALOGS);
-    return listCatalogsUnsafe().map(CatalogEntity::asCatalog).toList();
+    return listCatalogsUnsafe()
+        .map(catalogEntity -> 
catalogEntity.asCatalog(getServiceIdentityProvider()))
+        .toList();
   }
 
   /** List all catalogs without checking for permission. */
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
index 03a8001a8..eebd3aa16 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
@@ -71,6 +71,7 @@ import org.apache.polaris.core.entity.CatalogRoleEntity;
 import org.apache.polaris.core.entity.PolarisPrivilege;
 import org.apache.polaris.core.entity.PrincipalEntity;
 import org.apache.polaris.core.entity.PrincipalRoleEntity;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.dao.entity.BaseResult;
 import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult;
 import org.apache.polaris.service.admin.api.PolarisCatalogsApiService;
@@ -91,15 +92,18 @@ public class PolarisServiceImpl
   private final RealmConfig realmConfig;
   private final ReservedProperties reservedProperties;
   private final PolarisAdminService adminService;
+  private final ServiceIdentityProvider serviceIdentityProvider;
 
   @Inject
   public PolarisServiceImpl(
       RealmConfig realmConfig,
       ReservedProperties reservedProperties,
-      PolarisAdminService adminService) {
+      PolarisAdminService adminService,
+      ServiceIdentityProvider serviceIdentityProvider) {
     this.realmConfig = realmConfig;
     this.reservedProperties = reservedProperties;
     this.adminService = adminService;
+    this.serviceIdentityProvider = serviceIdentityProvider;
   }
 
   private static Response toResponse(BaseResult result, Response.Status 
successStatus) {
@@ -126,7 +130,8 @@ public class PolarisServiceImpl
     validateStorageConfig(catalog.getStorageConfigInfo());
     validateExternalCatalog(catalog);
     validateCatalogProperties(catalog.getProperties());
-    Catalog newCatalog = 
CatalogEntity.of(adminService.createCatalog(request)).asCatalog();
+    Catalog newCatalog =
+        
CatalogEntity.of(adminService.createCatalog(request)).asCatalog(serviceIdentityProvider);
     LOGGER.info("Created new catalog {}", newCatalog);
     return Response.status(Response.Status.CREATED).entity(newCatalog).build();
   }
@@ -237,7 +242,8 @@ public class PolarisServiceImpl
   @Override
   public Response getCatalog(
       String catalogName, RealmContext realmContext, SecurityContext 
securityContext) {
-    return 
Response.ok(adminService.getCatalog(catalogName).asCatalog()).build();
+    return 
Response.ok(adminService.getCatalog(catalogName).asCatalog(serviceIdentityProvider))
+        .build();
   }
 
   /** From PolarisCatalogsApiService */
@@ -251,7 +257,11 @@ public class PolarisServiceImpl
       validateStorageConfig(updateRequest.getStorageConfigInfo());
     }
     validateCatalogProperties(updateRequest.getProperties());
-    return Response.ok(adminService.updateCatalog(catalogName, 
updateRequest).asCatalog()).build();
+    return Response.ok(
+            adminService
+                .updateCatalog(catalogName, updateRequest)
+                .asCatalog(serviceIdentityProvider))
+        .build();
   }
 
   /** From PolarisCatalogsApiService */
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
index 6a70d2960..214ba9ad8 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
@@ -60,6 +60,7 @@ import 
org.apache.polaris.service.auth.AuthenticationConfiguration;
 import org.apache.polaris.service.auth.AuthenticationRealmConfiguration;
 import org.apache.polaris.service.auth.AuthenticationType;
 import org.apache.polaris.service.auth.Authenticator;
+import org.apache.polaris.service.auth.external.OidcConfiguration;
 import org.apache.polaris.service.auth.external.tenant.OidcTenantResolver;
 import org.apache.polaris.service.auth.internal.broker.TokenBroker;
 import org.apache.polaris.service.auth.internal.broker.TokenBrokerFactory;
@@ -387,8 +388,7 @@ public class ServiceProducers {
 
   @Produces
   public OidcTenantResolver oidcTenantResolver(
-      org.apache.polaris.service.auth.external.OidcConfiguration config,
-      @Any Instance<OidcTenantResolver> resolvers) {
+      OidcConfiguration config, @Any Instance<OidcTenantResolver> resolvers) {
     return 
resolvers.select(Identifier.Literal.of(config.tenantResolver())).get();
   }
 
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java
new file mode 100644
index 000000000..e92b28d88
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.service.identity;
+
+import jakarta.annotation.Nonnull;
+import java.util.Optional;
+import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.identity.ServiceIdentityType;
+import 
org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential;
+import org.apache.polaris.core.secrets.SecretReference;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+
+/**
+ * Configuration for an AWS IAM service identity used by Polaris to access AWS 
services.
+ *
+ * <p>This includes the IAM ARN and optionally, static credentials (access 
key, secret key, and
+ * session token). If credentials are provided, they will be used to construct 
a {@link
+ * AwsIamServiceIdentityCredential}; otherwise, the AWS default credential 
provider chain is used.
+ */
+public interface AwsIamServiceIdentityConfiguration extends 
ResolvableServiceIdentityConfiguration {
+
+  /** The IAM role or user ARN representing the service identity. */
+  String iamArn();
+
+  /**
+   * Optional AWS access key ID associated with the IAM identity. If not 
provided, the AWS default
+   * credential chain will be used.
+   */
+  Optional<String> accessKeyId();
+
+  /**
+   * Optional AWS secret access key associated with the IAM identity. If not 
provided, the AWS
+   * default credential chain will be used.
+   */
+  Optional<String> secretAccessKey();
+
+  /**
+   * Optional AWS session token associated with the IAM identity. If not 
provided, the AWS default
+   * credential chain will be used.
+   */
+  Optional<String> sessionToken();
+
+  /**
+   * Returns the type of service identity represented by this configuration, 
which is always {@link
+   * ServiceIdentityType#AWS_IAM}.
+   *
+   * @return the AWS IAM service identity type
+   */
+  @Override
+  default ServiceIdentityType getType() {
+    return ServiceIdentityType.AWS_IAM;
+  }
+
+  /**
+   * Returns the {@link AwsIamServiceIdentityInfo} model containing only the 
IAM ARN.
+   *
+   * <p>This method is lightweight and does not construct AWS credential 
providers. It should be
+   * used for API responses where only identity metadata is needed.
+   *
+   * @return the service identity info model, or empty if the IAM ARN is not 
configured
+   */
+  @Override
+  default Optional<AwsIamServiceIdentityInfo> asServiceIdentityInfoModel() {
+    if (iamArn() == null) {
+      return Optional.empty();
+    }
+    return Optional.of(
+        AwsIamServiceIdentityInfo.builder()
+            .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM)
+            .setIamArn(iamArn())
+            .build());
+  }
+
+  /**
+   * Converts this configuration into a {@link 
AwsIamServiceIdentityCredential} with actual AWS
+   * credentials.
+   *
+   * <p>Creates a credential object containing the configured IAM ARN and AWS 
credentials provider.
+   * The credentials provider is constructed based on whether static 
credentials (access key, secret
+   * key, session token) are configured or whether to use the default AWS 
credential chain.
+   *
+   * <p>This method should only be called when credentials are actually needed 
for authentication.
+   *
+   * @param secretReference the secret reference to associate with this 
credential
+   * @return the service identity credential, or empty if the IAM ARN is not 
configured
+   */
+  @Override
+  default Optional<AwsIamServiceIdentityCredential> 
asServiceIdentityCredential(
+      @Nonnull SecretReference secretReference) {
+    if (iamArn() == null) {
+      return Optional.empty();
+    }
+    return Optional.of(
+        new AwsIamServiceIdentityCredential(secretReference, iamArn(), 
awsCredentialsProvider()));
+  }
+
+  /**
+   * Constructs an {@link AwsCredentialsProvider} based on the configured 
access key, secret key,
+   * and session token. If the access key and secret key are provided, a 
static credentials provider
+   * is created; otherwise, the default credentials provider chain is used.
+   *
+   * @return the constructed AWS credentials provider
+   */
+  @Nonnull
+  default AwsCredentialsProvider awsCredentialsProvider() {
+    if (accessKeyId().isPresent() && secretAccessKey().isPresent()) {
+      if (sessionToken().isPresent()) {
+        return StaticCredentialsProvider.create(
+            AwsSessionCredentials.create(
+                accessKeyId().get(), secretAccessKey().get(), 
sessionToken().get()));
+      } else {
+        return StaticCredentialsProvider.create(
+            AwsBasicCredentials.create(accessKeyId().get(), 
secretAccessKey().get()));
+      }
+    } else {
+      return DefaultCredentialsProvider.builder().build();
+    }
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/identity/RealmServiceIdentityConfiguration.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/RealmServiceIdentityConfiguration.java
new file mode 100644
index 000000000..a58fa25a9
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/RealmServiceIdentityConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.service.identity;
+
+import io.smallrye.config.WithName;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Represents service identity configuration for a specific realm.
+ *
+ * <p>Supports multiple identity types, such as AWS IAM. This interface allows 
each realm to define
+ * the credentials and metadata needed to resolve service-managed identities.
+ */
+public interface RealmServiceIdentityConfiguration {
+  /**
+   * Returns the AWS IAM service identity configuration for this realm, if 
present.
+   *
+   * @return an optional AWS IAM configuration
+   */
+  @WithName("aws-iam")
+  Optional<AwsIamServiceIdentityConfiguration> awsIamServiceIdentity();
+
+  /**
+   * Aggregates all configured service identity types into a list. This 
includes AWS IAM and
+   * potentially other types in the future.
+   *
+   * @return a list of configured service identity definitions
+   */
+  default List<? extends ResolvableServiceIdentityConfiguration> 
serviceIdentityConfigurations() {
+    return 
Stream.of(awsIamServiceIdentity()).flatMap(Optional::stream).toList();
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java
new file mode 100644
index 000000000..dc03aa0c5
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.service.identity;
+
+import jakarta.annotation.Nonnull;
+import java.util.Optional;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.identity.ServiceIdentityType;
+import org.apache.polaris.core.identity.credential.ServiceIdentityCredential;
+import org.apache.polaris.core.secrets.SecretReference;
+
+/**
+ * Represents a service identity configuration that can be converted into a 
fully initialized {@link
+ * ServiceIdentityCredential}.
+ *
+ * <p>This interface allows identity configurations (e.g., AWS IAM) to 
encapsulate the logic
+ * required to construct runtime credentials and metadata needed to 
authenticate as a
+ * Polaris-managed service identity.
+ */
+public interface ResolvableServiceIdentityConfiguration {
+  /**
+   * Returns the type of service identity represented by this configuration.
+   *
+   * @return the service identity type, or {@link 
ServiceIdentityType#NULL_TYPE} if not specified
+   */
+  default ServiceIdentityType getType() {
+    return ServiceIdentityType.NULL_TYPE;
+  }
+
+  /**
+   * Converts this configuration into a {@link ServiceIdentityInfo} model 
containing identity
+   * metadata without credentials.
+   *
+   * <p>This method is used when only identity information (e.g., IAM ARN) is 
needed for API
+   * responses, without exposing sensitive credentials.
+   *
+   * @return an optional service identity info model, or empty if required 
configuration is missing
+   */
+  default Optional<? extends ServiceIdentityInfo> asServiceIdentityInfoModel() 
{
+    return Optional.empty();
+  }
+
+  /**
+   * Converts this configuration into a {@link ServiceIdentityCredential} with 
actual credentials.
+   *
+   * <p>This method should only be called when credentials are actually needed 
for authentication.
+   * Implementations should construct the appropriate credential object (e.g., 
{@link
+   * 
org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential}) 
using the
+   * configured values and the provided secret reference.
+   *
+   * @param secretReference the secret reference to associate with this 
credential for persistence
+   * @return an optional service identity credential, or empty if required 
configuration is missing
+   */
+  default Optional<? extends ServiceIdentityCredential> 
asServiceIdentityCredential(
+      @Nonnull SecretReference secretReference) {
+    return Optional.empty();
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java
new file mode 100644
index 000000000..fc6a2ce42
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.service.identity;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefaults;
+import io.smallrye.config.WithParentName;
+import io.smallrye.config.WithUnnamedKey;
+import java.util.Map;
+import org.apache.polaris.core.context.RealmContext;
+
+/**
+ * Configuration interface for managing service identities across multiple 
realms in Polaris.
+ *
+ * <p>A service identity represents the Polaris service itself when it needs 
to authenticate to
+ * external systems (e.g., AWS services for SigV4 authentication). Each realm 
can configure its own
+ * set of service identities for different cloud providers.
+ *
+ * <p>This interface supports multi-tenant deployments where each realm 
(tenant) can have distinct
+ * service identities, as well as single-tenant deployments with a default 
configuration shared
+ * across all catalogs.
+ *
+ * <p>Configuration is loaded from {@code polaris.service-identity.*} 
properties at startup and
+ * includes credentials that Polaris uses to assume customer-provided roles 
when accessing federated
+ * catalogs.
+ *
+ * <p><b>Example Configuration:</b>
+ *
+ * <pre>{@code
+ * # Default service identity (used when no realm-specific configuration 
exists)
+ * 
polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-default-user
+ * # Optional: provide static credentials, or omit to use AWS default 
credential chain
+ * polaris.service-identity.aws-iam.access-key-id=<access-key-id>
+ * polaris.service-identity.aws-iam.secret-access-key=<secret-access-key>
+ * polaris.service-identity.aws-iam.session-token=<optional-session-token>
+ *
+ * # Realm-specific service identity for multi-tenant deployments
+ * 
polaris.service-identity.my-realm.aws-iam.iam-arn=arn:aws:iam::123456789012:user/my-realm-user
+ * polaris.service-identity.my-realm.aws-iam.access-key-id=<access-key-id>
+ * 
polaris.service-identity.my-realm.aws-iam.secret-access-key=<secret-access-key>
+ * }</pre>
+ */
+@ConfigMapping(prefix = "polaris.service-identity")
+public interface ServiceIdentityConfiguration {
+  /**
+   * The key used to identify the default realm configuration.
+   *
+   * <p>This default is especially useful in testing scenarios and 
single-tenant deployments where
+   * only one realm is expected and explicitly configuring realms is 
unnecessary.
+   */
+  String DEFAULT_REALM_KEY = "<default>";
+
+  /**
+   * Returns a map of realm identifiers to their corresponding service 
identity configurations.
+   *
+   * @return the map of realm-specific configurations
+   */
+  @WithParentName
+  @WithUnnamedKey(DEFAULT_REALM_KEY)
+  @WithDefaults
+  Map<String, RealmServiceIdentityConfiguration> realms();
+
+  /**
+   * Retrieves the configuration entry for the given realm context.
+   *
+   * <p>If the realm has no specific configuration, falls back to the default 
realm configuration.
+   *
+   * @param realmContext the realm context
+   * @return the configuration entry containing the realm identifier and its 
configuration
+   */
+  default RealmConfigEntry forRealm(RealmContext realmContext) {
+    return forRealm(realmContext.getRealmIdentifier());
+  }
+
+  /**
+   * Retrieves the configuration entry for the given realm identifier.
+   *
+   * <p>If the realm has no specific configuration, falls back to the default 
realm configuration.
+   *
+   * @param realmIdentifier the realm identifier
+   * @return the configuration entry containing the realm identifier and its 
configuration
+   */
+  default RealmConfigEntry forRealm(String realmIdentifier) {
+    String resolvedRealmIdentifier =
+        realms().containsKey(realmIdentifier) ? realmIdentifier : 
DEFAULT_REALM_KEY;
+    return new RealmConfigEntry(resolvedRealmIdentifier, 
realms().get(resolvedRealmIdentifier));
+  }
+
+  /**
+   * A pairing of a realm identifier and its associated service identity 
configuration.
+   *
+   * @param realm the realm identifier (may be the default if the requested 
realm was not
+   *     configured)
+   * @param config the service identity configuration for this realm
+   */
+  record RealmConfigEntry(String realm, RealmServiceIdentityConfiguration 
config) {}
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java
new file mode 100644
index 000000000..bd11150b1
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.service.identity.provider;
+
+import com.google.common.annotations.VisibleForTesting;
+import jakarta.annotation.Nonnull;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.polaris.core.admin.model.AuthenticationParameters;
+import org.apache.polaris.core.admin.model.ConnectionConfigInfo;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.core.identity.ServiceIdentityType;
+import org.apache.polaris.core.identity.credential.ServiceIdentityCredential;
+import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
+import org.apache.polaris.core.secrets.SecretReference;
+import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration;
+import 
org.apache.polaris.service.identity.ResolvableServiceIdentityConfiguration;
+import org.apache.polaris.service.identity.ServiceIdentityConfiguration;
+
+/**
+ * Default implementation of {@link ServiceIdentityProvider} that provides 
service identity
+ * credentials from statically configured values.
+ *
+ * <p>This implementation loads service identity configurations at startup and 
uses them to provide
+ * identity information and credentials on demand. All resolution is done 
lazily - credentials are
+ * only created when actually needed for authentication.
+ */
+@RequestScoped
+public class DefaultServiceIdentityProvider implements ServiceIdentityProvider 
{
+  public static final String DEFAULT_REALM_KEY = 
ServiceIdentityConfiguration.DEFAULT_REALM_KEY;
+  public static final String DEFAULT_REALM_NSS = "system:default";
+  private static final String IDENTITY_INFO_REFERENCE_URN_FORMAT =
+      "urn:polaris-secret:default-identity-provider:%s:%s";
+
+  private final String realm;
+  private final RealmServiceIdentityConfiguration config;
+
+  public DefaultServiceIdentityProvider() {
+    this.realm = DEFAULT_REALM_KEY;
+    this.config = null;
+  }
+
+  @Inject
+  public DefaultServiceIdentityProvider(
+      RealmContext realmContext, ServiceIdentityConfiguration 
serviceIdentityConfiguration) {
+    ServiceIdentityConfiguration.RealmConfigEntry entry =
+        serviceIdentityConfiguration.forRealm(realmContext);
+    this.realm = entry.realm();
+    this.config = entry.config();
+  }
+
+  @Override
+  public Optional<ServiceIdentityInfoDpo> allocateServiceIdentity(
+      @Nonnull ConnectionConfigInfo connectionConfig) {
+    if (config == null || connectionConfig.getAuthenticationParameters() == 
null) {
+      return Optional.empty();
+    }
+
+    AuthenticationParameters.AuthenticationTypeEnum authType =
+        connectionConfig.getAuthenticationParameters().getAuthenticationType();
+
+    // Map authentication type to service identity type and check if configured
+    return switch (authType) {
+      case SIGV4 ->
+          config.awsIamServiceIdentity().isPresent()
+              ? Optional.of(
+                  new AwsIamServiceIdentityInfoDpo(
+                      buildIdentityInfoReference(realm, 
ServiceIdentityType.AWS_IAM)))
+              : Optional.empty();
+      default -> Optional.empty();
+    };
+  }
+
+  @Override
+  public Optional<ServiceIdentityInfo> getServiceIdentityInfo(
+      @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) {
+    if (config == null) {
+      return Optional.empty();
+    }
+
+    // Find the configuration matching the reference and return metadata only
+    SecretReference actualRef = serviceIdentityInfo.getIdentityInfoReference();
+
+    return config.serviceIdentityConfigurations().stream()
+        .filter(
+            identityConfig ->
+                buildIdentityInfoReference(realm, 
identityConfig.getType()).equals(actualRef))
+        .findFirst()
+        
.flatMap(ResolvableServiceIdentityConfiguration::asServiceIdentityInfoModel);
+  }
+
+  @Override
+  public Optional<ServiceIdentityCredential> getServiceIdentityCredential(
+      @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) {
+    if (config == null) {
+      return Optional.empty();
+    }
+
+    // Find the configuration matching the reference and resolve credential 
lazily
+    SecretReference ref = serviceIdentityInfo.getIdentityInfoReference();
+
+    return config.serviceIdentityConfigurations().stream()
+        .filter(
+            identityConfig ->
+                buildIdentityInfoReference(realm, 
identityConfig.getType()).equals(ref))
+        .findFirst()
+        .flatMap(identityConfig -> 
identityConfig.asServiceIdentityCredential(ref));
+  }
+
+  @VisibleForTesting
+  public RealmServiceIdentityConfiguration getRealmConfig() {
+    return config;
+  }
+
+  /**
+   * Builds a {@link SecretReference} for the given realm and service identity 
type.
+   *
+   * <p>The URN format is: 
urn:polaris-secret:default-identity-provider:&lt;realm&gt;:&lt;type&gt;
+   *
+   * <p>If the realm is the default realm key, it is replaced with 
"system:default" in the URN.
+   *
+   * @param realm the realm identifier
+   * @param type the service identity type
+   * @return the constructed secret reference for this service identity
+   */
+  public static SecretReference buildIdentityInfoReference(String realm, 
ServiceIdentityType type) {
+    // urn:polaris-secret:default-identity-provider:<realm>:<type>
+    return new SecretReference(
+        IDENTITY_INFO_REFERENCE_URN_FORMAT.formatted(
+            realm.equals(DEFAULT_REALM_KEY) ? DEFAULT_REALM_NSS : realm, 
type.name()),
+        Map.of());
+  }
+}
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
index 87f98be0e..e13d6fe08 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
@@ -59,6 +59,7 @@ import 
org.apache.polaris.core.persistence.dao.entity.EntityResult;
 import org.apache.polaris.core.secrets.UnsafeInMemorySecretsManager;
 import org.apache.polaris.service.TestServices;
 import org.apache.polaris.service.config.ReservedProperties;
+import 
org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -376,6 +377,7 @@ public class ManagementServiceTest {
         services.resolutionManifestFactory(),
         metaStoreManager,
         new UnsafeInMemorySecretsManager(),
+        new DefaultServiceIdentityProvider(),
         new SecurityContext() {
           @Override
           public Principal getUserPrincipal() {
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
index b60bcb0a9..066aebf20 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
@@ -56,6 +56,7 @@ public class PolarisAdminServiceAuthzTest extends 
PolarisAuthzTestBase {
         resolutionManifestFactory,
         metaStoreManager,
         userSecretsManager,
+        serviceIdentityProvider,
         securityContext(authenticatedPrincipal),
         polarisAuthorizer,
         reservedProperties);
@@ -134,7 +135,8 @@ public class PolarisAdminServiceAuthzTest extends 
PolarisAuthzTestBase {
         adminService.grantPrivilegeOnRootContainerToPrincipalRole(
             PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_DROP));
     final CatalogEntity newCatalog = new 
CatalogEntity.Builder().setName("new_catalog").build();
-    final CreateCatalogRequest createRequest = new 
CreateCatalogRequest(newCatalog.asCatalog());
+    final CreateCatalogRequest createRequest =
+        new 
CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider));
 
     doTestSufficientPrivileges(
         List.of(
@@ -153,7 +155,8 @@ public class PolarisAdminServiceAuthzTest extends 
PolarisAuthzTestBase {
   @Test
   public void testCreateCatalogInsufficientPrivileges() {
     final CatalogEntity newCatalog = new 
CatalogEntity.Builder().setName("new_catalog").build();
-    final CreateCatalogRequest createRequest = new 
CreateCatalogRequest(newCatalog.asCatalog());
+    final CreateCatalogRequest createRequest =
+        new 
CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider));
 
     doTestInsufficientPrivileges(
         List.of(
@@ -288,7 +291,8 @@ public class PolarisAdminServiceAuthzTest extends 
PolarisAuthzTestBase {
         adminService.grantPrivilegeOnRootContainerToPrincipalRole(
             PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_CREATE));
     final CatalogEntity newCatalog = new 
CatalogEntity.Builder().setName("new_catalog").build();
-    final CreateCatalogRequest createRequest = new 
CreateCatalogRequest(newCatalog.asCatalog());
+    final CreateCatalogRequest createRequest =
+        new 
CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider));
     adminService.createCatalog(createRequest);
 
     doTestSufficientPrivileges(
@@ -308,7 +312,8 @@ public class PolarisAdminServiceAuthzTest extends 
PolarisAuthzTestBase {
   @Test
   public void testDeleteCatalogInsufficientPrivileges() {
     final CatalogEntity newCatalog = new 
CatalogEntity.Builder().setName("new_catalog").build();
-    final CreateCatalogRequest createRequest = new 
CreateCatalogRequest(newCatalog.asCatalog());
+    final CreateCatalogRequest createRequest =
+        new 
CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider));
     adminService.createCatalog(createRequest);
 
     doTestInsufficientPrivileges(
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java
index 3181a1ef5..fb58dc344 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java
@@ -47,6 +47,7 @@ import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.PolarisPrivilege;
 import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
 import org.apache.polaris.core.persistence.dao.entity.BaseResult;
@@ -72,6 +73,7 @@ public class PolarisAdminServiceTest {
   @Mock private ResolutionManifestFactory resolutionManifestFactory;
   @Mock private PolarisMetaStoreManager metaStoreManager;
   @Mock private UserSecretsManager userSecretsManager;
+  @Mock private ServiceIdentityProvider identityProvider;
   @Mock private SecurityContext securityContext;
   @Mock private PolarisAuthorizer authorizer;
   @Mock private ReservedProperties reservedProperties;
@@ -108,6 +110,7 @@ public class PolarisAdminServiceTest {
             resolutionManifestFactory,
             metaStoreManager,
             userSecretsManager,
+            identityProvider,
             securityContext,
             authorizer,
             reservedProperties);
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java
index d0239a83d..4eb9b0f46 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java
@@ -72,6 +72,7 @@ import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.PolarisPrivilege;
 import org.apache.polaris.core.entity.PrincipalEntity;
 import org.apache.polaris.core.entity.PrincipalRoleEntity;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.dao.entity.BaseResult;
@@ -195,6 +196,7 @@ public abstract class PolarisAuthzTestBase {
   @Inject protected ResolutionManifestFactory resolutionManifestFactory;
   @Inject protected CallContextCatalogFactory callContextCatalogFactory;
   @Inject protected UserSecretsManagerFactory userSecretsManagerFactory;
+  @Inject protected ServiceIdentityProvider serviceIdentityProvider;
   @Inject protected PolarisDiagnostics diagServices;
   @Inject protected FileIOFactory fileIOFactory;
   @Inject protected PolarisEventListener polarisEventListener;
@@ -263,6 +265,7 @@ public abstract class PolarisAuthzTestBase {
             resolutionManifestFactory,
             metaStoreManager,
             userSecretsManager,
+            serviceIdentityProvider,
             securityContext(authenticatedRoot),
             polarisAuthorizer,
             reservedProperties);
@@ -329,7 +332,7 @@ public abstract class PolarisAuthzTestBase {
                     .setDefaultBaseLocation(storageLocation)
                     .setStorageConfigurationInfo(realmConfig, 
storageConfigModel, storageLocation)
                     .build()
-                    .asCatalog()));
+                    .asCatalog(serviceIdentityProvider)));
     federatedCatalogEntity = adminService.createCatalog(new 
CreateCatalogRequest(externalCatalog));
 
     initBaseCatalog();
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java
index 926c6c89c..0a8996e5f 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java
@@ -40,9 +40,11 @@ import org.apache.polaris.core.auth.PolarisPrincipal;
 import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.context.CallContext;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.secrets.UserSecretsManager;
+import org.apache.polaris.core.secrets.UserSecretsManagerFactory;
 import org.apache.polaris.service.config.ReservedProperties;
 import org.apache.polaris.service.events.listeners.NoOpPolarisEventListener;
 import org.apache.polaris.service.events.listeners.PolarisEventListener;
@@ -54,8 +56,10 @@ public class PolarisServiceImplTest {
 
   private final PolarisDiagnostics diagnostics = new 
PolarisDefaultDiagServiceImpl();
   private ResolutionManifestFactory resolutionManifestFactory;
+  private UserSecretsManagerFactory userSecretsManagerFactory;
   private PolarisMetaStoreManager metaStoreManager;
   private UserSecretsManager userSecretsManager;
+  private ServiceIdentityProvider serviceIdentityProvider;
   private PolarisAuthorizer polarisAuthorizer;
   private CallContext callContext;
   private ReservedProperties reservedProperties;
@@ -70,6 +74,7 @@ public class PolarisServiceImplTest {
     resolutionManifestFactory = Mockito.mock(ResolutionManifestFactory.class);
     metaStoreManager = Mockito.mock(PolarisMetaStoreManager.class);
     userSecretsManager = Mockito.mock(UserSecretsManager.class);
+    serviceIdentityProvider = Mockito.mock(ServiceIdentityProvider.class);
     polarisAuthorizer = Mockito.mock(PolarisAuthorizer.class);
     callContext = Mockito.mock(CallContext.class);
     reservedProperties = Mockito.mock(ReservedProperties.class);
@@ -93,10 +98,13 @@ public class PolarisServiceImplTest {
             resolutionManifestFactory,
             metaStoreManager,
             userSecretsManager,
+            serviceIdentityProvider,
             securityContext,
             polarisAuthorizer,
             reservedProperties);
-    polarisService = new PolarisServiceImpl(realmConfig, reservedProperties, 
adminService);
+    polarisService =
+        new PolarisServiceImpl(
+            realmConfig, reservedProperties, adminService, 
serviceIdentityProvider);
   }
 
   @Test
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java
index 8e930dd51..1d1071dfe 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java
@@ -52,6 +52,7 @@ import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntity;
 import org.apache.polaris.core.entity.PrincipalEntity;
 import org.apache.polaris.core.entity.table.GenericTableEntity;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
@@ -98,6 +99,7 @@ public abstract class AbstractPolarisGenericTableCatalogTest {
 
   @Inject MetaStoreManagerFactory metaStoreManagerFactory;
   @Inject UserSecretsManagerFactory userSecretsManagerFactory;
+  @Inject ServiceIdentityProvider serviceIdentityProvider;
   @Inject PolarisConfigurationStore configurationStore;
   @Inject StorageCredentialCache storageCredentialCache;
   @Inject PolarisStorageIntegrationProvider storageIntegrationProvider;
@@ -173,6 +175,7 @@ public abstract class 
AbstractPolarisGenericTableCatalogTest {
             resolutionManifestFactory,
             metaStoreManager,
             userSecretsManager,
+            serviceIdentityProvider,
             securityContext,
             authorizer,
             reservedProperties);
@@ -202,7 +205,7 @@ public abstract class 
AbstractPolarisGenericTableCatalogTest {
                         
FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true")
                     .setStorageConfigurationInfo(realmConfig, 
storageConfigModel, storageLocation)
                     .build()
-                    .asCatalog()));
+                    .asCatalog(serviceIdentityProvider)));
 
     PolarisPassthroughResolutionView passthroughView =
         new PolarisPassthroughResolutionView(
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java
index 64654aeb8..2418bcfcc 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java
@@ -110,6 +110,7 @@ import org.apache.polaris.core.entity.PolarisEntityType;
 import org.apache.polaris.core.entity.PrincipalEntity;
 import org.apache.polaris.core.entity.TaskEntity;
 import org.apache.polaris.core.exceptions.CommitConflictException;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
@@ -229,6 +230,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
   @Inject StorageCredentialCache storageCredentialCache;
   @Inject PolarisStorageIntegrationProvider storageIntegrationProvider;
   @Inject UserSecretsManagerFactory userSecretsManagerFactory;
+  @Inject ServiceIdentityProvider serviceIdentityProvider;
   @Inject PolarisDiagnostics diagServices;
   @Inject PolarisEventListener polarisEventListener;
 
@@ -318,6 +320,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
             resolutionManifestFactory,
             metaStoreManager,
             userSecretsManager,
+            serviceIdentityProvider,
             securityContext,
             authorizer,
             reservedProperties);
@@ -347,7 +350,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
                         
FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true")
                     .setStorageConfigurationInfo(realmConfig, 
storageConfigModel, storageLocation)
                     .build()
-                    .asCatalog()));
+                    .asCatalog(serviceIdentityProvider)));
 
     this.fileIOFactory = new DefaultFileIOFactory(storageCredentialCache, 
metaStoreManagerFactory);
 
@@ -1378,7 +1381,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
                     .setDefaultBaseLocation("file://")
                     .setName(catalogWithoutStorage)
                     .build()
-                    .asCatalog()));
+                    .asCatalog(serviceIdentityProvider)));
 
     IcebergCatalog catalog = newIcebergCatalog(catalogWithoutStorage);
     catalog.initialize(
@@ -1428,7 +1431,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
                 .setDefaultBaseLocation("http://maliciousdomain.com";)
                 .setName(catalogName)
                 .build()
-                .asCatalog()));
+                .asCatalog(serviceIdentityProvider)));
 
     IcebergCatalog catalog = newIcebergCatalog(catalogName);
     catalog.initialize(
@@ -1946,7 +1949,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
                 .setStorageConfigurationInfo(
                     realmConfig, noPurgeStorageConfigModel, storageLocation)
                 .build()
-                .asCatalog()));
+                .asCatalog(serviceIdentityProvider)));
     IcebergCatalog noPurgeCatalog =
         newIcebergCatalog(noPurgeCatalogName, metaStoreManager, fileIOFactory);
     noPurgeCatalog.initialize(
@@ -2204,7 +2207,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
                           .setName("createCatalogWithReservedProperty")
                           .setProperties(ImmutableMap.of("polaris.reserved", 
"true"))
                           .build()
-                          .asCatalog()));
+                          .asCatalog(serviceIdentityProvider)));
             })
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessageContaining("reserved prefix");
@@ -2219,7 +2222,7 @@ public abstract class AbstractIcebergCatalogTest extends 
CatalogTests<IcebergCat
                 .setName("updateCatalogWithReservedProperty")
                 .setProperties(ImmutableMap.of("a", "b"))
                 .build()
-                .asCatalog()));
+                .asCatalog(serviceIdentityProvider)));
     Assertions.assertThatCode(
             () -> {
               adminService.updateCatalog(
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java
index 8c34f74f4..c4b68658a 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java
@@ -49,6 +49,7 @@ import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PrincipalEntity;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
@@ -107,6 +108,7 @@ public abstract class AbstractIcebergCatalogViewTest 
extends ViewCatalogTests<Ic
 
   @Inject MetaStoreManagerFactory metaStoreManagerFactory;
   @Inject UserSecretsManagerFactory userSecretsManagerFactory;
+  @Inject ServiceIdentityProvider serviceIdentityProvider;
   @Inject PolarisConfigurationStore configurationStore;
   @Inject StorageCredentialCache storageCredentialCache;
   @Inject PolarisDiagnostics diagServices;
@@ -180,6 +182,7 @@ public abstract class AbstractIcebergCatalogViewTest 
extends ViewCatalogTests<Ic
             resolutionManifestFactory,
             metaStoreManager,
             userSecretsManager,
+            serviceIdentityProvider,
             securityContext,
             authorizer,
             reservedProperties);
@@ -199,7 +202,7 @@ public abstract class AbstractIcebergCatalogViewTest 
extends ViewCatalogTests<Ic
                         StorageConfigInfo.StorageTypeEnum.FILE, 
List.of("file://", "/", "*")),
                     "file://tmp")
                 .build()
-                .asCatalog()));
+                .asCatalog(serviceIdentityProvider)));
 
     PolarisPassthroughResolutionView passthroughView =
         new PolarisPassthroughResolutionView(
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
index 2c6493c1b..f9827fddf 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
@@ -1707,7 +1707,7 @@ public class IcebergCatalogHandlerAuthzTest extends 
PolarisAuthzTestBase {
                 .setStorageConfigurationInfo(realmConfig, storageConfigModel, 
storageLocation)
                 .setCatalogType("EXTERNAL")
                 .build()
-                .asCatalog()));
+                .asCatalog(serviceIdentityProvider)));
     adminService.createCatalogRole(
         externalCatalog, new 
CatalogRoleEntity.Builder().setName(CATALOG_ROLE1).build());
     adminService.createCatalogRole(
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java
index 8473198f7..9da8260eb 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java
@@ -58,6 +58,7 @@ import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntity;
 import org.apache.polaris.core.entity.PrincipalEntity;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException;
@@ -124,6 +125,7 @@ public abstract class AbstractPolicyCatalogTest {
 
   @Inject MetaStoreManagerFactory metaStoreManagerFactory;
   @Inject UserSecretsManagerFactory userSecretsManagerFactory;
+  @Inject ServiceIdentityProvider serviceIdentityProvider;
   @Inject PolarisConfigurationStore configurationStore;
   @Inject StorageCredentialCache storageCredentialCache;
   @Inject PolarisStorageIntegrationProvider storageIntegrationProvider;
@@ -194,6 +196,7 @@ public abstract class AbstractPolicyCatalogTest {
             resolutionManifestFactory,
             metaStoreManager,
             userSecretsManager,
+            serviceIdentityProvider,
             securityContext,
             authorizer,
             reservedProperties);
@@ -221,7 +224,7 @@ public abstract class AbstractPolicyCatalogTest {
                         "true")
                     .setStorageConfigurationInfo(realmConfig, 
storageConfigModel, storageLocation)
                     .build()
-                    .asCatalog()));
+                    .asCatalog(serviceIdentityProvider)));
 
     PolarisPassthroughResolutionView passthroughView =
         new PolarisPassthroughResolutionView(
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
index fceeaa54e..47e61f573 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
@@ -23,19 +23,30 @@ import static org.assertj.core.api.Assertions.assertThat;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Stream;
+import org.apache.polaris.core.admin.model.AuthenticationParameters;
+import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
 import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
 import org.apache.polaris.core.admin.model.AzureStorageConfigInfo;
 import org.apache.polaris.core.admin.model.Catalog;
 import org.apache.polaris.core.admin.model.CatalogProperties;
+import org.apache.polaris.core.admin.model.ConnectionConfigInfo;
+import org.apache.polaris.core.admin.model.ExternalCatalog;
 import org.apache.polaris.core.admin.model.GcpStorageConfigInfo;
+import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo;
 import org.apache.polaris.core.admin.model.PolarisCatalog;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.core.config.PolarisConfigurationStore;
 import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.config.RealmConfigImpl;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.CatalogEntity;
+import 
org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential;
+import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -43,16 +54,30 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
 
 public class CatalogEntityTest {
   private static final ObjectMapper MAPPER = new ObjectMapper();
 
   private RealmConfig realmConfig;
+  private ServiceIdentityProvider serviceIdentityProvider;
 
   @BeforeEach
   public void setup() {
     RealmContext realmContext = () -> "realm";
     this.realmConfig = new RealmConfigImpl(new PolarisConfigurationStore() {}, 
realmContext);
+    this.serviceIdentityProvider = Mockito.mock(ServiceIdentityProvider.class);
+    Mockito.when(serviceIdentityProvider.getServiceIdentityInfo(Mockito.any()))
+        .thenReturn(
+            Optional.of(
+                AwsIamServiceIdentityInfo.builder()
+                    
.setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM)
+                    .setIamArn("arn:aws:iam::123456789012:user/test-user")
+                    .build()));
+    
Mockito.when(serviceIdentityProvider.getServiceIdentityCredential(Mockito.any()))
+        .thenReturn(
+            Optional.of(
+                new 
AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user")));
   }
 
   @ParameterizedTest
@@ -278,7 +303,7 @@ public class CatalogEntityTest {
             .setStorageConfigurationInfo(realmConfig, storageConfigModel, 
baseLocation)
             .build();
 
-    Catalog catalog = catalogEntity.asCatalog();
+    Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider);
     assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL);
   }
 
@@ -301,7 +326,7 @@ public class CatalogEntityTest {
             .setStorageConfigurationInfo(realmConfig, storageConfigModel, 
baseLocation)
             .build();
 
-    Catalog catalog = catalogEntity.asCatalog();
+    Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider);
     assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL);
   }
 
@@ -324,7 +349,7 @@ public class CatalogEntityTest {
             .setStorageConfigurationInfo(realmConfig, storageConfigModel, 
baseLocation)
             .build();
 
-    Catalog catalog = catalogEntity.asCatalog();
+    Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider);
     assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL);
   }
 
@@ -362,11 +387,70 @@ public class CatalogEntityTest {
                 config.getAllowedLocations().getFirst())
             .build();
 
-    Catalog catalog = catalogEntity.asCatalog();
+    Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider);
     assertThat(catalog.getStorageConfigInfo()).isEqualTo(config);
     
assertThat(MAPPER.writeValueAsString(catalog.getStorageConfigInfo())).isEqualTo(configStr);
   }
 
+  @Test
+  public void testServiceIdentityInjection() {
+    String baseLocation = "s3://test-bucket/path";
+    AwsStorageConfigInfo storageConfigModel =
+        AwsStorageConfigInfo.builder()
+            .setRoleArn("arn:aws:iam::012345678901:role/test-role")
+            .setExternalId("externalId")
+            .setUserArn("aws::a:user:arn")
+            .setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
+            .setAllowedLocations(List.of(baseLocation))
+            .build();
+    IcebergRestConnectionConfigInfo icebergRestConnectionConfigInfoModel =
+        IcebergRestConnectionConfigInfo.builder()
+            
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST)
+            .setUri("https://glue.us-west-2.amazonaws.com";)
+            .setAuthenticationParameters(
+                SigV4AuthenticationParameters.builder()
+                    
.setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.SIGV4)
+                    .setRoleArn("arn:aws:iam::123456789012:role/test-role")
+                    .setSigningName("glue")
+                    .setSigningRegion("us-west-2")
+                    .build())
+            .build();
+    CatalogEntity catalogEntity =
+        new CatalogEntity.Builder()
+            .setName("test-catalog")
+            .setCatalogType(Catalog.TypeEnum.EXTERNAL.name())
+            .setDefaultBaseLocation(baseLocation)
+            .setStorageConfigurationInfo(realmConfig, storageConfigModel, 
baseLocation)
+            .setConnectionConfigInfoDpoWithSecrets(
+                icebergRestConnectionConfigInfoModel, null, new 
AwsIamServiceIdentityInfoDpo(null))
+            .build();
+
+    Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider);
+    assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL);
+    ExternalCatalog externalCatalog = (ExternalCatalog) catalog;
+    assertThat(externalCatalog.getConnectionConfigInfo().getConnectionType())
+        .isEqualTo(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST);
+    assertThat(externalCatalog.getConnectionConfigInfo().getUri())
+        .isEqualTo("https://glue.us-west-2.amazonaws.com";);
+
+    AuthenticationParameters authParams =
+        
externalCatalog.getConnectionConfigInfo().getAuthenticationParameters();
+    assertThat(authParams.getAuthenticationType())
+        .isEqualTo(AuthenticationParameters.AuthenticationTypeEnum.SIGV4);
+    SigV4AuthenticationParameters sigV4AuthParams = 
(SigV4AuthenticationParameters) authParams;
+    assertThat(sigV4AuthParams.getSigningName()).isEqualTo("glue");
+    assertThat(sigV4AuthParams.getSigningRegion()).isEqualTo("us-west-2");
+    
assertThat(sigV4AuthParams.getRoleArn()).isEqualTo("arn:aws:iam::123456789012:role/test-role");
+
+    ServiceIdentityInfo serviceIdentity =
+        externalCatalog.getConnectionConfigInfo().getServiceIdentity();
+    assertThat(serviceIdentity.getIdentityType())
+        .isEqualTo(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM);
+    AwsIamServiceIdentityInfo awsIamServiceIdentity = 
(AwsIamServiceIdentityInfo) serviceIdentity;
+    assertThat(awsIamServiceIdentity.getIamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/test-user");
+  }
+
   public static Stream<Arguments> testAwsConfigRoundTrip() {
     AwsStorageConfigInfo.Builder b =
         AwsStorageConfigInfo.builder()
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java
new file mode 100644
index 000000000..9342994e0
--- /dev/null
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java
@@ -0,0 +1,369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.service.identity.provider;
+
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.TestProfile;
+import jakarta.inject.Inject;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.polaris.core.admin.model.AuthenticationParameters;
+import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo;
+import org.apache.polaris.core.admin.model.BearerAuthenticationParameters;
+import org.apache.polaris.core.admin.model.ConnectionConfigInfo;
+import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo;
+import org.apache.polaris.core.admin.model.ServiceIdentityInfo;
+import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters;
+import org.apache.polaris.core.context.RealmContext;
+import org.apache.polaris.core.identity.ServiceIdentityType;
+import 
org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential;
+import org.apache.polaris.core.identity.credential.ServiceIdentityCredential;
+import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo;
+import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo;
+import org.apache.polaris.core.secrets.SecretReference;
+import org.apache.polaris.service.identity.AwsIamServiceIdentityConfiguration;
+import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration;
+import org.apache.polaris.service.identity.ServiceIdentityConfiguration;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+
+@QuarkusTest
+@TestProfile(DefaultServiceIdentityProviderTest.Profile.class)
+public class DefaultServiceIdentityProviderTest {
+  private static final String DEFAULT_REALM_KEY = 
ServiceIdentityConfiguration.DEFAULT_REALM_KEY;
+  private static final String MY_REALM_KEY = "my-realm";
+
+  @InjectMock RealmContext realmContext;
+  @Inject ServiceIdentityConfiguration serviceIdentityConfiguration;
+
+  public static class Profile implements QuarkusTestProfile {
+    @Override
+    public Map<String, String> getConfigOverrides() {
+      return Map.of(
+          "quarkus.identity-provider.type",
+          "default",
+          "polaris.service-identity.aws-iam.iam-arn",
+          "arn:aws:iam::123456789012:user/polaris-default-iam-user",
+          "polaris.service-identity.my-realm.aws-iam.iam-arn",
+          "arn:aws:iam::123456789012:user/polaris-iam-user",
+          "polaris.service-identity.my-realm.aws-iam.access-key-id",
+          "access-key-id",
+          "polaris.service-identity.my-realm.aws-iam.secret-access-key",
+          "secret-access-key",
+          "polaris.service-identity.my-realm.aws-iam.session-token",
+          "session-token");
+    }
+  }
+
+  @Test
+  void testServiceIdentityConfiguration() {
+    // Ensure that the service identity configuration is loaded correctly
+    Assertions.assertThat(serviceIdentityConfiguration.realms()).isNotNull();
+    Assertions.assertThat(serviceIdentityConfiguration.realms())
+        .containsKey(ServiceIdentityConfiguration.DEFAULT_REALM_KEY)
+        .containsKey(MY_REALM_KEY)
+        .size()
+        .isEqualTo(2);
+
+    // Check the default realm configuration
+    ServiceIdentityConfiguration.RealmConfigEntry defaultConfigEntry =
+        serviceIdentityConfiguration.forRealm(DEFAULT_REALM_KEY);
+    
Assertions.assertThat(defaultConfigEntry.realm()).isEqualTo(DEFAULT_REALM_KEY);
+    RealmServiceIdentityConfiguration defaultConfig = 
defaultConfigEntry.config();
+    
Assertions.assertThat(defaultConfig.awsIamServiceIdentity().isPresent()).isTrue();
+    Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user");
+    
Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().accessKeyId()).isEmpty();
+    
Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().secretAccessKey()).isEmpty();
+    
Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().sessionToken()).isEmpty();
+
+    // Check the my-realm configuration
+    ServiceIdentityConfiguration.RealmConfigEntry myRealmConfigEntry =
+        serviceIdentityConfiguration.forRealm(MY_REALM_KEY);
+    Assertions.assertThat(myRealmConfigEntry.realm()).isEqualTo(MY_REALM_KEY);
+    RealmServiceIdentityConfiguration myRealmConfig = 
myRealmConfigEntry.config();
+    
Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().isPresent()).isTrue();
+    Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user");
+    
Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().accessKeyId())
+        .isEqualTo(Optional.of("access-key-id"));
+    
Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().secretAccessKey())
+        .isEqualTo(Optional.of("secret-access-key"));
+    
Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().sessionToken())
+        .isEqualTo(Optional.of("session-token"));
+
+    // Check the unexisting realm configuration
+    ServiceIdentityConfiguration.RealmConfigEntry otherConfigEntry =
+        serviceIdentityConfiguration.forRealm("other-realm");
+    
Assertions.assertThat(otherConfigEntry.realm()).isEqualTo(DEFAULT_REALM_KEY);
+    RealmServiceIdentityConfiguration otherConfig = otherConfigEntry.config();
+    
Assertions.assertThat(otherConfig.awsIamServiceIdentity().isPresent()).isTrue();
+    Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user");
+    
Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().accessKeyId()).isEmpty();
+    
Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().secretAccessKey()).isEmpty();
+    
Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().sessionToken()).isEmpty();
+  }
+
+  @Test
+  void testAwsIamConfigurationLoading() {
+    // Check the default realm
+    
Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY);
+    DefaultServiceIdentityProvider defaultProvider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    Optional<AwsIamServiceIdentityConfiguration> awsConfig =
+        defaultProvider.getRealmConfig().awsIamServiceIdentity();
+    Assertions.assertThat(awsConfig).isPresent();
+    Assertions.assertThat(awsConfig.get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user");
+    Assertions.assertThat(awsConfig.get().accessKeyId()).isEmpty();
+
+    // Check the my-realm with static credentials
+    Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY);
+    DefaultServiceIdentityProvider myRealmProvider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    awsConfig = myRealmProvider.getRealmConfig().awsIamServiceIdentity();
+    Assertions.assertThat(awsConfig).isPresent();
+    Assertions.assertThat(awsConfig.get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user");
+    
Assertions.assertThat(awsConfig.get().accessKeyId()).isEqualTo(Optional.of("access-key-id"));
+    Assertions.assertThat(awsConfig.get().secretAccessKey())
+        .isEqualTo(Optional.of("secret-access-key"));
+    
Assertions.assertThat(awsConfig.get().sessionToken()).isEqualTo(Optional.of("session-token"));
+
+    // Check the other realm (should fallback to default)
+    Mockito.when(realmContext.getRealmIdentifier()).thenReturn("other-realm");
+    DefaultServiceIdentityProvider otherProvider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    awsConfig = otherProvider.getRealmConfig().awsIamServiceIdentity();
+    Assertions.assertThat(awsConfig).isPresent();
+    Assertions.assertThat(awsConfig.get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user");
+  }
+
+  @Test
+  void testAllocateServiceIdentityWithSigV4Authentication() {
+    // Test allocateServiceIdentity with SigV4 authentication
+    
Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY);
+    DefaultServiceIdentityProvider provider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    ConnectionConfigInfo connectionConfig =
+        IcebergRestConnectionConfigInfo.builder()
+            
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST)
+            .setUri("https://example.com/catalog";)
+            .setAuthenticationParameters(
+                SigV4AuthenticationParameters.builder()
+                    
.setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.SIGV4)
+                    .setRoleArn("arn:aws:iam::123456789012:role/customer-role")
+                    .build())
+            .build();
+
+    Optional<ServiceIdentityInfoDpo> result = 
provider.allocateServiceIdentity(connectionConfig);
+
+    Assertions.assertThat(result).isPresent();
+    ServiceIdentityInfoDpo serviceIdentityDpo = result.get();
+    
Assertions.assertThat(serviceIdentityDpo).isInstanceOf(AwsIamServiceIdentityInfoDpo.class);
+    Assertions.assertThat(serviceIdentityDpo.getIdentityType())
+        .isEqualTo(ServiceIdentityType.AWS_IAM);
+    
Assertions.assertThat(serviceIdentityDpo.getIdentityInfoReference().getUrn())
+        .contains("default-identity-provider");
+  }
+
+  @Test
+  void testAllocateServiceIdentityWithBearerAuthenticationReturnsEmpty() {
+    // Test allocateServiceIdentity with non-SigV4 authentication returns empty
+    
Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY);
+    DefaultServiceIdentityProvider provider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    ConnectionConfigInfo connectionConfig =
+        IcebergRestConnectionConfigInfo.builder()
+            
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST)
+            .setUri("https://example.com/catalog";)
+            .setAuthenticationParameters(
+                BearerAuthenticationParameters.builder()
+                    
.setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.BEARER)
+                    .setBearerToken("some-token")
+                    .build())
+            .build();
+
+    Optional<ServiceIdentityInfoDpo> result = 
provider.allocateServiceIdentity(connectionConfig);
+
+    Assertions.assertThat(result).isEmpty();
+  }
+
+  @Test
+  void testAllocateServiceIdentityWithNullAuthParametersReturnsEmpty() {
+    // Test allocateServiceIdentity with null authentication parameters
+    
Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY);
+    DefaultServiceIdentityProvider provider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    ConnectionConfigInfo connectionConfig =
+        IcebergRestConnectionConfigInfo.builder()
+            
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST)
+            .setUri("https://example.com/catalog";)
+            .build();
+
+    Optional<ServiceIdentityInfoDpo> result = 
provider.allocateServiceIdentity(connectionConfig);
+
+    Assertions.assertThat(result).isEmpty();
+  }
+
+  @Test
+  void testGetServiceIdentityInfoReturnsInfoWithoutCredentials() {
+    // Test getServiceIdentityInfo returns user-facing info without credentials
+    
Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY);
+    DefaultServiceIdentityProvider provider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    ServiceIdentityInfoDpo serviceIdentityDpo =
+        new AwsIamServiceIdentityInfoDpo(
+            new SecretReference(
+                
"urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", 
Map.of()));
+
+    Optional<ServiceIdentityInfo> result = 
provider.getServiceIdentityInfo(serviceIdentityDpo);
+
+    Assertions.assertThat(result).isPresent();
+    ServiceIdentityInfo info = result.get();
+    Assertions.assertThat(info.getIdentityType())
+        .isEqualTo(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM);
+    Assertions.assertThat(info).isInstanceOf(AwsIamServiceIdentityInfo.class);
+    AwsIamServiceIdentityInfo awsInfo = (AwsIamServiceIdentityInfo) info;
+    Assertions.assertThat(awsInfo.getIamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user");
+  }
+
+  @Test
+  void testGetServiceIdentityCredentialReturnsCredentialWithSecrets() {
+    // Test getServiceIdentityCredential returns full credential with secrets
+    Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY);
+    DefaultServiceIdentityProvider provider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    ServiceIdentityInfoDpo serviceIdentityDpo =
+        new AwsIamServiceIdentityInfoDpo(
+            new SecretReference(
+                
"urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM", Map.of()));
+
+    Optional<ServiceIdentityCredential> result =
+        provider.getServiceIdentityCredential(serviceIdentityDpo);
+
+    Assertions.assertThat(result).isPresent();
+    ServiceIdentityCredential credential = result.get();
+    
Assertions.assertThat(credential).isInstanceOf(AwsIamServiceIdentityCredential.class);
+
+    AwsIamServiceIdentityCredential awsCredential = 
(AwsIamServiceIdentityCredential) credential;
+    Assertions.assertThat(awsCredential.getIamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user");
+    Assertions.assertThat(awsCredential.getAwsCredentialsProvider())
+        .isInstanceOf(StaticCredentialsProvider.class);
+
+    // Verify credentials are accessible
+    StaticCredentialsProvider credProvider =
+        (StaticCredentialsProvider) awsCredential.getAwsCredentialsProvider();
+    AwsSessionCredentials creds = (AwsSessionCredentials) 
credProvider.resolveCredentials();
+    Assertions.assertThat(creds.accessKeyId()).isEqualTo("access-key-id");
+    
Assertions.assertThat(creds.secretAccessKey()).isEqualTo("secret-access-key");
+    Assertions.assertThat(creds.sessionToken()).isEqualTo("session-token");
+  }
+
+  @Test
+  void testEmptyProviderAllocateServiceIdentityReturnsEmpty() {
+    // Test that an empty provider returns empty when allocating
+    DefaultServiceIdentityProvider emptyProvider = new 
DefaultServiceIdentityProvider();
+
+    ConnectionConfigInfo connectionConfig =
+        IcebergRestConnectionConfigInfo.builder()
+            
.setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST)
+            .setUri("https://example.com/catalog";)
+            .setAuthenticationParameters(
+                SigV4AuthenticationParameters.builder()
+                    
.setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.SIGV4)
+                    .setRoleArn("arn:aws:iam::123456789012:role/customer-role")
+                    .build())
+            .build();
+
+    Optional<ServiceIdentityInfoDpo> result =
+        emptyProvider.allocateServiceIdentity(connectionConfig);
+
+    Assertions.assertThat(result).isEmpty();
+  }
+
+  @Test
+  void testMultiTenantScenarioDifferentRealmsGetDifferentIdentities() {
+    // Test that different realms have different configurations
+    
Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY);
+    DefaultServiceIdentityProvider defaultProvider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY);
+    DefaultServiceIdentityProvider myRealmProvider =
+        new DefaultServiceIdentityProvider(realmContext, 
serviceIdentityConfiguration);
+
+    // Verify different IAM ARNs from configuration
+    
Assertions.assertThat(defaultProvider.getRealmConfig().awsIamServiceIdentity()).isPresent();
+    
Assertions.assertThat(defaultProvider.getRealmConfig().awsIamServiceIdentity().get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user");
+
+    
Assertions.assertThat(myRealmProvider.getRealmConfig().awsIamServiceIdentity()).isPresent();
+    
Assertions.assertThat(myRealmProvider.getRealmConfig().awsIamServiceIdentity().get().iamArn())
+        .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user");
+
+    // Verify different credential configurations
+    Assertions.assertThat(
+            
defaultProvider.getRealmConfig().awsIamServiceIdentity().get().accessKeyId())
+        .isEmpty();
+    Assertions.assertThat(
+            
myRealmProvider.getRealmConfig().awsIamServiceIdentity().get().accessKeyId())
+        .isEqualTo(Optional.of("access-key-id"));
+  }
+
+  @Test
+  void testBuildIdentityInfoReferenceForDefaultRealm() {
+    // Test URN generation for default realm
+    SecretReference ref =
+        DefaultServiceIdentityProvider.buildIdentityInfoReference(
+            DEFAULT_REALM_KEY, ServiceIdentityType.AWS_IAM);
+
+    Assertions.assertThat(ref.getUrn())
+        
.isEqualTo("urn:polaris-secret:default-identity-provider:system:default:AWS_IAM");
+  }
+
+  @Test
+  void testBuildIdentityInfoReferenceForCustomRealm() {
+    // Test URN generation for custom realm
+    SecretReference ref =
+        DefaultServiceIdentityProvider.buildIdentityInfoReference(
+            "custom-realm", ServiceIdentityType.AWS_IAM);
+
+    Assertions.assertThat(ref.getUrn())
+        
.isEqualTo("urn:polaris-secret:default-identity-provider:custom-realm:AWS_IAM");
+  }
+}
diff --git 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index 044398daf..d118e52e9 100644
--- 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -46,6 +46,7 @@ import org.apache.polaris.core.config.RealmConfig;
 import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.context.RealmContext;
 import org.apache.polaris.core.entity.PrincipalEntity;
+import org.apache.polaris.core.identity.provider.ServiceIdentityProvider;
 import org.apache.polaris.core.persistence.BasePersistence;
 import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
@@ -74,6 +75,7 @@ import 
org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
 import 
org.apache.polaris.service.context.catalog.PolarisCallContextCatalogFactory;
 import org.apache.polaris.service.events.listeners.PolarisEventListener;
 import org.apache.polaris.service.events.listeners.TestPolarisEventListener;
+import 
org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider;
 import 
org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
 import org.apache.polaris.service.secrets.UnsafeInMemorySecretsManagerFactory;
 import 
org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl;
@@ -217,6 +219,7 @@ public record TestServices(
 
       UserSecretsManager userSecretsManager =
           
userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext);
+      ServiceIdentityProvider serviceIdentityProvider = new 
DefaultServiceIdentityProvider();
 
       FileIOFactory fileIOFactory =
           fileIOFactorySupplier.apply(storageCredentialCache, 
metaStoreManagerFactory);
@@ -304,12 +307,14 @@ public record TestServices(
               resolutionManifestFactory,
               metaStoreManager,
               userSecretsManager,
+              serviceIdentityProvider,
               securityContext,
               authorizer,
               reservedProperties);
       PolarisCatalogsApi catalogsApi =
           new PolarisCatalogsApi(
-              new PolarisServiceImpl(realmConfig, reservedProperties, 
adminService));
+              new PolarisServiceImpl(
+                  realmConfig, reservedProperties, adminService, 
serviceIdentityProvider));
 
       return new TestServices(
           clock,


Reply via email to