Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/check-remote.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ 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="$( \
curl -s https://api.github.com/repos/${{ env.TRACKING_GIT_REPO }}/releases \
| 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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>

<name>athenz-plugins</name>
<description>Core Auth Interfaces</description>
<description>Athenz Plugin Implementations</description>
<groupId>io.athenz</groupId>
<artifactId>athenz-plugins</artifactId>
<version>0.0.0</version>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> awsDNSSuffixes = new HashSet<>();
List<String> 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<OpenIDConnectProviderListEntry> 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<String, String> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<String> gcpDNSSuffixes = new HashSet<>();
List<String> 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<String, String> 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;
}
}
Loading
Loading