From e2bd1e10cae1d2b9add7c170c3ce57d452a217cf Mon Sep 17 00:00:00 2001 From: Tatsuya Yano Date: Wed, 9 Apr 2025 10:56:07 +0900 Subject: [PATCH 1/7] Fixed .github/workflows/check-remote.yaml --- .github/workflows/check-remote.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-remote.yaml b/.github/workflows/check-remote.yaml index d1d41d2..8c3de1d 100644 --- a/.github/workflows/check-remote.yaml +++ b/.github/workflows/check-remote.yaml @@ -56,7 +56,7 @@ jobs: | jq -r .[].tag_name \ | grep -E ".*(v[0-9]*.[0-9]*.[0-9]*).*" \ | sed -e 's/.*v\([0-9]*.[0-9]*.[0-9]*\).*/\1/g' \ - | sort -ru \ + | sort -ruV \ | head -n1 )" TAG_VERSION="$( \ @@ -64,7 +64,7 @@ jobs: | jq -r .[].tag_name \ | grep -E ".*(v[0-9]*.[0-9]*.[0-9]*).*" \ | sed -e 's/.*\(v[0-9]*.[0-9]*.[0-9]*\).*/\1/g' \ - | sort -ru \ + | sort -ruV \ | head -n1 )" fi From 64c4bdd4345c47666f635f26f1b174e3fac1d07a Mon Sep 17 00:00:00 2001 From: Tatsuya Yano Date: Wed, 9 Apr 2025 11:02:02 +0900 Subject: [PATCH 2/7] Fixed InstanceJenkinsProvider --- .../instance/provider/impl/InstanceJenkinsProvider.java | 7 +++---- .../provider/impl/InstanceJenkinsProviderTest.java | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProvider.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProvider.java index 21521bb..eed595a 100644 --- a/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProvider.java +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProvider.java @@ -305,16 +305,15 @@ boolean validateTenantDomainToken(final JWTClaimsSet claimsSet, final String dom // we need to extract and generate our action value for the authz check - final String action = "jenkins-pipeline"; + final String action = "run_jenkins_pipeline"; // we need to generate our resource value based on the subject - final String subject = claimsSet.getSubject(); + final String resource = claimsSet.getSubject(); // generate our principal object and carry out authorization check - - final String resource = domainName + ":" + subject; Principal principal = SimplePrincipal.create(domainName, serviceName, (String) null); + boolean accessCheck = authorizer.access(action, resource, principal, null); if (!accessCheck) { errMsg.append("authorization check failed for action: ").append(action) diff --git a/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProviderTest.java b/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProviderTest.java index 052c856..a7efae1 100644 --- a/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProviderTest.java +++ b/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceJenkinsProviderTest.java @@ -119,7 +119,7 @@ public void testConfirmInstance() throws JOSEException, ProviderResourceExceptio Authorizer authorizer = Mockito.mock(Authorizer.class); Principal principal = SimplePrincipal.create("sports", "api", (String) null); - Mockito.when(authorizer.access("jenkins-pipeline", "sports:https://jenkins.athenz.svc.cluster.local/job/test-project/1", principal, null)) + Mockito.when(authorizer.access("run_jenkins_pipeline", "https://jenkins.athenz.svc.cluster.local/job/test-project/1", principal, null)) .thenReturn(true); provider.setAuthorizer(authorizer); @@ -156,7 +156,7 @@ public void testConfirmInstanceFailuresInvalidSANEntries() throws JOSEException Authorizer authorizer = Mockito.mock(Authorizer.class); Principal principal = SimplePrincipal.create("sports", "api", (String) null); - Mockito.when(authorizer.access("jenkins-pipeline", "sports:https://jenkins.athenz.svc.cluster.local/job/test-project/1", principal, null)) + Mockito.when(authorizer.access("run_jenkins_pipeline", "https://jenkins.athenz.svc.cluster.local/job/test-project/1", principal, null)) .thenReturn(true); provider.setAuthorizer(authorizer); From 87d73594fe03f536a7c6b5931d46e5f0d6629b31 Mon Sep 17 00:00:00 2001 From: Tatsuya Yano Date: Wed, 9 Apr 2025 11:07:58 +0900 Subject: [PATCH 3/7] Fixed package description --- pom.xml.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml.template b/pom.xml.template index 902166b..98f36ee 100644 --- a/pom.xml.template +++ b/pom.xml.template @@ -3,7 +3,7 @@ 4.0.0 athenz-plugins - Core Auth Interfaces + Athenz Plugin Implementations io.athenz athenz-plugins 0.0.0 From 65300f9112f5c86844f6cc76771170d5131a6441 Mon Sep 17 00:00:00 2001 From: ctyano Date: Sun, 23 Mar 2025 09:40:03 +0900 Subject: [PATCH 4/7] Added unedited files --- ...tAWSElasticKubernetesServiceValidator.java | 231 ++++++++++++++++++ ...ultGCPGoogleKubernetesEngineValidator.java | 129 ++++++++++ .../provider/impl/DefaultKubernetesValidator | 231 ++++++++++++++++++ ...ubernetesDistributionValidatorFactory.java | 24 ++ 4 files changed, 615 insertions(+) create mode 100644 src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java create mode 100644 src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java create mode 100644 src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator create mode 100644 src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java new file mode 100644 index 0000000..0ae6613 --- /dev/null +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java @@ -0,0 +1,231 @@ +package com.yahoo.athenz.instance.provider.impl; + +import com.yahoo.athenz.auth.Authorizer; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.impl.SimplePrincipal; +import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigBoolean; +import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigCsv; +import com.yahoo.athenz.instance.provider.AttrValidator; +import com.yahoo.athenz.instance.provider.AttrValidatorFactory; +import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import org.eclipse.jetty.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import java.lang.invoke.MethodHandles; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.iam.model.ListOpenIdConnectProvidersRequest; +import software.amazon.awssdk.services.iam.model.ListOpenIdConnectProvidersResponse; +import software.amazon.awssdk.services.iam.model.OpenIDConnectProviderListEntry; +import software.amazon.awssdk.services.iam.IamClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; + +import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER; +import static com.yahoo.athenz.instance.provider.InstanceProvider.ZTS_INSTANCE_AWS_ACCOUNT; +import static com.yahoo.athenz.instance.provider.impl.InstanceAWSProvider.*; + +public class DefaultAWSElasticKubernetesServiceValidator extends CommonKubernetesDistributionValidator { + + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final DefaultAWSElasticKubernetesServiceValidator INSTANCE = new DefaultAWSElasticKubernetesServiceValidator(); + static final String AWS_EKS_OIDC_ISSUER_REGEX = "oidc\\.eks\\.[a-z0-9-]+\\.amazonaws\\.com"; + private static final Pattern AWS_EKS_OIDC_ISSUER_PATTERN = Pattern.compile(AWS_EKS_OIDC_ISSUER_REGEX); + + private static final String ZTS_PROP_K8S_PROVIDER_ATTESTATION_AWS_ASSUME_ROLE_NAME = "athenz.zts.k8s_provider_attestation_aws_assume_role_name"; + private static final String ASSUME_ROLE_NAME = System.getProperty(ZTS_PROP_K8S_PROVIDER_ATTESTATION_AWS_ASSUME_ROLE_NAME, "oidc-issuers-reader"); + static final String ZTS_PROP_K8S_PROVIDER_AWS_ATTR_VALIDATOR_FACTORY_CLASS = "athenz.zts.k8s_provider_aws_attr_validator_factory_class"; + + StsClient stsClient; + String serverRegion; + + Set awsDNSSuffixes = new HashSet<>(); + List eksDnsSuffixes; + DynamicConfigCsv eksClusterNames; // list of eks cluster names + + private static final String ZTS_PROP_K8S_PROVIDER_AWS_ATTESTATION_USING_IAM_ROLE = "athenz.zts.k8s_provider_aws_attestation_using_iam_role"; + DynamicConfigBoolean useIamRoleForIssuerAttestation; + AttrValidator attrValidator; + + public static DefaultAWSElasticKubernetesServiceValidator getInstance() { + return INSTANCE; + } + + private DefaultAWSElasticKubernetesServiceValidator() { + } + + static AttrValidator newAttrValidator(final SSLContext sslContext) { + final String factoryClass = System.getProperty(ZTS_PROP_K8S_PROVIDER_AWS_ATTR_VALIDATOR_FACTORY_CLASS); + LOGGER.info("AWS K8S AttributeValidatorFactory class: {}", factoryClass); + if (factoryClass == null) { + return null; + } + + AttrValidatorFactory attrValidatorFactory; + try { + attrValidatorFactory = (AttrValidatorFactory) Class.forName(factoryClass).getConstructor().newInstance(); + } catch (Exception e) { + LOGGER.error("Invalid AttributeValidatorFactory class: {}", factoryClass, e); + throw new IllegalArgumentException("Invalid AttributeValidatorFactory class"); + } + + return attrValidatorFactory.create(sslContext); + } + + @Override + public void initialize(final SSLContext sslContext, Authorizer authorizer) { + super.initialize(sslContext, authorizer); + serverRegion = System.getProperty(AWS_PROP_REGION_NAME); + + useIamRoleForIssuerAttestation = new DynamicConfigBoolean(CONFIG_MANAGER, ZTS_PROP_K8S_PROVIDER_AWS_ATTESTATION_USING_IAM_ROLE, true); + + if (useIamRoleForIssuerValidation()) { + // Create an STS client using default credentials + stsClient = StsClient.builder().credentialsProvider(DefaultCredentialsProvider.builder().build()) + .region(Region.of(serverRegion)).build(); + } + final String dnsSuffix = System.getProperty(AWS_PROP_DNS_SUFFIX); + if (!StringUtil.isEmpty(dnsSuffix)) { + awsDNSSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); + } + // get our allowed eks dns suffixes + eksDnsSuffixes = InstanceUtils.processK8SDnsSuffixList(AWS_PROP_EKS_DNS_SUFFIX); + // get our dynamic list of eks cluster names + eksClusterNames = new DynamicConfigCsv(CONFIG_MANAGER, AWS_PROP_EKS_CLUSTER_NAMES, null); + + this.attrValidator = newAttrValidator(sslContext); + } + + @Override + public String validateIssuer(InstanceConfirmation confirmation, IdTokenAttestationData attestationData, StringBuilder errMsg) { + + String issuer = getIssuerFromToken(attestationData, errMsg); + if (StringUtil.isEmpty(issuer)) { + return null; + } + + String issuerDomain = InstanceUtils.extractURLDomainName(issuer); + if (issuerDomain == null) { + return null; + } + Matcher matcher = AWS_EKS_OIDC_ISSUER_PATTERN.matcher(issuerDomain); + if (!matcher.matches()) { + return null; + } + + if (useIamRoleForIssuerValidation()) { + String awsAccount = confirmation.getAttributes().get(ZTS_INSTANCE_AWS_ACCOUNT); + if (!verifyIssuerPresenceInDomainAWSAccount(issuer, awsAccount)) { + return null; + } + // If the issuer is present in the same AWS account as the requested identity + // then we should use the same for the launch authorization + confirmation.getAttributes().put(ZTS_INSTANCE_ISSUER_AWS_ACCOUNT, awsAccount); + } else { + if (attrValidator != null) { + confirmation.getAttributes().put(ZTS_INSTANCE_UNATTESTED_ISSUER, issuer); + // Confirm the issuer as per the attribute validator + if (!attrValidator.confirm(confirmation)) { + return null; + } + } + } + + final String domainName = confirmation.getDomain(); + final String serviceName = confirmation.getService(); + // attribute set after iam role validation or attribute validation + final String issuerAwsAccount = confirmation.getAttributes().get(ZTS_INSTANCE_ISSUER_AWS_ACCOUNT); + final String resource = String.format("%s:%s:%s", domainName, serviceName, issuerAwsAccount); + + Principal principal = SimplePrincipal.create(domainName, serviceName, (String) null); + boolean accessCheck = authorizer.access(ACTION_LAUNCH, resource, principal, null); + if (!accessCheck) { + errMsg.append("eks launch authorization check failed for action: ").append(ACTION_LAUNCH) + .append(" resource: ").append(resource); + return null; + } + + return issuer; + } + + IamClient getIamClient(final String awsAccount) { + + final String roleArn = String.format("arn:aws:iam::%s:role/%s", awsAccount, ASSUME_ROLE_NAME); + final String roleSessionName = ASSUME_ROLE_NAME + "-Session"; + + // Assume the role in the target AWS account + + AssumeRoleRequest assumeRoleRequest = AssumeRoleRequest.builder() + .roleArn(roleArn).roleSessionName(roleSessionName).build(); + AssumeRoleResponse assumeRoleResponse = stsClient.assumeRole(assumeRoleRequest); + + // Create Static Credentials Provider + + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsSessionCredentials.create(assumeRoleResponse.credentials().accessKeyId(), + assumeRoleResponse.credentials().secretAccessKey(), + assumeRoleResponse.credentials().sessionToken())); + + // Create IAM Client + + return IamClient.builder().credentialsProvider(credentialsProvider).region(Region.of(serverRegion)).build(); + } + + boolean verifyIssuerPresenceInDomainAWSAccount(final String issuer, final String awsAccount) { + + boolean result = false; + + // get our IAM Client + + IamClient iamClient = getIamClient(awsAccount); + + // Call the IAM API to get the list of OIDC issuers + + ListOpenIdConnectProvidersRequest request = ListOpenIdConnectProvidersRequest.builder().build(); + ListOpenIdConnectProvidersResponse response = iamClient.listOpenIDConnectProviders(request); + List oidcIssuers = response.openIDConnectProviderList(); + if (oidcIssuers != null) { + String issuerWithoutProtocol = issuer.replaceFirst("^https://", ""); + for (OpenIDConnectProviderListEntry oidcIssuer : oidcIssuers) { + if (oidcIssuer != null && oidcIssuer.arn() != null && oidcIssuer.arn().endsWith(issuerWithoutProtocol)) { + result = true; + break; + } + } + } + return result; + } + + @Override + public boolean validateSanDNSEntries(InstanceConfirmation confirmation, StringBuilder errMsg) { + + StringBuilder instanceId = new StringBuilder(256); + final Map instanceAttributes = confirmation.getAttributes(); + final String awsAccount = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_AWS_ACCOUNT); + if (StringUtil.isEmpty(awsAccount)) { + errMsg.append("Unable to find AWS account number"); + return false; + } + if (!InstanceUtils.validateCertRequestSanDnsNames(instanceAttributes, confirmation.getDomain(), + confirmation.getService(), awsDNSSuffixes, eksDnsSuffixes, eksClusterNames.getStringsList(), + true, instanceId, null)) { + errMsg.append("Unable to validate certificate request hostnames"); + return false; + } + return true; + } + + boolean useIamRoleForIssuerValidation() { + return Boolean.TRUE.equals(useIamRoleForIssuerAttestation.get()); + } +} diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java new file mode 100644 index 0000000..fe8dd37 --- /dev/null +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java @@ -0,0 +1,129 @@ +package com.yahoo.athenz.instance.provider.impl; + +import com.yahoo.athenz.auth.Authorizer; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.impl.SimplePrincipal; +import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigCsv; +import com.yahoo.athenz.instance.provider.AttrValidator; +import com.yahoo.athenz.instance.provider.AttrValidatorFactory; +import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import com.yahoo.athenz.instance.provider.InstanceProvider; +import javax.net.ssl.SSLContext; +import org.eclipse.jetty.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.invoke.MethodHandles; +import java.util.*; + +import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER; +import static com.yahoo.athenz.instance.provider.impl.InstanceGCPProvider.*; + +public class DefaultGCPGoogleKubernetesEngineValidator extends CommonKubernetesDistributionValidator { + + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + Set gcpDNSSuffixes = new HashSet<>(); + List gkeDnsSuffixes; + DynamicConfigCsv gkeClusterNames; + + private static final DefaultGCPGoogleKubernetesEngineValidator INSTANCE = new DefaultGCPGoogleKubernetesEngineValidator(); + static final String GCP_OIDC_ISSUER_PREFIX = "https://container.googleapis.com/v1/projects/"; + AttrValidator attrValidator; + static final String ZTS_PROP_K8S_PROVIDER_GCP_ATTR_VALIDATOR_FACTORY_CLASS = "athenz.zts.k8s_provider_gcp_attr_validator_factory_class"; + + public static DefaultGCPGoogleKubernetesEngineValidator getInstance() { + return INSTANCE; + } + private DefaultGCPGoogleKubernetesEngineValidator() { + } + + static AttrValidator newAttrValidator(final SSLContext sslContext) { + final String factoryClass = System.getProperty(ZTS_PROP_K8S_PROVIDER_GCP_ATTR_VALIDATOR_FACTORY_CLASS); + LOGGER.info("GCP K8S AttributeValidatorFactory class: {}", factoryClass); + if (factoryClass == null) { + return null; + } + + AttrValidatorFactory attrValidatorFactory; + try { + attrValidatorFactory = (AttrValidatorFactory) Class.forName(factoryClass).getConstructor().newInstance(); + } catch (Exception e) { + LOGGER.error("Invalid AttributeValidatorFactory class: {}", factoryClass, e); + throw new IllegalArgumentException("Invalid AttributeValidatorFactory class"); + } + + return attrValidatorFactory.create(sslContext); + } + + @Override + public void initialize(final SSLContext sslContext, Authorizer authorizer) { + super.initialize(sslContext, authorizer); + final String dnsSuffix = System.getProperty(GCP_PROP_DNS_SUFFIX); + if (!StringUtil.isEmpty(dnsSuffix)) { + gcpDNSSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); + } + // get our allowed gke dns suffixes + gkeDnsSuffixes = InstanceUtils.processK8SDnsSuffixList(GCP_PROP_GKE_DNS_SUFFIX); + // get our dynamic list of gke cluster names + gkeClusterNames = new DynamicConfigCsv(CONFIG_MANAGER, GCP_PROP_GKE_CLUSTER_NAMES, null); + this.attrValidator = newAttrValidator(sslContext); + } + + @Override + public String validateIssuer(InstanceConfirmation confirmation, IdTokenAttestationData attestationData, StringBuilder errMsg) { + String issuer = getIssuerFromToken(attestationData, errMsg); + if (StringUtil.isEmpty(issuer)) { + return null; + } + String gcpProject = confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_GCP_PROJECT); + if (!issuer.startsWith(GCP_OIDC_ISSUER_PREFIX + gcpProject)) { + // could be a multi-tenant setup where the issuer is not present in the identity's GCP project + if (attrValidator != null) { + confirmation.getAttributes().put(ZTS_INSTANCE_UNATTESTED_ISSUER, issuer); + // Confirm the issuer as per the attribute validator + if (!attrValidator.confirm(confirmation)) { + return null; + } + } else { + errMsg.append("Issuer is not present in the GCP project associated with the domain"); + return null; + } + } else { + // issuer exists in the same GCP project as the requested identity + confirmation.getAttributes().put(ZTS_INSTANCE_ISSUER_GCP_PROJECT, gcpProject); + } + + final String domainName = confirmation.getDomain(); + final String serviceName = confirmation.getService(); + // attribute set after verification above or attribute validation + final String issuerGcpProject = confirmation.getAttributes().get(ZTS_INSTANCE_ISSUER_GCP_PROJECT); + final String resource = String.format("%s:%s:%s", domainName, serviceName, issuerGcpProject); + + Principal principal = SimplePrincipal.create(domainName, serviceName, (String) null); + boolean accessCheck = authorizer.access(ACTION_LAUNCH, resource, principal, null); + if (!accessCheck) { + errMsg.append("gke launch authorization check failed for action: ").append(ACTION_LAUNCH) + .append(" resource: ").append(resource); + return null; + } + return issuer; + } + + @Override + public boolean validateSanDNSEntries(InstanceConfirmation confirmation, StringBuilder errMsg) { + StringBuilder instanceId = new StringBuilder(256); + final Map instanceAttributes = confirmation.getAttributes(); + final String gcpProject = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_GCP_PROJECT); + if (StringUtil.isEmpty(gcpProject)) { + errMsg.append("Unable to find GCP project id"); + return false; + } + if (!InstanceUtils.validateCertRequestSanDnsNames(instanceAttributes, confirmation.getDomain(), + confirmation.getService(), gcpDNSSuffixes, gkeDnsSuffixes, gkeClusterNames.getStringsList(), + true, instanceId, null)) { + errMsg.append("Unable to validate certificate request hostnames"); + return false; + } + return true; + } +} diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator new file mode 100644 index 0000000..0ae6613 --- /dev/null +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator @@ -0,0 +1,231 @@ +package com.yahoo.athenz.instance.provider.impl; + +import com.yahoo.athenz.auth.Authorizer; +import com.yahoo.athenz.auth.Principal; +import com.yahoo.athenz.auth.impl.SimplePrincipal; +import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigBoolean; +import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigCsv; +import com.yahoo.athenz.instance.provider.AttrValidator; +import com.yahoo.athenz.instance.provider.AttrValidatorFactory; +import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import org.eclipse.jetty.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import java.lang.invoke.MethodHandles; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.iam.model.ListOpenIdConnectProvidersRequest; +import software.amazon.awssdk.services.iam.model.ListOpenIdConnectProvidersResponse; +import software.amazon.awssdk.services.iam.model.OpenIDConnectProviderListEntry; +import software.amazon.awssdk.services.iam.IamClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; + +import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER; +import static com.yahoo.athenz.instance.provider.InstanceProvider.ZTS_INSTANCE_AWS_ACCOUNT; +import static com.yahoo.athenz.instance.provider.impl.InstanceAWSProvider.*; + +public class DefaultAWSElasticKubernetesServiceValidator extends CommonKubernetesDistributionValidator { + + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final DefaultAWSElasticKubernetesServiceValidator INSTANCE = new DefaultAWSElasticKubernetesServiceValidator(); + static final String AWS_EKS_OIDC_ISSUER_REGEX = "oidc\\.eks\\.[a-z0-9-]+\\.amazonaws\\.com"; + private static final Pattern AWS_EKS_OIDC_ISSUER_PATTERN = Pattern.compile(AWS_EKS_OIDC_ISSUER_REGEX); + + private static final String ZTS_PROP_K8S_PROVIDER_ATTESTATION_AWS_ASSUME_ROLE_NAME = "athenz.zts.k8s_provider_attestation_aws_assume_role_name"; + private static final String ASSUME_ROLE_NAME = System.getProperty(ZTS_PROP_K8S_PROVIDER_ATTESTATION_AWS_ASSUME_ROLE_NAME, "oidc-issuers-reader"); + static final String ZTS_PROP_K8S_PROVIDER_AWS_ATTR_VALIDATOR_FACTORY_CLASS = "athenz.zts.k8s_provider_aws_attr_validator_factory_class"; + + StsClient stsClient; + String serverRegion; + + Set awsDNSSuffixes = new HashSet<>(); + List eksDnsSuffixes; + DynamicConfigCsv eksClusterNames; // list of eks cluster names + + private static final String ZTS_PROP_K8S_PROVIDER_AWS_ATTESTATION_USING_IAM_ROLE = "athenz.zts.k8s_provider_aws_attestation_using_iam_role"; + DynamicConfigBoolean useIamRoleForIssuerAttestation; + AttrValidator attrValidator; + + public static DefaultAWSElasticKubernetesServiceValidator getInstance() { + return INSTANCE; + } + + private DefaultAWSElasticKubernetesServiceValidator() { + } + + static AttrValidator newAttrValidator(final SSLContext sslContext) { + final String factoryClass = System.getProperty(ZTS_PROP_K8S_PROVIDER_AWS_ATTR_VALIDATOR_FACTORY_CLASS); + LOGGER.info("AWS K8S AttributeValidatorFactory class: {}", factoryClass); + if (factoryClass == null) { + return null; + } + + AttrValidatorFactory attrValidatorFactory; + try { + attrValidatorFactory = (AttrValidatorFactory) Class.forName(factoryClass).getConstructor().newInstance(); + } catch (Exception e) { + LOGGER.error("Invalid AttributeValidatorFactory class: {}", factoryClass, e); + throw new IllegalArgumentException("Invalid AttributeValidatorFactory class"); + } + + return attrValidatorFactory.create(sslContext); + } + + @Override + public void initialize(final SSLContext sslContext, Authorizer authorizer) { + super.initialize(sslContext, authorizer); + serverRegion = System.getProperty(AWS_PROP_REGION_NAME); + + useIamRoleForIssuerAttestation = new DynamicConfigBoolean(CONFIG_MANAGER, ZTS_PROP_K8S_PROVIDER_AWS_ATTESTATION_USING_IAM_ROLE, true); + + if (useIamRoleForIssuerValidation()) { + // Create an STS client using default credentials + stsClient = StsClient.builder().credentialsProvider(DefaultCredentialsProvider.builder().build()) + .region(Region.of(serverRegion)).build(); + } + final String dnsSuffix = System.getProperty(AWS_PROP_DNS_SUFFIX); + if (!StringUtil.isEmpty(dnsSuffix)) { + awsDNSSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); + } + // get our allowed eks dns suffixes + eksDnsSuffixes = InstanceUtils.processK8SDnsSuffixList(AWS_PROP_EKS_DNS_SUFFIX); + // get our dynamic list of eks cluster names + eksClusterNames = new DynamicConfigCsv(CONFIG_MANAGER, AWS_PROP_EKS_CLUSTER_NAMES, null); + + this.attrValidator = newAttrValidator(sslContext); + } + + @Override + public String validateIssuer(InstanceConfirmation confirmation, IdTokenAttestationData attestationData, StringBuilder errMsg) { + + String issuer = getIssuerFromToken(attestationData, errMsg); + if (StringUtil.isEmpty(issuer)) { + return null; + } + + String issuerDomain = InstanceUtils.extractURLDomainName(issuer); + if (issuerDomain == null) { + return null; + } + Matcher matcher = AWS_EKS_OIDC_ISSUER_PATTERN.matcher(issuerDomain); + if (!matcher.matches()) { + return null; + } + + if (useIamRoleForIssuerValidation()) { + String awsAccount = confirmation.getAttributes().get(ZTS_INSTANCE_AWS_ACCOUNT); + if (!verifyIssuerPresenceInDomainAWSAccount(issuer, awsAccount)) { + return null; + } + // If the issuer is present in the same AWS account as the requested identity + // then we should use the same for the launch authorization + confirmation.getAttributes().put(ZTS_INSTANCE_ISSUER_AWS_ACCOUNT, awsAccount); + } else { + if (attrValidator != null) { + confirmation.getAttributes().put(ZTS_INSTANCE_UNATTESTED_ISSUER, issuer); + // Confirm the issuer as per the attribute validator + if (!attrValidator.confirm(confirmation)) { + return null; + } + } + } + + final String domainName = confirmation.getDomain(); + final String serviceName = confirmation.getService(); + // attribute set after iam role validation or attribute validation + final String issuerAwsAccount = confirmation.getAttributes().get(ZTS_INSTANCE_ISSUER_AWS_ACCOUNT); + final String resource = String.format("%s:%s:%s", domainName, serviceName, issuerAwsAccount); + + Principal principal = SimplePrincipal.create(domainName, serviceName, (String) null); + boolean accessCheck = authorizer.access(ACTION_LAUNCH, resource, principal, null); + if (!accessCheck) { + errMsg.append("eks launch authorization check failed for action: ").append(ACTION_LAUNCH) + .append(" resource: ").append(resource); + return null; + } + + return issuer; + } + + IamClient getIamClient(final String awsAccount) { + + final String roleArn = String.format("arn:aws:iam::%s:role/%s", awsAccount, ASSUME_ROLE_NAME); + final String roleSessionName = ASSUME_ROLE_NAME + "-Session"; + + // Assume the role in the target AWS account + + AssumeRoleRequest assumeRoleRequest = AssumeRoleRequest.builder() + .roleArn(roleArn).roleSessionName(roleSessionName).build(); + AssumeRoleResponse assumeRoleResponse = stsClient.assumeRole(assumeRoleRequest); + + // Create Static Credentials Provider + + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsSessionCredentials.create(assumeRoleResponse.credentials().accessKeyId(), + assumeRoleResponse.credentials().secretAccessKey(), + assumeRoleResponse.credentials().sessionToken())); + + // Create IAM Client + + return IamClient.builder().credentialsProvider(credentialsProvider).region(Region.of(serverRegion)).build(); + } + + boolean verifyIssuerPresenceInDomainAWSAccount(final String issuer, final String awsAccount) { + + boolean result = false; + + // get our IAM Client + + IamClient iamClient = getIamClient(awsAccount); + + // Call the IAM API to get the list of OIDC issuers + + ListOpenIdConnectProvidersRequest request = ListOpenIdConnectProvidersRequest.builder().build(); + ListOpenIdConnectProvidersResponse response = iamClient.listOpenIDConnectProviders(request); + List oidcIssuers = response.openIDConnectProviderList(); + if (oidcIssuers != null) { + String issuerWithoutProtocol = issuer.replaceFirst("^https://", ""); + for (OpenIDConnectProviderListEntry oidcIssuer : oidcIssuers) { + if (oidcIssuer != null && oidcIssuer.arn() != null && oidcIssuer.arn().endsWith(issuerWithoutProtocol)) { + result = true; + break; + } + } + } + return result; + } + + @Override + public boolean validateSanDNSEntries(InstanceConfirmation confirmation, StringBuilder errMsg) { + + StringBuilder instanceId = new StringBuilder(256); + final Map instanceAttributes = confirmation.getAttributes(); + final String awsAccount = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_AWS_ACCOUNT); + if (StringUtil.isEmpty(awsAccount)) { + errMsg.append("Unable to find AWS account number"); + return false; + } + if (!InstanceUtils.validateCertRequestSanDnsNames(instanceAttributes, confirmation.getDomain(), + confirmation.getService(), awsDNSSuffixes, eksDnsSuffixes, eksClusterNames.getStringsList(), + true, instanceId, null)) { + errMsg.append("Unable to validate certificate request hostnames"); + return false; + } + return true; + } + + boolean useIamRoleForIssuerValidation() { + return Boolean.TRUE.equals(useIamRoleForIssuerAttestation.get()); + } +} diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java new file mode 100644 index 0000000..bf46565 --- /dev/null +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java @@ -0,0 +1,24 @@ +package com.yahoo.athenz.instance.provider.impl; + +import com.yahoo.athenz.instance.provider.KubernetesDistributionValidator; +import com.yahoo.athenz.instance.provider.KubernetesDistributionValidatorFactory; + +import java.util.HashMap; +import java.util.Map; + +public class DefaultKubernetesDistributionValidatorFactory implements KubernetesDistributionValidatorFactory { + + Map supportedDistributionsMap = new HashMap<>(); + static final String CLOUD_AWS = "aws"; + static final String CLOUD_GCP = "gcp"; + @Override + public void initialize() { + supportedDistributionsMap.put(CLOUD_AWS, DefaultAWSElasticKubernetesServiceValidator.getInstance()); + supportedDistributionsMap.put(CLOUD_GCP, DefaultGCPGoogleKubernetesEngineValidator.getInstance()); + } + + @Override + public Map getSupportedDistributions() { + return supportedDistributionsMap; + } +} \ No newline at end of file From 080f6fe1bbe910c4947d6c7be3d5ad7e8ee14b54 Mon Sep 17 00:00:00 2001 From: ctyano Date: Sun, 23 Mar 2025 09:47:09 +0900 Subject: [PATCH 5/7] Diff between EKS and GKE --- .../provider/impl/DefaultKubernetesValidator | 176 ++++-------------- 1 file changed, 37 insertions(+), 139 deletions(-) diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator index 0ae6613..fe8dd37 100644 --- a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator @@ -3,70 +3,43 @@ package com.yahoo.athenz.instance.provider.impl; import com.yahoo.athenz.auth.Authorizer; import com.yahoo.athenz.auth.Principal; import com.yahoo.athenz.auth.impl.SimplePrincipal; -import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigBoolean; import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigCsv; import com.yahoo.athenz.instance.provider.AttrValidator; import com.yahoo.athenz.instance.provider.AttrValidatorFactory; import com.yahoo.athenz.instance.provider.InstanceConfirmation; +import com.yahoo.athenz.instance.provider.InstanceProvider; +import javax.net.ssl.SSLContext; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLContext; import java.lang.invoke.MethodHandles; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.sts.StsClient; -import software.amazon.awssdk.services.iam.model.ListOpenIdConnectProvidersRequest; -import software.amazon.awssdk.services.iam.model.ListOpenIdConnectProvidersResponse; -import software.amazon.awssdk.services.iam.model.OpenIDConnectProviderListEntry; -import software.amazon.awssdk.services.iam.IamClient; -import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; -import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER; -import static com.yahoo.athenz.instance.provider.InstanceProvider.ZTS_INSTANCE_AWS_ACCOUNT; -import static com.yahoo.athenz.instance.provider.impl.InstanceAWSProvider.*; +import static com.yahoo.athenz.instance.provider.impl.InstanceGCPProvider.*; -public class DefaultAWSElasticKubernetesServiceValidator extends CommonKubernetesDistributionValidator { +public class DefaultGCPGoogleKubernetesEngineValidator extends CommonKubernetesDistributionValidator { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + Set gcpDNSSuffixes = new HashSet<>(); + List gkeDnsSuffixes; + DynamicConfigCsv gkeClusterNames; - private static final DefaultAWSElasticKubernetesServiceValidator INSTANCE = new DefaultAWSElasticKubernetesServiceValidator(); - static final String AWS_EKS_OIDC_ISSUER_REGEX = "oidc\\.eks\\.[a-z0-9-]+\\.amazonaws\\.com"; - private static final Pattern AWS_EKS_OIDC_ISSUER_PATTERN = Pattern.compile(AWS_EKS_OIDC_ISSUER_REGEX); - - private static final String ZTS_PROP_K8S_PROVIDER_ATTESTATION_AWS_ASSUME_ROLE_NAME = "athenz.zts.k8s_provider_attestation_aws_assume_role_name"; - private static final String ASSUME_ROLE_NAME = System.getProperty(ZTS_PROP_K8S_PROVIDER_ATTESTATION_AWS_ASSUME_ROLE_NAME, "oidc-issuers-reader"); - static final String ZTS_PROP_K8S_PROVIDER_AWS_ATTR_VALIDATOR_FACTORY_CLASS = "athenz.zts.k8s_provider_aws_attr_validator_factory_class"; - - StsClient stsClient; - String serverRegion; - - Set awsDNSSuffixes = new HashSet<>(); - List eksDnsSuffixes; - DynamicConfigCsv eksClusterNames; // list of eks cluster names - - private static final String ZTS_PROP_K8S_PROVIDER_AWS_ATTESTATION_USING_IAM_ROLE = "athenz.zts.k8s_provider_aws_attestation_using_iam_role"; - DynamicConfigBoolean useIamRoleForIssuerAttestation; + private static final DefaultGCPGoogleKubernetesEngineValidator INSTANCE = new DefaultGCPGoogleKubernetesEngineValidator(); + static final String GCP_OIDC_ISSUER_PREFIX = "https://container.googleapis.com/v1/projects/"; AttrValidator attrValidator; + static final String ZTS_PROP_K8S_PROVIDER_GCP_ATTR_VALIDATOR_FACTORY_CLASS = "athenz.zts.k8s_provider_gcp_attr_validator_factory_class"; - public static DefaultAWSElasticKubernetesServiceValidator getInstance() { + public static DefaultGCPGoogleKubernetesEngineValidator getInstance() { return INSTANCE; } - - private DefaultAWSElasticKubernetesServiceValidator() { + private DefaultGCPGoogleKubernetesEngineValidator() { } static AttrValidator newAttrValidator(final SSLContext sslContext) { - final String factoryClass = System.getProperty(ZTS_PROP_K8S_PROVIDER_AWS_ATTR_VALIDATOR_FACTORY_CLASS); - LOGGER.info("AWS K8S AttributeValidatorFactory class: {}", factoryClass); + final String factoryClass = System.getProperty(ZTS_PROP_K8S_PROVIDER_GCP_ATTR_VALIDATOR_FACTORY_CLASS); + LOGGER.info("GCP K8S AttributeValidatorFactory class: {}", factoryClass); if (factoryClass == null) { return null; } @@ -85,147 +58,72 @@ public class DefaultAWSElasticKubernetesServiceValidator extends CommonKubernete @Override public void initialize(final SSLContext sslContext, Authorizer authorizer) { super.initialize(sslContext, authorizer); - serverRegion = System.getProperty(AWS_PROP_REGION_NAME); - - useIamRoleForIssuerAttestation = new DynamicConfigBoolean(CONFIG_MANAGER, ZTS_PROP_K8S_PROVIDER_AWS_ATTESTATION_USING_IAM_ROLE, true); - - if (useIamRoleForIssuerValidation()) { - // Create an STS client using default credentials - stsClient = StsClient.builder().credentialsProvider(DefaultCredentialsProvider.builder().build()) - .region(Region.of(serverRegion)).build(); - } - final String dnsSuffix = System.getProperty(AWS_PROP_DNS_SUFFIX); + final String dnsSuffix = System.getProperty(GCP_PROP_DNS_SUFFIX); if (!StringUtil.isEmpty(dnsSuffix)) { - awsDNSSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); + gcpDNSSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); } - // get our allowed eks dns suffixes - eksDnsSuffixes = InstanceUtils.processK8SDnsSuffixList(AWS_PROP_EKS_DNS_SUFFIX); - // get our dynamic list of eks cluster names - eksClusterNames = new DynamicConfigCsv(CONFIG_MANAGER, AWS_PROP_EKS_CLUSTER_NAMES, null); - + // get our allowed gke dns suffixes + gkeDnsSuffixes = InstanceUtils.processK8SDnsSuffixList(GCP_PROP_GKE_DNS_SUFFIX); + // get our dynamic list of gke cluster names + gkeClusterNames = new DynamicConfigCsv(CONFIG_MANAGER, GCP_PROP_GKE_CLUSTER_NAMES, null); this.attrValidator = newAttrValidator(sslContext); } @Override public String validateIssuer(InstanceConfirmation confirmation, IdTokenAttestationData attestationData, StringBuilder errMsg) { - String issuer = getIssuerFromToken(attestationData, errMsg); if (StringUtil.isEmpty(issuer)) { return null; } - - String issuerDomain = InstanceUtils.extractURLDomainName(issuer); - if (issuerDomain == null) { - return null; - } - Matcher matcher = AWS_EKS_OIDC_ISSUER_PATTERN.matcher(issuerDomain); - if (!matcher.matches()) { - return null; - } - - if (useIamRoleForIssuerValidation()) { - String awsAccount = confirmation.getAttributes().get(ZTS_INSTANCE_AWS_ACCOUNT); - if (!verifyIssuerPresenceInDomainAWSAccount(issuer, awsAccount)) { - return null; - } - // If the issuer is present in the same AWS account as the requested identity - // then we should use the same for the launch authorization - confirmation.getAttributes().put(ZTS_INSTANCE_ISSUER_AWS_ACCOUNT, awsAccount); - } else { + String gcpProject = confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_GCP_PROJECT); + if (!issuer.startsWith(GCP_OIDC_ISSUER_PREFIX + gcpProject)) { + // could be a multi-tenant setup where the issuer is not present in the identity's GCP project if (attrValidator != null) { confirmation.getAttributes().put(ZTS_INSTANCE_UNATTESTED_ISSUER, issuer); // Confirm the issuer as per the attribute validator if (!attrValidator.confirm(confirmation)) { return null; } + } else { + errMsg.append("Issuer is not present in the GCP project associated with the domain"); + return null; } + } else { + // issuer exists in the same GCP project as the requested identity + confirmation.getAttributes().put(ZTS_INSTANCE_ISSUER_GCP_PROJECT, gcpProject); } final String domainName = confirmation.getDomain(); final String serviceName = confirmation.getService(); - // attribute set after iam role validation or attribute validation - final String issuerAwsAccount = confirmation.getAttributes().get(ZTS_INSTANCE_ISSUER_AWS_ACCOUNT); - final String resource = String.format("%s:%s:%s", domainName, serviceName, issuerAwsAccount); + // attribute set after verification above or attribute validation + final String issuerGcpProject = confirmation.getAttributes().get(ZTS_INSTANCE_ISSUER_GCP_PROJECT); + final String resource = String.format("%s:%s:%s", domainName, serviceName, issuerGcpProject); Principal principal = SimplePrincipal.create(domainName, serviceName, (String) null); boolean accessCheck = authorizer.access(ACTION_LAUNCH, resource, principal, null); if (!accessCheck) { - errMsg.append("eks launch authorization check failed for action: ").append(ACTION_LAUNCH) + errMsg.append("gke launch authorization check failed for action: ").append(ACTION_LAUNCH) .append(" resource: ").append(resource); return null; } - return issuer; } - IamClient getIamClient(final String awsAccount) { - - final String roleArn = String.format("arn:aws:iam::%s:role/%s", awsAccount, ASSUME_ROLE_NAME); - final String roleSessionName = ASSUME_ROLE_NAME + "-Session"; - - // Assume the role in the target AWS account - - AssumeRoleRequest assumeRoleRequest = AssumeRoleRequest.builder() - .roleArn(roleArn).roleSessionName(roleSessionName).build(); - AssumeRoleResponse assumeRoleResponse = stsClient.assumeRole(assumeRoleRequest); - - // Create Static Credentials Provider - - StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( - AwsSessionCredentials.create(assumeRoleResponse.credentials().accessKeyId(), - assumeRoleResponse.credentials().secretAccessKey(), - assumeRoleResponse.credentials().sessionToken())); - - // Create IAM Client - - return IamClient.builder().credentialsProvider(credentialsProvider).region(Region.of(serverRegion)).build(); - } - - boolean verifyIssuerPresenceInDomainAWSAccount(final String issuer, final String awsAccount) { - - boolean result = false; - - // get our IAM Client - - IamClient iamClient = getIamClient(awsAccount); - - // Call the IAM API to get the list of OIDC issuers - - ListOpenIdConnectProvidersRequest request = ListOpenIdConnectProvidersRequest.builder().build(); - ListOpenIdConnectProvidersResponse response = iamClient.listOpenIDConnectProviders(request); - List oidcIssuers = response.openIDConnectProviderList(); - if (oidcIssuers != null) { - String issuerWithoutProtocol = issuer.replaceFirst("^https://", ""); - for (OpenIDConnectProviderListEntry oidcIssuer : oidcIssuers) { - if (oidcIssuer != null && oidcIssuer.arn() != null && oidcIssuer.arn().endsWith(issuerWithoutProtocol)) { - result = true; - break; - } - } - } - return result; - } - @Override public boolean validateSanDNSEntries(InstanceConfirmation confirmation, StringBuilder errMsg) { - StringBuilder instanceId = new StringBuilder(256); final Map instanceAttributes = confirmation.getAttributes(); - final String awsAccount = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_AWS_ACCOUNT); - if (StringUtil.isEmpty(awsAccount)) { - errMsg.append("Unable to find AWS account number"); + final String gcpProject = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_GCP_PROJECT); + if (StringUtil.isEmpty(gcpProject)) { + errMsg.append("Unable to find GCP project id"); return false; } if (!InstanceUtils.validateCertRequestSanDnsNames(instanceAttributes, confirmation.getDomain(), - confirmation.getService(), awsDNSSuffixes, eksDnsSuffixes, eksClusterNames.getStringsList(), + confirmation.getService(), gcpDNSSuffixes, gkeDnsSuffixes, gkeClusterNames.getStringsList(), true, instanceId, null)) { errMsg.append("Unable to validate certificate request hostnames"); return false; } return true; } - - boolean useIamRoleForIssuerValidation() { - return Boolean.TRUE.equals(useIamRoleForIssuerAttestation.get()); - } } From afb5e6683e5441a1ce6edcf401c311092d0bd258 Mon Sep 17 00:00:00 2001 From: Tatsuya Yano Date: Wed, 9 Apr 2025 10:48:16 +0900 Subject: [PATCH 6/7] Fixed DefaultKubernetesValidator.java --- ...DefaultKubernetesValidator => DefaultKubernetesValidator.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/com/yahoo/athenz/instance/provider/impl/{DefaultKubernetesValidator => DefaultKubernetesValidator.java} (100%) diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator.java similarity index 100% rename from src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator rename to src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator.java From 831353597f73884011bd9310ba9ba52b4ccff897 Mon Sep 17 00:00:00 2001 From: Tatsuya Yano Date: Wed, 9 Apr 2025 10:52:20 +0900 Subject: [PATCH 7/7] Fixed InstanceK8sProvider related implementation --- ...tAWSElasticKubernetesServiceValidator.java | 2 +- ...ultGCPGoogleKubernetesEngineValidator.java | 2 +- .../impl/DefaultKubernetesValidator.java | 91 ++++++------------- ...ubernetesDistributionValidatorFactory.java | 9 +- 4 files changed, 32 insertions(+), 72 deletions(-) diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java index 0ae6613..8e6fafc 100644 --- a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultAWSElasticKubernetesServiceValidator.java @@ -228,4 +228,4 @@ public boolean validateSanDNSEntries(InstanceConfirmation confirmation, StringBu boolean useIamRoleForIssuerValidation() { return Boolean.TRUE.equals(useIamRoleForIssuerAttestation.get()); } -} +} \ No newline at end of file diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java index fe8dd37..2b632ee 100644 --- a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultGCPGoogleKubernetesEngineValidator.java @@ -126,4 +126,4 @@ public boolean validateSanDNSEntries(InstanceConfirmation confirmation, StringBu } return true; } -} +} \ No newline at end of file diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator.java index fe8dd37..254ac55 100644 --- a/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator.java +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/DefaultKubernetesValidator.java @@ -3,11 +3,7 @@ import com.yahoo.athenz.auth.Authorizer; import com.yahoo.athenz.auth.Principal; import com.yahoo.athenz.auth.impl.SimplePrincipal; -import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigCsv; -import com.yahoo.athenz.instance.provider.AttrValidator; -import com.yahoo.athenz.instance.provider.AttrValidatorFactory; import com.yahoo.athenz.instance.provider.InstanceConfirmation; -import com.yahoo.athenz.instance.provider.InstanceProvider; import javax.net.ssl.SSLContext; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; @@ -16,93 +12,59 @@ import java.lang.invoke.MethodHandles; import java.util.*; -import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER; import static com.yahoo.athenz.instance.provider.impl.InstanceGCPProvider.*; -public class DefaultGCPGoogleKubernetesEngineValidator extends CommonKubernetesDistributionValidator { +public class DefaultKubernetesValidator extends CommonKubernetesDistributionValidator { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - Set gcpDNSSuffixes = new HashSet<>(); - List gkeDnsSuffixes; - DynamicConfigCsv gkeClusterNames; + Set k8sDNSSuffixes; - private static final DefaultGCPGoogleKubernetesEngineValidator INSTANCE = new DefaultGCPGoogleKubernetesEngineValidator(); - static final String GCP_OIDC_ISSUER_PREFIX = "https://container.googleapis.com/v1/projects/"; - AttrValidator attrValidator; - static final String ZTS_PROP_K8S_PROVIDER_GCP_ATTR_VALIDATOR_FACTORY_CLASS = "athenz.zts.k8s_provider_gcp_attr_validator_factory_class"; + private static final DefaultKubernetesValidator INSTANCE = new DefaultKubernetesValidator(); + static final String K8S_OIDC_ISSUER = "https://kubernetes.default.svc.cluster.local"; + static final String K8S_PROP_BOOT_TIME_OFFSET = "athenz.zts.k8s_boot_time_offset"; + static final String K8S_PROP_DNS_SUFFIX = "athenz.zts.k8s_dns_suffix"; - public static DefaultGCPGoogleKubernetesEngineValidator getInstance() { + public static DefaultKubernetesValidator getInstance() { return INSTANCE; } - private DefaultGCPGoogleKubernetesEngineValidator() { - } - - static AttrValidator newAttrValidator(final SSLContext sslContext) { - final String factoryClass = System.getProperty(ZTS_PROP_K8S_PROVIDER_GCP_ATTR_VALIDATOR_FACTORY_CLASS); - LOGGER.info("GCP K8S AttributeValidatorFactory class: {}", factoryClass); - if (factoryClass == null) { - return null; - } - - AttrValidatorFactory attrValidatorFactory; - try { - attrValidatorFactory = (AttrValidatorFactory) Class.forName(factoryClass).getConstructor().newInstance(); - } catch (Exception e) { - LOGGER.error("Invalid AttributeValidatorFactory class: {}", factoryClass, e); - throw new IllegalArgumentException("Invalid AttributeValidatorFactory class"); - } - - return attrValidatorFactory.create(sslContext); + private DefaultKubernetesValidator() { } @Override public void initialize(final SSLContext sslContext, Authorizer authorizer) { super.initialize(sslContext, authorizer); - final String dnsSuffix = System.getProperty(GCP_PROP_DNS_SUFFIX); + final String dnsSuffix = System.getProperty(K8S_PROP_DNS_SUFFIX); if (!StringUtil.isEmpty(dnsSuffix)) { - gcpDNSSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); + k8sDNSSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); } - // get our allowed gke dns suffixes - gkeDnsSuffixes = InstanceUtils.processK8SDnsSuffixList(GCP_PROP_GKE_DNS_SUFFIX); - // get our dynamic list of gke cluster names - gkeClusterNames = new DynamicConfigCsv(CONFIG_MANAGER, GCP_PROP_GKE_CLUSTER_NAMES, null); - this.attrValidator = newAttrValidator(sslContext); } @Override public String validateIssuer(InstanceConfirmation confirmation, IdTokenAttestationData attestationData, StringBuilder errMsg) { String issuer = getIssuerFromToken(attestationData, errMsg); if (StringUtil.isEmpty(issuer)) { + errMsg.append("Issuer is empty"); return null; } - String gcpProject = confirmation.getAttributes().get(InstanceProvider.ZTS_INSTANCE_GCP_PROJECT); - if (!issuer.startsWith(GCP_OIDC_ISSUER_PREFIX + gcpProject)) { - // could be a multi-tenant setup where the issuer is not present in the identity's GCP project - if (attrValidator != null) { - confirmation.getAttributes().put(ZTS_INSTANCE_UNATTESTED_ISSUER, issuer); - // Confirm the issuer as per the attribute validator - if (!attrValidator.confirm(confirmation)) { - return null; - } - } else { - errMsg.append("Issuer is not present in the GCP project associated with the domain"); - return null; - } - } else { - // issuer exists in the same GCP project as the requested identity - confirmation.getAttributes().put(ZTS_INSTANCE_ISSUER_GCP_PROJECT, gcpProject); + if (!issuer.equals(K8S_OIDC_ISSUER)) { + errMsg.append("Issuer is not ").append(K8S_OIDC_ISSUER); + return null; } + final Map instanceAttributes = confirmation.getAttributes(); + final String cloudName = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_CLOUD); + final String providerName = confirmation.getProvider(); + final String k8sNamespaceName = attestationData.getIdentityToken(); + final String k8sServiceAccountName = attestationData.getIdentityToken(); final String domainName = confirmation.getDomain(); final String serviceName = confirmation.getService(); // attribute set after verification above or attribute validation - final String issuerGcpProject = confirmation.getAttributes().get(ZTS_INSTANCE_ISSUER_GCP_PROJECT); - final String resource = String.format("%s:%s:%s", domainName, serviceName, issuerGcpProject); + final String resource = String.format("cloud:%s:provider:%s:system:serviceaccount:%s:%s", cloudName, providerName, k8sNamespaceName, k8sServiceAccountName); Principal principal = SimplePrincipal.create(domainName, serviceName, (String) null); boolean accessCheck = authorizer.access(ACTION_LAUNCH, resource, principal, null); if (!accessCheck) { - errMsg.append("gke launch authorization check failed for action: ").append(ACTION_LAUNCH) + errMsg.append("k8s launch authorization check failed for action: ").append(ACTION_LAUNCH) .append(" resource: ").append(resource); return null; } @@ -113,15 +75,14 @@ public String validateIssuer(InstanceConfirmation confirmation, IdTokenAttestati public boolean validateSanDNSEntries(InstanceConfirmation confirmation, StringBuilder errMsg) { StringBuilder instanceId = new StringBuilder(256); final Map instanceAttributes = confirmation.getAttributes(); - final String gcpProject = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_GCP_PROJECT); - if (StringUtil.isEmpty(gcpProject)) { - errMsg.append("Unable to find GCP project id"); + final String cloudName = InstanceUtils.getInstanceProperty(instanceAttributes, ZTS_INSTANCE_CLOUD); + if (StringUtil.isEmpty(cloudName) || cloudName.equals("")) { + errMsg.append("Unable to find cloud name"); return false; } if (!InstanceUtils.validateCertRequestSanDnsNames(instanceAttributes, confirmation.getDomain(), - confirmation.getService(), gcpDNSSuffixes, gkeDnsSuffixes, gkeClusterNames.getStringsList(), - true, instanceId, null)) { - errMsg.append("Unable to validate certificate request hostnames"); + confirmation.getService(), k8sDNSSuffixes, null, null, false, instanceId, null)) { + errMsg.append("Unable to validate certificate request hostnames for SAN DNS names"); return false; } return true; diff --git a/src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java b/src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java index bf46565..70abfdc 100644 --- a/src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java +++ b/src/main/java/com/yahoo/athenz/instance/provider/impl/SimpleKubernetesDistributionValidatorFactory.java @@ -6,15 +6,14 @@ import java.util.HashMap; import java.util.Map; -public class DefaultKubernetesDistributionValidatorFactory implements KubernetesDistributionValidatorFactory { +public class SimpleKubernetesDistributionValidatorFactory implements KubernetesDistributionValidatorFactory { Map supportedDistributionsMap = new HashMap<>(); - static final String CLOUD_AWS = "aws"; - static final String CLOUD_GCP = "gcp"; + static final String CLOUD_K8S = "k8s"; + @Override public void initialize() { - supportedDistributionsMap.put(CLOUD_AWS, DefaultAWSElasticKubernetesServiceValidator.getInstance()); - supportedDistributionsMap.put(CLOUD_GCP, DefaultGCPGoogleKubernetesEngineValidator.getInstance()); + supportedDistributionsMap.put(CLOUD_K8S, DefaultKubernetesValidator.getInstance()); } @Override