summaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java')
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java72
1 files changed, 69 insertions, 3 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
index 1b9bdcdb987..4c01b0943e4 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java
@@ -6,16 +6,24 @@ import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
import com.yahoo.container.jdisc.secretstore.SecretStore;
+import com.yahoo.jdisc.http.servlet.ServletRequest;
+import com.yahoo.log.LogLevel;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.Path;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SubjectAlternativeName;
import com.yahoo.security.X509CertificateUtils;
import com.yahoo.slime.Slime;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceConfirmation;
+import com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation.InstanceValidator;
import com.yahoo.vespa.hosted.ca.Certificates;
import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity;
+import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh;
import com.yahoo.yolean.Exceptions;
import java.io.IOException;
@@ -23,6 +31,10 @@ import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Clock;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.logging.Level;
@@ -43,18 +55,20 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler {
private final Certificates certificates;
private final String caPrivateKeySecretName;
private final String caCertificateSecretName;
+ private final InstanceValidator instanceValidator;
@Inject
- public CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, AthenzProviderServiceConfig athenzProviderServiceConfig) {
- this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig);
+ public CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) {
+ this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig, instanceValidator);
}
- CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, Certificates certificates, AthenzProviderServiceConfig athenzProviderServiceConfig) {
+ CertificateAuthorityApiHandler(Context ctx, SecretStore secretStore, Certificates certificates, AthenzProviderServiceConfig athenzProviderServiceConfig, InstanceValidator instanceValidator) {
super(ctx);
this.secretStore = secretStore;
this.certificates = certificates;
this.caPrivateKeySecretName = athenzProviderServiceConfig.secretName();
this.caCertificateSecretName = athenzProviderServiceConfig.domain() + ".ca.cert";
+ this.instanceValidator = instanceValidator;
}
@Override
@@ -81,6 +95,14 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler {
private HttpResponse registerInstance(HttpRequest request) {
var instanceRegistration = deserializeRequest(request, InstanceSerializer::registrationFromSlime);
+
+ InstanceConfirmation confirmation = new InstanceConfirmation(instanceRegistration.provider(), instanceRegistration.domain(), instanceRegistration.service(), EntityBindingsMapper.toSignedIdentityDocumentEntity(instanceRegistration.attestationData()));
+ confirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.IP_ADDRESS));
+ confirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.DNS_NAME));
+ if (!instanceValidator.isValidInstance(confirmation)) {
+ log.log(LogLevel.INFO, "Invalid instance registration for " + instanceRegistration.toString());
+ return ErrorResponse.forbidden("Unable to launch service: " +instanceRegistration.service());
+ }
var certificate = certificates.create(instanceRegistration.csr(), caCertificate(), caPrivateKey());
var instanceId = Certificates.instanceIdFrom(instanceRegistration.csr());
var identity = new InstanceIdentity(instanceRegistration.provider(), instanceRegistration.service(), instanceId,
@@ -91,21 +113,65 @@ public class CertificateAuthorityApiHandler extends LoggingRequestHandler {
private HttpResponse refreshInstance(HttpRequest request, String provider, String service, String instanceId) {
var instanceRefresh = deserializeRequest(request, InstanceSerializer::refreshFromSlime);
var instanceIdFromCsr = Certificates.instanceIdFrom(instanceRefresh.csr());
+ var athenzService = new AthenzService(request.getJDiscRequest().getUserPrincipal().getName());
if (!instanceIdFromCsr.equals(instanceId)) {
throw new IllegalArgumentException("Mismatch between instance ID in URL path and instance ID in CSR " +
"[instanceId=" + instanceId + ",instanceIdFromCsr=" + instanceIdFromCsr +
"]");
}
+
+ // Verify that the csr instance id matches one of the certificates in the chain
+ refreshesSameInstanceId(instanceIdFromCsr, request);
+
+
+ // Validate that there is no privilege escalation (can only refresh same service)
+ refreshesSameService(instanceRefresh, athenzService);
+
+ InstanceConfirmation instanceConfirmation = new InstanceConfirmation(provider, athenzService.getDomain().getName(), athenzService.getName(), null);
+ instanceConfirmation.set(InstanceValidator.SAN_IPS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.IP_ADDRESS));
+ instanceConfirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.DNS_NAME));
+ if(!instanceValidator.isValidRefresh(instanceConfirmation)) {
+ return ErrorResponse.forbidden("Unable to refresh cert: " + instanceRefresh.csr().getSubject().toString());
+ }
+
var certificate = certificates.create(instanceRefresh.csr(), caCertificate(), caPrivateKey());
var identity = new InstanceIdentity(provider, service, instanceIdFromCsr, Optional.of(certificate));
return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity));
}
+ public void refreshesSameInstanceId(String csrInstanceId, HttpRequest request) {
+ String certificateInstanceId = getRequestCertificateChain(request).stream()
+ .map(Certificates::instanceIdFrom)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findAny().orElseThrow(() -> new IllegalArgumentException("No client certificate with instance id in request."));
+
+ if(! Objects.equals(certificateInstanceId, csrInstanceId)) {
+ throw new IllegalArgumentException("Mismatch between instance ID in client certificate and instance ID in CSR " +
+ "[instanceId=" + certificateInstanceId + ",instanceIdFromCsr=" + csrInstanceId +
+ "]");
+ }
+ }
+
+ private void refreshesSameService(InstanceRefresh instanceRefresh, AthenzService athenzService) {
+ List<String> commonNames = X509CertificateUtils.getCommonNames(instanceRefresh.csr().getSubject());
+ if(commonNames.size() != 1 && !Objects.equals(commonNames.get(0), athenzService.getFullName())) {
+ throw new IllegalArgumentException(String.format("Invalid request, trying to refresh service %s using service %s.", instanceRefresh.csr().getSubject().getName(), athenzService.getFullName()));
+ }
+ }
+
/** Returns CA certificate from secret store */
private X509Certificate caCertificate() {
return X509CertificateUtils.fromPem(secretStore.getSecret(caCertificateSecretName));
}
+ private List<X509Certificate> getRequestCertificateChain(HttpRequest request) {
+ return Optional.ofNullable(request.getJDiscRequest().context().get(ServletRequest.JDISC_REQUEST_X509CERT))
+ .map(X509Certificate[].class::cast)
+ .map(Arrays::asList)
+ .orElse(Collections.emptyList());
+ }
+
/** Returns CA private key from secret store */
private PrivateKey caPrivateKey() {
return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(caPrivateKeySecretName));