diff options
Diffstat (limited to 'athenz-identity-provider-service/src/main')
17 files changed, 0 insertions, 1513 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CertificateExpiryMetricUpdater.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CertificateExpiryMetricUpdater.java deleted file mode 100644 index f3568caac04..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CertificateExpiryMetricUpdater.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.yahoo.component.annotation.Inject; -import com.yahoo.component.AbstractComponent; -import com.yahoo.jdisc.Metric; - -import java.time.Duration; -import java.time.Instant; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * @author freva - */ -public class CertificateExpiryMetricUpdater extends AbstractComponent { - - private static final Duration METRIC_REFRESH_PERIOD = Duration.ofMinutes(5); - private static final String ATHENZ_CONFIGSERVER_CERT_METRIC_NAME = "athenz-configserver-cert.expiry.seconds"; - - private final Logger logger = Logger.getLogger(CertificateExpiryMetricUpdater.class.getName()); - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - private final Metric metric; - private final ConfigserverSslContextFactoryProvider provider; - - @Inject - public CertificateExpiryMetricUpdater(Metric metric, - ConfigserverSslContextFactoryProvider provider) { - this.metric = metric; - this.provider = provider; - - scheduler.scheduleAtFixedRate(this::updateMetrics, - 30/*initial delay*/, - METRIC_REFRESH_PERIOD.getSeconds(), - TimeUnit.SECONDS); - } - - @Override - public void deconstruct() { - try { - scheduler.shutdownNow(); - scheduler.awaitTermination(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException("Failed to shutdown certificate expiry metrics updater on time", e); - } - } - - private void updateMetrics() { - try { - Duration keyStoreExpiry = Duration.between(Instant.now(), provider.getCertificateNotAfter()); - metric.set(ATHENZ_CONFIGSERVER_CERT_METRIC_NAME, keyStoreExpiry.getSeconds(), null); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to update key store expiry metric: " + e.getMessage(), e); - } - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CkmsKeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CkmsKeyProvider.java deleted file mode 100644 index c659c454420..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/CkmsKeyProvider.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.yahoo.component.annotation.Inject; -import com.yahoo.config.provision.Zone; -import com.yahoo.container.jdisc.secretstore.SecretStore; -import com.yahoo.security.KeyUtils; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; - -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.HashMap; -import java.util.Map; - -/** - * @author mortent - * @author bjorncs - */ -@SuppressWarnings("unused") // Injected component -public class CkmsKeyProvider implements KeyProvider { - - private final SecretStore secretStore; - private final String secretName; - private final Map<Integer, KeyPair> secrets; - - @Inject - public CkmsKeyProvider(SecretStore secretStore, - Zone zone, - AthenzProviderServiceConfig config) { - this.secretStore = secretStore; - this.secretName = config.secretName(); - this.secrets = new HashMap<>(); - } - - @Override - public PrivateKey getPrivateKey(int version) { - return getKeyPair(version).getPrivate(); - } - - @Override - public PublicKey getPublicKey(int version) { - return getKeyPair(version).getPublic(); - } - - @Override - public KeyPair getKeyPair(int version) { - synchronized (secrets) { - KeyPair keyPair = secrets.get(version); - if (keyPair == null) { - keyPair = readKeyPair(version); - secrets.put(version, keyPair); - } - return keyPair; - } - } - - private KeyPair readKeyPair(int version) { - PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(secretName, version)); - PublicKey publicKey = KeyUtils.extractPublicKey(privateKey); - return new KeyPair(publicKey, privateKey); - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java deleted file mode 100644 index 61a4a0fe41f..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/ConfigserverSslContextFactoryProvider.java +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.yahoo.component.annotation.Inject; -import com.yahoo.jdisc.http.ssl.impl.TlsContextBasedProvider; -import com.yahoo.security.KeyStoreBuilder; -import com.yahoo.security.KeyStoreType; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.SslContextBuilder; -import com.yahoo.security.tls.DefaultTlsContext; -import com.yahoo.security.MutableX509KeyManager; -import com.yahoo.security.tls.PeerAuthentication; -import com.yahoo.security.tls.TlsContext; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient; -import com.yahoo.vespa.athenz.client.zts.Identity; -import com.yahoo.vespa.athenz.client.zts.ZtsClient; -import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; -import com.yahoo.vespa.athenz.utils.SiaUtils; -import com.yahoo.vespa.defaults.Defaults; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; - -import javax.net.ssl.SSLContext; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.KeyPair; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.X509Certificate; -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Configures the JDisc https connector with the configserver's Athenz provider certificate and private key. - * - * @author bjorncs - */ -public class ConfigserverSslContextFactoryProvider extends TlsContextBasedProvider { - - private static final String CERTIFICATE_ALIAS = "athenz"; - private static final Duration EXPIRATION_MARGIN = Duration.ofHours(6); - private static final Path VESPA_SIA_DIRECTORY = Paths.get(Defaults.getDefaults().underVespaHome("var/vespa/sia")); - - private static final Logger log = Logger.getLogger(ConfigserverSslContextFactoryProvider.class.getName()); - - private final TlsContext tlsContext; - private final MutableX509KeyManager keyManager = new MutableX509KeyManager(); - private final ScheduledExecutorService scheduler = - Executors.newSingleThreadScheduledExecutor(runnable -> new Thread(runnable, "configserver-ssl-context-factory-provider")); - private final ZtsClient ztsClient; - private final KeyProvider keyProvider; - private final AthenzProviderServiceConfig athenzProviderServiceConfig; - private final AthenzService configserverIdentity; - - @Inject - public ConfigserverSslContextFactoryProvider(ServiceIdentityProvider bootstrapIdentity, - KeyProvider keyProvider, - AthenzProviderServiceConfig config) { - this.athenzProviderServiceConfig = config; - this.ztsClient = new DefaultZtsClient.Builder(URI.create(athenzProviderServiceConfig.ztsUrl())) - .withIdentityProvider(bootstrapIdentity).build(); - this.keyProvider = keyProvider; - this.configserverIdentity = new AthenzService(athenzProviderServiceConfig.domain(), athenzProviderServiceConfig.serviceName()); - - Duration updatePeriod = Duration.ofDays(config.updatePeriodDays()); - Path trustStoreFile = Paths.get(config.athenzCaTrustStore()); - this.tlsContext = createTlsContext(keyProvider, keyManager, trustStoreFile, updatePeriod, configserverIdentity, ztsClient, athenzProviderServiceConfig); - scheduler.scheduleAtFixedRate(new KeystoreUpdater(keyManager), - updatePeriod.toDays()/*initial delay*/, - updatePeriod.toDays(), - TimeUnit.DAYS); - } - - @Override - protected TlsContext getTlsContext(String containerId, int port) { - return tlsContext; - } - - Instant getCertificateNotAfter() { - return keyManager.currentManager().getCertificateChain(CERTIFICATE_ALIAS)[0].getNotAfter().toInstant(); - } - - @Override - public void deconstruct() { - try { - scheduler.shutdownNow(); - scheduler.awaitTermination(30, TimeUnit.SECONDS); - ztsClient.close(); - super.deconstruct(); - } catch (InterruptedException e) { - throw new RuntimeException("Failed to shutdown Athenz certificate updater on time", e); - } - } - - private static TlsContext createTlsContext(KeyProvider keyProvider, - MutableX509KeyManager keyManager, - Path trustStoreFile, - Duration updatePeriod, - AthenzService configserverIdentity, - ZtsClient ztsClient, - AthenzProviderServiceConfig zoneConfig) { - KeyStore keyStore = - tryReadKeystoreFile(configserverIdentity, updatePeriod) - .orElseGet(() -> updateKeystore(configserverIdentity, generateKeystorePassword(), keyProvider, ztsClient, zoneConfig)); - keyManager.updateKeystore(keyStore, new char[0]); - SSLContext sslContext = new SslContextBuilder() - .withTrustStore(trustStoreFile, KeyStoreType.JKS) - .withKeyManager(keyManager) - .build(); - return new DefaultTlsContext(sslContext, PeerAuthentication.WANT); - } - - private static Optional<KeyStore> tryReadKeystoreFile(AthenzService configserverIdentity, Duration updatePeriod) { - Optional<X509Certificate> certificate = SiaUtils.readCertificateFile(VESPA_SIA_DIRECTORY, configserverIdentity); - if (!certificate.isPresent()) return Optional.empty(); - Optional<PrivateKey> privateKey = SiaUtils.readPrivateKeyFile(VESPA_SIA_DIRECTORY, configserverIdentity); - if (!privateKey.isPresent()) return Optional.empty(); - Instant minimumExpiration = Instant.now().plus(updatePeriod).plus(EXPIRATION_MARGIN); - boolean isExpired = certificate.get().getNotAfter().toInstant().isBefore(minimumExpiration); - if (isExpired) return Optional.empty(); - KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS) - .withKeyEntry(CERTIFICATE_ALIAS, privateKey.get(), certificate.get()) - .build(); - return Optional.of(keyStore); - } - - private static KeyStore updateKeystore(AthenzService configserverIdentity, - char[] keystorePwd, - KeyProvider keyProvider, - ZtsClient ztsClient, - AthenzProviderServiceConfig zoneConfig) { - PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion()); - PublicKey publicKey = KeyUtils.extractPublicKey(privateKey); - Identity serviceIdentity = ztsClient.getServiceIdentity(configserverIdentity, - Integer.toString(zoneConfig.secretVersion()), - new KeyPair(publicKey, privateKey), - zoneConfig.certDnsSuffix()); - X509Certificate certificate = serviceIdentity.certificate(); - SiaUtils.writeCertificateFile(VESPA_SIA_DIRECTORY, configserverIdentity, certificate); - SiaUtils.writePrivateKeyFile(VESPA_SIA_DIRECTORY, configserverIdentity, privateKey); - Instant expirationTime = certificate.getNotAfter().toInstant(); - Duration expiry = Duration.between(certificate.getNotBefore().toInstant(), expirationTime); - log.log(Level.INFO, String.format("Got Athenz x509 certificate with expiry %s (expires %s)", expiry, expirationTime)); - return KeyStoreBuilder.withType(KeyStoreType.JKS) - .withKeyEntry(CERTIFICATE_ALIAS, privateKey, keystorePwd, certificate) - .build(); - } - - private static char[] generateKeystorePassword() { - return UUID.randomUUID().toString().toCharArray(); - } - - private class KeystoreUpdater implements Runnable { - final MutableX509KeyManager keyManager; - - KeystoreUpdater(MutableX509KeyManager keyManager) { - this.keyManager = keyManager; - } - - @Override - public void run() { - try { - log.log(Level.INFO, "Updating configserver provider certificate from ZTS"); - char[] keystorePwd = generateKeystorePassword(); - KeyStore keyStore = updateKeystore(configserverIdentity, keystorePwd, keyProvider, ztsClient, athenzProviderServiceConfig); - keyManager.updateKeystore(keyStore, keystorePwd); - log.log(Level.INFO, "Certificate successfully updated"); - } catch (Throwable t) { - log.log(Level.SEVERE, "Failed to update certificate from ZTS: " + t.getMessage(), t); - } - } - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java deleted file mode 100644 index 5143a38b2c1..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityDocumentGenerator.java +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.yahoo.component.annotation.Inject; -import com.yahoo.config.provision.Zone; -import com.yahoo.net.HostName; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; -import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.provision.Node; -import com.yahoo.vespa.hosted.provision.NodeRepository; -import com.yahoo.vespa.hosted.provision.node.Allocation; - -import java.security.PrivateKey; -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; - -/** - * Generates a signed identity document for a given hostname and type - * - * @author mortent - * @author bjorncs - */ -public class IdentityDocumentGenerator { - - private final IdentityDocumentSigner signer = new IdentityDocumentSigner(); - private final NodeRepository nodeRepository; - private final Zone zone; - private final KeyProvider keyProvider; - private final AthenzProviderServiceConfig athenzProviderServiceConfig; - - @Inject - public IdentityDocumentGenerator(AthenzProviderServiceConfig config, - NodeRepository nodeRepository, - Zone zone, - KeyProvider keyProvider) { - this.athenzProviderServiceConfig = config; - this.nodeRepository = nodeRepository; - this.zone = zone; - this.keyProvider = keyProvider; - } - - public SignedIdentityDocument generateSignedIdentityDocument(String hostname, IdentityType identityType) { - try { - Node node = nodeRepository.nodes().node(hostname).orElseThrow(() -> new RuntimeException("Unable to find node " + hostname)); - Allocation allocation = node.allocation().orElseThrow(() -> new RuntimeException("No allocation for node " + node.hostname())); - VespaUniqueInstanceId providerUniqueId = new VespaUniqueInstanceId( - allocation.membership().index(), - allocation.membership().cluster().id().value(), - allocation.owner().instance().value(), - allocation.owner().application().value(), - allocation.owner().tenant().value(), - zone.region().value(), - zone.environment().value(), - identityType); - - Set<String> ips = new HashSet<>(node.ipConfig().primary()); - - PrivateKey privateKey = keyProvider.getPrivateKey(athenzProviderServiceConfig.secretVersion()); - AthenzService providerService = new AthenzService(athenzProviderServiceConfig.domain(), athenzProviderServiceConfig.serviceName()); - - String configServerHostname = HostName.getLocalhost(); - Instant createdAt = Instant.now(); - var clusterType = ClusterType.from(allocation.membership().cluster().type().name()); - String signature = signer.generateSignature( - providerUniqueId, providerService, configServerHostname, - node.hostname(), createdAt, ips, identityType, privateKey); - return new SignedIdentityDocument( - signature, athenzProviderServiceConfig.secretVersion(), providerUniqueId, providerService, - SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION, configServerHostname, node.hostname(), - createdAt, ips, identityType, clusterType); - } catch (Exception e) { - throw new RuntimeException("Exception generating identity document: " + e.getMessage(), e); - } - } - -} - diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java deleted file mode 100644 index c1dd70d7656..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/IdentityProviderRequestHandler.java +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.yahoo.component.annotation.Inject; -import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; -import com.yahoo.restapi.RestApi; -import com.yahoo.restapi.RestApiException; -import com.yahoo.restapi.RestApiRequestHandler; -import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; -import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; -import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; - -import java.util.logging.Level; - -/** - * Handler implementing the Athenz Identity Provider API (Copper Argos). - * - * @author bjorncs - */ -public class IdentityProviderRequestHandler extends RestApiRequestHandler<IdentityProviderRequestHandler> { - - private final IdentityDocumentGenerator documentGenerator; - private final InstanceValidator instanceValidator; - - @Inject - public IdentityProviderRequestHandler(ThreadedHttpRequestHandler.Context context, - IdentityDocumentGenerator documentGenerator, - InstanceValidator instanceValidator) { - super(context, IdentityProviderRequestHandler::createRestApi); - this.documentGenerator = documentGenerator; - this.instanceValidator = instanceValidator; - } - - private static RestApi createRestApi(IdentityProviderRequestHandler self) { - return RestApi.builder() - .addRoute(RestApi.route("/athenz/v1/provider/identity-document/node/{host}") - .get(self::getNodeIdentityDocument)) - .addRoute(RestApi.route("/athenz/v1/provider/identity-document/tenant/{host}") - .get(self::getTenantIdentityDocument)) - .addRoute(RestApi.route("/athenz/v1/provider/instance") - .post(InstanceConfirmation.class, self::confirmInstance)) - .addRoute(RestApi.route("/athenz/v1/provider/refresh") - .post(InstanceConfirmation.class, self::confirmInstanceRefresh)) - .registerJacksonRequestEntity(InstanceConfirmation.class) - .registerJacksonResponseEntity(InstanceConfirmation.class) - .registerJacksonResponseEntity(SignedIdentityDocumentEntity.class) - // Overriding object mapper to change serialization of timestamps - .setObjectMapper(new ObjectMapper() - .registerModule(new JavaTimeModule()) - .registerModule(new Jdk8Module()) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)) - .build(); - } - - private SignedIdentityDocumentEntity getNodeIdentityDocument(RestApi.RequestContext context) { - String host = context.pathParameters().getString("host").orElse(null); - return getIdentityDocument(host, IdentityType.NODE); - } - - private SignedIdentityDocumentEntity getTenantIdentityDocument(RestApi.RequestContext context) { - String host = context.pathParameters().getString("host").orElse(null); - return getIdentityDocument(host, IdentityType.TENANT); - } - - private InstanceConfirmation confirmInstance(RestApi.RequestContext context, InstanceConfirmation instanceConfirmation) { - log.log(Level.FINE, () -> instanceConfirmation.toString()); - if (!instanceValidator.isValidInstance(instanceConfirmation)) { - log.log(Level.SEVERE, "Invalid instance: " + instanceConfirmation); - throw new RestApiException.Forbidden("Instance is invalid"); - } - return instanceConfirmation; - } - - private InstanceConfirmation confirmInstanceRefresh(RestApi.RequestContext context, InstanceConfirmation instanceConfirmation) { - log.log(Level.FINE, () -> instanceConfirmation.toString()); - if (!instanceValidator.isValidRefresh(instanceConfirmation)) { - log.log(Level.SEVERE, "Invalid instance refresh: " + instanceConfirmation); - throw new RestApiException.Forbidden("Instance is invalid"); - } - return instanceConfirmation; - } - - private SignedIdentityDocumentEntity getIdentityDocument(String hostname, IdentityType identityType) { - if (hostname == null) { - throw new RestApiException.BadRequest("The 'hostname' query parameter is missing"); - } - try { - return EntityBindingsMapper.toSignedIdentityDocumentEntity(documentGenerator.generateSignedIdentityDocument(hostname, identityType)); - } catch (Exception e) { - String message = String.format("Unable to generate identity document for '%s': %s", hostname, e.getMessage()); - log.log(Level.SEVERE, message, e); - throw new RestApiException.InternalServerError(message, e); - } - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java deleted file mode 100644 index 6c09a35ee3d..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceConfirmation.java +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * InstanceConfirmation object as per Athenz InstanceConfirmation API. - * - * @author bjorncs - */ -public class InstanceConfirmation { - - @JsonProperty("provider") public final String provider; - @JsonProperty("domain") public final String domain; - @JsonProperty("service") public final String service; - - @JsonProperty("attestationData") @JsonSerialize(using = SignedIdentitySerializer.class) - public final SignedIdentityDocumentEntity signedIdentityDocument; - @JsonUnwrapped public final Map<String, String> attributes = new HashMap<>(); // optional attributes that Athenz may provide - - @JsonCreator - public InstanceConfirmation(@JsonProperty("provider") String provider, - @JsonProperty("domain") String domain, - @JsonProperty("service") String service, - @JsonProperty("attestationData") @JsonDeserialize(using = SignedIdentityDeserializer.class) - SignedIdentityDocumentEntity signedIdentityDocument) { - this.provider = provider; - this.domain = domain; - this.service = service; - this.signedIdentityDocument = signedIdentityDocument; - } - - @JsonAnySetter - public void set(String name, String value) { - attributes.put(name, value); - } - - @Override - public String toString() { - return "InstanceConfirmation{" + - "provider='" + provider + '\'' + - ", domain='" + domain + '\'' + - ", service='" + service + '\'' + - ", signedIdentityDocument='" + signedIdentityDocument + '\'' + - ", attributes=" + attributes + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - InstanceConfirmation that = (InstanceConfirmation) o; - return Objects.equals(provider, that.provider) && - Objects.equals(domain, that.domain) && - Objects.equals(service, that.service) && - Objects.equals(signedIdentityDocument, that.signedIdentityDocument) && - Objects.equals(attributes, that.attributes); - } - - @Override - public int hashCode() { - return Objects.hash(provider, domain, service, signedIdentityDocument, attributes); - } - - public static class SignedIdentityDeserializer extends JsonDeserializer<SignedIdentityDocumentEntity> { - @Override - public SignedIdentityDocumentEntity deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { - String value = jsonParser.getValueAsString(); - return Utils.getMapper().readValue(value, SignedIdentityDocumentEntity.class); - } - } - - public static class SignedIdentitySerializer extends JsonSerializer<SignedIdentityDocumentEntity> { - @Override - public void serialize( - SignedIdentityDocumentEntity document, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(Utils.getMapper().writeValueAsString(document)); - } - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java deleted file mode 100644 index d8bbf743d8c..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/InstanceValidator.java +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.google.common.net.InetAddresses; -import com.yahoo.component.annotation.Inject; -import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.ServiceInfo; -import com.yahoo.config.model.api.SuperModelProvider; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; -import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -import com.yahoo.vespa.hosted.provision.Node; -import com.yahoo.vespa.hosted.provision.NodeRepository; - -import java.net.InetAddress; -import java.net.URI; -import java.security.PublicKey; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * Verifies that the instance's identity document is valid - * - * @author bjorncs - * @author mortent - */ -public class InstanceValidator { - - private static final Logger log = Logger.getLogger(InstanceValidator.class.getName()); - static final String SERVICE_PROPERTIES_DOMAIN_KEY = "identity.domain"; - static final String SERVICE_PROPERTIES_SERVICE_KEY = "identity.service"; - static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz."; - - public static final String SAN_IPS_ATTRNAME = "sanIP"; - public static final String SAN_DNS_ATTRNAME = "sanDNS"; - public static final String SAN_URI_ATTRNAME = "sanURI"; - - private final AthenzService tenantDockerContainerIdentity; - private final IdentityDocumentSigner signer; - private final KeyProvider keyProvider; - private final SuperModelProvider superModelProvider; - private final NodeRepository nodeRepository; - - @Inject - public InstanceValidator(KeyProvider keyProvider, - SuperModelProvider superModelProvider, - NodeRepository nodeRepository, - AthenzProviderServiceConfig config) { - this(keyProvider, superModelProvider, nodeRepository, new IdentityDocumentSigner(), new AthenzService(config.tenantService())); - } - - public InstanceValidator(KeyProvider keyProvider, - SuperModelProvider superModelProvider, - NodeRepository nodeRepository, - IdentityDocumentSigner identityDocumentSigner, - AthenzService tenantIdentity){ - this.keyProvider = keyProvider; - this.superModelProvider = superModelProvider; - this.nodeRepository = nodeRepository; - this.signer = identityDocumentSigner; - this.tenantDockerContainerIdentity = tenantIdentity; - } - - public boolean isValidInstance(InstanceConfirmation instanceConfirmation) { - try { - validateInstance(instanceConfirmation); - return true; - } catch (ValidationException e) { - log.log(e.logLevel(), e.messageSupplier()); - return false; - } - } - - public void validateInstance(InstanceConfirmation req) throws ValidationException { - SignedIdentityDocument signedIdentityDocument = EntityBindingsMapper.toSignedIdentityDocument(req.signedIdentityDocument); - VespaUniqueInstanceId providerUniqueId = signedIdentityDocument.providerUniqueId(); - ApplicationId applicationId = ApplicationId.from( - providerUniqueId.tenant(), providerUniqueId.application(), providerUniqueId.instance()); - - VespaUniqueInstanceId csrProviderUniqueId = getVespaUniqueInstanceId(req); - if(! providerUniqueId.equals(csrProviderUniqueId)) { - var msg = String.format("Instance %s has invalid provider unique ID in CSR (%s)", providerUniqueId, csrProviderUniqueId); - throw new ValidationException(Level.WARNING, () -> msg); - } - - if (! isSameIdentityAsInServicesXml(applicationId, req.domain, req.service)) { - Supplier<String> msg = () -> "Invalid identity '%s.%s' in services.xml".formatted(req.domain, req.service); - throw new ValidationException(Level.FINE, msg); - } - - log.log(Level.FINE, () -> String.format("Validating instance %s.", providerUniqueId)); - - PublicKey publicKey = keyProvider.getPublicKey(signedIdentityDocument.signingKeyVersion()); - if (! signer.hasValidSignature(signedIdentityDocument, publicKey)) { - var msg = String.format("Instance %s has invalid signature.", providerUniqueId); - throw new ValidationException(Level.SEVERE, () -> msg); - } - - validateAttributes(req, providerUniqueId); - log.log(Level.FINE, () -> String.format("Instance %s is valid.", providerUniqueId)); - } - - // TODO Add actual validation. Cannot reuse isValidInstance as identity document is not part of the refresh request. - // We'll have to perform some validation on the instance id and other fields of the attribute map. - // Separate between tenant and node certificate as well. - public boolean isValidRefresh(InstanceConfirmation confirmation) { - log.log(Level.FINE, () -> String.format("Accepting refresh for instance with identity '%s', provider '%s', instanceId '%s'.", - new AthenzService(confirmation.domain, confirmation.service).getFullName(), - confirmation.provider, - confirmation.attributes.get(SAN_DNS_ATTRNAME))); - try { - validateAttributes(confirmation, getVespaUniqueInstanceId(confirmation)); - return true; - } catch (ValidationException e) { - log.log(e.logLevel(), e.messageSupplier()); - return false; - } catch (Exception e) { - log.log(Level.WARNING, "Encountered exception while refreshing certificate for confirmation: " + confirmation, e); - return false; - } - } - - private VespaUniqueInstanceId getVespaUniqueInstanceId(InstanceConfirmation instanceConfirmation) { - // Find a list of SAN DNS - List<String> sanDNS = Optional.ofNullable(instanceConfirmation.attributes.get(SAN_DNS_ATTRNAME)) - .map(s -> s.split(",")) - .map(Arrays::asList).stream().flatMap(Collection::stream).toList(); - - return sanDNS.stream() - .filter(dns -> dns.contains(INSTANCE_ID_DELIMITER)) - .findFirst() - .map(s -> s.replaceAll(INSTANCE_ID_DELIMITER + ".*", "")) - .map(VespaUniqueInstanceId::fromDottedString) - .orElse(null); - } - - private void validateAttributes(InstanceConfirmation confirmation, VespaUniqueInstanceId vespaUniqueInstanceId) - throws ValidationException { - if(vespaUniqueInstanceId == null) { - var msg = "Unable to find unique instance ID in refresh request: " + confirmation.toString(); - throw new ValidationException(Level.WARNING, () -> msg); - } - - // Find node matching vespa unique id - Node node = nodeRepository.nodes().list().stream() - .filter(n -> n.allocation().isPresent()) - .filter(n -> nodeMatchesVespaUniqueId(n, vespaUniqueInstanceId)) - .findFirst() // Should be only one - .orElse(null); - if(node == null) { - var msg = "Invalid InstanceConfirmation, No nodes matching uniqueId: " + vespaUniqueInstanceId; - throw new ValidationException(Level.WARNING, () -> msg); - } - - // Find list of ipaddresses - List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get(SAN_IPS_ATTRNAME)) - .map(s -> s.split(",")) - .map(Arrays::asList).stream().flatMap(Collection::stream) - .map(InetAddresses::forString) - .toList(); - - List<InetAddress> nodeIpAddresses = node.ipConfig().primary().stream() - .map(InetAddresses::forString) - .toList(); - - // Validate that ipaddresses in request are valid for node - - if(! nodeIpAddresses.containsAll(ips)) { - var msg = "Invalid InstanceConfirmation, wrong ip in : " + vespaUniqueInstanceId; - throw new ValidationException(Level.WARNING, () -> msg); - } - - var urisCommaSeparated = confirmation.attributes.get(SAN_URI_ATTRNAME); - Set<URI> requestedUris; - try { - requestedUris = Optional.ofNullable(urisCommaSeparated).stream() - .flatMap(s -> Arrays.stream(s.split(","))).map(URI::create).collect(Collectors.toSet()); - } catch (IllegalArgumentException e) { - throw new ValidationException(Level.WARNING, () -> "Invalid SAN URIs: " + urisCommaSeparated, e); - } - var clusterType = node.allocation().map(a -> a.membership().cluster().type()).orElse(null); - Set<URI> allowedUris = clusterType != null - ? Set.of(ClusterType.from(clusterType.name()).asCertificateSanUri()) : Set.of(); - if (!allowedUris.containsAll(requestedUris)) { - Supplier<String> msg = () -> "Illegal SAN URIs: expected '%s' found '%s'".formatted(allowedUris, requestedUris); - throw new ValidationException(Level.WARNING, msg); - } - } - - private boolean nodeMatchesVespaUniqueId(Node node, VespaUniqueInstanceId vespaUniqueInstanceId) { - return node.allocation().map(allocation -> - allocation.membership().index() == vespaUniqueInstanceId.clusterIndex() && - allocation.membership().cluster().id().value().equals(vespaUniqueInstanceId.clusterId()) && - allocation.owner().instance().value().equals(vespaUniqueInstanceId.instance()) && - allocation.owner().application().value().equals(vespaUniqueInstanceId.application()) && - allocation.owner().tenant().value().equals(vespaUniqueInstanceId.tenant())) - .orElse(false); - } - - // If/when we don't care about logging exactly whats wrong, this can be simplified - // TODO Use identity type to determine if this check should be performed - private boolean isSameIdentityAsInServicesXml(ApplicationId applicationId, String domain, String service) { - - Optional<ApplicationInfo> applicationInfo = superModelProvider.getSuperModel().getApplicationInfo(applicationId); - - if (applicationInfo.isEmpty()) { - log.info(String.format("Could not find application info for %s, existing applications: %s", - applicationId.serializedForm(), - superModelProvider.getSuperModel().getAllApplicationInfos())); - return false; - } - - if (tenantDockerContainerIdentity.equals(new AthenzService(domain, service))) { - return true; - } - - Optional<ServiceInfo> matchingServiceInfo = applicationInfo.get() - .getModel() - .getHosts() - .stream() - .flatMap(hostInfo -> hostInfo.getServices().stream()) - .filter(serviceInfo -> serviceInfo.getProperty(SERVICE_PROPERTIES_DOMAIN_KEY).isPresent()) - .filter(serviceInfo -> serviceInfo.getProperty(SERVICE_PROPERTIES_SERVICE_KEY).isPresent()) - .findFirst(); - - if (matchingServiceInfo.isEmpty()) { - log.info(String.format("Application %s has not specified domain/service", applicationId.serializedForm())); - return false; - } - - String domainInConfig = matchingServiceInfo.get().getProperty(SERVICE_PROPERTIES_DOMAIN_KEY).get(); - String serviceInConfig = matchingServiceInfo.get().getProperty(SERVICE_PROPERTIES_SERVICE_KEY).get(); - if (!domainInConfig.equals(domain) || !serviceInConfig.equals(service)) { - log.warning(String.format("domain '%s' or service '%s' does not match the one in config for application %s", - domain, service, applicationId.serializedForm())); - return false; - } - - return true; - } - - public static class ValidationException extends Exception { - private final Level logLevel; - private final Supplier<String> msg; - - public ValidationException(Level logLevel, Supplier<String> msg) { this(logLevel, msg, null); } - public ValidationException(Level logLevel, Supplier<String> msg, Throwable cause) { super(cause); this.logLevel = logLevel; this.msg = msg; } - - @Override public String getMessage() { return msg.get(); } - public Level logLevel() { return logLevel; } - public Supplier<String> messageSupplier() { return msg; } - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java deleted file mode 100644 index 324f927fd73..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/KeyProvider.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; - -/** - * @author bjorncs - */ -public interface KeyProvider { - PrivateKey getPrivateKey(int version); - - PublicKey getPublicKey(int version); - - default KeyPair getKeyPair(int version) { - return new KeyPair(getPublicKey(version), getPrivateKey(version)); - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/Utils.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/Utils.java deleted file mode 100644 index 5c4942f37cb..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/Utils.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -/** - * @author bjorncs - */ -public class Utils { - - private static final ObjectMapper mapper = createObjectMapper(); - - public static ObjectMapper getMapper() { - return mapper; - } - - private static ObjectMapper createObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - return mapper; - } - -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java deleted file mode 100644 index 0cb5c9d4f82..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author bjorncs - */ -@ExportPackage -package com.yahoo.vespa.hosted.athenz.instanceproviderservice; - -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java deleted file mode 100644 index df904bf8010..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.ca; - -import com.yahoo.security.Pkcs10Csr; -import com.yahoo.security.SubjectAlternativeName; -import com.yahoo.security.X509CertificateBuilder; -import com.yahoo.security.X509CertificateUtils; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; - -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.time.Clock; -import java.time.Duration; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; -import static com.yahoo.security.SubjectAlternativeName.Type.DNS; - -/** - * Helper class for creating {@link X509Certificate}s. - * - * @author mpolden - */ -public class Certificates { - - private static final Duration CERTIFICATE_TTL = Duration.ofDays(30); - private static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz."; - - private final Clock clock; - - public Certificates(Clock clock) { - this.clock = Objects.requireNonNull(clock, "clock must be non-null"); - } - - /** Create a new certificate from csr signed by the given CA private key */ - public X509Certificate create(Pkcs10Csr csr, X509Certificate caCertificate, PrivateKey caPrivateKey) { - var x500principal = caCertificate.getSubjectX500Principal(); - var now = clock.instant(); - var notBefore = now.minus(Duration.ofHours(1)); - var notAfter = now.plus(CERTIFICATE_TTL); - var builder = X509CertificateBuilder.fromCsr(csr, - x500principal, - notBefore, - notAfter, - caPrivateKey, - SHA256_WITH_ECDSA, - X509CertificateBuilder.generateRandomSerialNumber()); - for (var san : csr.getSubjectAlternativeNames()) { - builder = builder.addSubjectAlternativeName(san.decode()); - } - return builder.build(); - } - - /** Returns instance ID parsed from the Subject Alternative Names in given csr */ - public static String instanceIdFrom(Pkcs10Csr csr) { - return getInstanceIdFromSAN(csr.getSubjectAlternativeNames()) - .orElseThrow(() -> new IllegalArgumentException("No instance ID found in CSR")); - } - - public static Optional<String> instanceIdFrom(X509Certificate certificate) { - return getInstanceIdFromSAN(X509CertificateUtils.getSubjectAlternativeNames(certificate)); - } - - private static Optional<String> getInstanceIdFromSAN(List<SubjectAlternativeName> subjectAlternativeNames) { - return subjectAlternativeNames.stream() - .filter(san -> san.getType() == DNS) - .map(SubjectAlternativeName::getValue) - .map(Certificates::parseInstanceId) - .flatMap(Optional::stream) - .map(VespaUniqueInstanceId::asDottedString) - .findFirst(); - } - - private static Optional<VespaUniqueInstanceId> parseInstanceId(String dnsName) { - var delimiterStart = dnsName.indexOf(INSTANCE_ID_DELIMITER); - if (delimiterStart == -1) return Optional.empty(); - dnsName = dnsName.substring(0, delimiterStart); - try { - return Optional.of(VespaUniqueInstanceId.fromDottedString(dnsName)); - } catch (IllegalArgumentException e) { - return Optional.empty(); - } - } - - public static String getSubjectAlternativeNames(Pkcs10Csr csr, SubjectAlternativeName.Type sanType) { - return csr.getSubjectAlternativeNames().stream() - .map(SubjectAlternativeName::decode) - .filter(san -> san.getType() == sanType) - .map(SubjectAlternativeName::getValue) - .collect(Collectors.joining(",")); - } -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java deleted file mode 100644 index f33ec4fbd6d..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceIdentity.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.ca.instance; - -import java.security.cert.X509Certificate; -import java.util.Objects; -import java.util.Optional; - -/** - * A signed instance identity object that includes a client certificate. This is the result of a successful - * {@link InstanceRegistration} and is the same type as InstanceIdentity in the ZTS API. - * - * @author mpolden - */ -public class InstanceIdentity { - - private final String provider; - private final String service; - private final String instanceId; - private final Optional<X509Certificate> x509Certificate; - - public InstanceIdentity(String provider, String service, String instanceId, Optional<X509Certificate> x509Certificate) { - this.provider = Objects.requireNonNull(provider, "provider must be non-null"); - this.service = Objects.requireNonNull(service, "service must be non-null"); - this.instanceId = Objects.requireNonNull(instanceId, "instanceId must be non-null"); - this.x509Certificate = Objects.requireNonNull(x509Certificate, "x509Certificate must be non-null"); - } - - /** Same as {@link InstanceRegistration#domain()} */ - public String provider() { - return provider; - } - - /** Same as {@link InstanceRegistration#service()} ()} */ - public String service() { - return service; - } - - /** A unique identifier of the instance to which the certificate is issued */ - public String instanceId() { - return instanceId; - } - - /** The issued certificate */ - public Optional<X509Certificate> x509Certificate() { - return x509Certificate; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - InstanceIdentity that = (InstanceIdentity) o; - return provider.equals(that.provider) && - service.equals(that.service) && - instanceId.equals(that.instanceId) && - x509Certificate.equals(that.x509Certificate); - } - - @Override - public int hashCode() { - return Objects.hash(provider, service, instanceId, x509Certificate); - } - -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java deleted file mode 100644 index d63ee7f979f..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRefresh.java +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.ca.instance; - -import com.yahoo.security.Pkcs10Csr; - -import java.util.Objects; - -/** - * Information for refreshing a instance in the system. This is the same type as InstanceRefreshInformation type in - * the ZTS API. - * - * @author mpolden - */ -public class InstanceRefresh { - - private final Pkcs10Csr csr; - - public InstanceRefresh(Pkcs10Csr csr) { - this.csr = Objects.requireNonNull(csr, "csr must be non-null"); - } - - /** The Certificate Signed Request describing the wanted certificate */ - public Pkcs10Csr csr() { - return csr; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - InstanceRefresh that = (InstanceRefresh) o; - return csr.equals(that.csr); - } - - @Override - public int hashCode() { - return Objects.hash(csr); - } - -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java deleted file mode 100644 index 231954976bf..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/instance/InstanceRegistration.java +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.ca.instance; - -import com.yahoo.security.Pkcs10Csr; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; - -import java.util.Objects; - -/** - * Information for registering a new instance in the system. This is the same type as InstanceRegisterInformation type - * in the ZTS API. - * - * @author mpolden - */ -public class InstanceRegistration { - - private final String provider; - private final String domain; - private final String service; - private final SignedIdentityDocument attestationData; - private final Pkcs10Csr csr; - - public InstanceRegistration(String provider, String domain, String service, SignedIdentityDocument attestationData, Pkcs10Csr csr) { - this.provider = Objects.requireNonNull(provider, "provider must be non-null"); - this.domain = Objects.requireNonNull(domain, "domain must be non-null"); - this.service = Objects.requireNonNull(service, "service must be non-null"); - this.attestationData = Objects.requireNonNull(attestationData, "attestationData must be non-null"); - this.csr = Objects.requireNonNull(csr, "csr must be non-null"); - } - - /** The provider which issued the attestation data contained in this */ - public String provider() { - return provider; - } - - /** Athenz domain of the instance */ - public String domain() { - return domain; - } - - /** Athenz service of the instance */ - public String service() { - return service; - } - - /** Host document describing this instance (received from config server) */ - public SignedIdentityDocument attestationData() { - return attestationData; - } - - /** The Certificate Signed Request describing the wanted certificate */ - public Pkcs10Csr csr() { - return csr; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - InstanceRegistration that = (InstanceRegistration) o; - return provider.equals(that.provider) && - domain.equals(that.domain) && - service.equals(that.service) && - attestationData.equals(that.attestationData) && - csr.equals(that.csr); - } - - @Override - public int hashCode() { - return Objects.hash(provider, domain, service, attestationData, csr); - } - - @Override - public String toString() { - return "InstanceRegistration{" + - "provider='" + provider + '\'' + - ", domain='" + domain + '\'' + - ", service='" + service + '\'' + - ", attestationData='" + attestationData.toString() + '\'' + - ", csr=" + csr.toString() + - '}'; - } -} 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 deleted file mode 100644 index 531a815922b..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.ca.restapi; - -import com.yahoo.component.annotation.Inject; -import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; -import com.yahoo.container.jdisc.secretstore.SecretStore; -import com.yahoo.jdisc.http.server.jetty.RequestUtils; -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.slime.SlimeUtils; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceConfirmation; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.InstanceValidator; -import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig; -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; -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; - -/** - * REST API for issuing and refreshing node certificates in a hosted Vespa system. - * - * The API implements the following subset of methods from the Athenz ZTS REST API: - * - * - Instance registration - * - Instance refresh - * - * @author mpolden - */ -public class CertificateAuthorityApiHandler extends ThreadedHttpRequestHandler { - - private final SecretStore secretStore; - 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, InstanceValidator instanceValidator) { - this(ctx, secretStore, new Certificates(Clock.systemUTC()), athenzProviderServiceConfig, instanceValidator); - } - - 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.caCertSecretName(); - this.instanceValidator = instanceValidator; - } - - @Override - public HttpResponse handle(HttpRequest request) { - try { - switch (request.getMethod()) { - case POST: return handlePost(request); - default: return ErrorResponse.methodNotAllowed("Method " + request.getMethod() + " is unsupported"); - } - } catch (IllegalArgumentException e) { - return ErrorResponse.badRequest(request.getMethod() + " " + request.getUri() + " failed: " + Exceptions.toMessageString(e)); - } catch (RuntimeException e) { - log.log(Level.WARNING, "Unexpected error handling " + request.getMethod() + " " + request.getUri(), e); - return ErrorResponse.internalServerError(Exceptions.toMessageString(e)); - } - } - - private HttpResponse handlePost(HttpRequest request) { - Path path = new Path(request.getUri()); - if (path.matches("/ca/v1/instance/")) return registerInstance(request); - if (path.matches("/ca/v1/instance/{provider}/{domain}/{service}/{instanceId}")) return refreshInstance(request, path.get("provider"), path.get("service"), path.get("instanceId")); - return ErrorResponse.notFoundError("Nothing at " + path); - } - - 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)); - confirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRegistration.csr(), SubjectAlternativeName.Type.DNS)); - if (!instanceValidator.isValidInstance(confirmation)) { - log.log(Level.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, - Optional.of(certificate)); - return new SlimeJsonResponse(InstanceSerializer.identityToSlime(identity)); - } - - 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 = getRequestAthenzService(request); - - 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)); - instanceConfirmation.set(InstanceValidator.SAN_DNS_ATTRNAME, Certificates.getSubjectAlternativeNames(instanceRefresh.csr(), SubjectAlternativeName.Type.DNS)); - 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(RequestUtils.JDISC_REQUEST_X509CERT)) - .map(X509Certificate[].class::cast) - .map(Arrays::asList) - .orElse(Collections.emptyList()); - } - - private AthenzService getRequestAthenzService(HttpRequest request) { - return getRequestCertificateChain(request).stream() - .findFirst() - .flatMap(X509CertificateUtils::getSubjectCommonName) - .map(AthenzService::new) - .orElseThrow(() -> new RuntimeException("No certificate found")); - } - - /** Returns CA private key from secret store */ - private PrivateKey caPrivateKey() { - return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(caPrivateKeySecretName)); - } - - private static <T> T deserializeRequest(HttpRequest request, Function<Slime, T> serializer) { - try { - var slime = SlimeUtils.jsonToSlime(request.getData().readAllBytes()); - return serializer.apply(slime); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java deleted file mode 100644 index fec03afab69..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/InstanceSerializer.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.ca.restapi; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.yahoo.security.Pkcs10CsrUtils; -import com.yahoo.security.X509CertificateUtils; -import com.yahoo.slime.ArrayTraverser; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Slime; -import com.yahoo.slime.SlimeUtils; -import com.yahoo.text.StringUtilities; -import com.yahoo.vespa.athenz.api.AthenzService; -import com.yahoo.vespa.athenz.identityprovider.api.ClusterType; -import com.yahoo.vespa.athenz.identityprovider.api.IdentityType; -import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.vespa.hosted.ca.instance.InstanceIdentity; -import com.yahoo.vespa.hosted.ca.instance.InstanceRefresh; -import com.yahoo.vespa.hosted.ca.instance.InstanceRegistration; - -import java.io.IOException; -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; - -/** - * @author mpolden - */ -public class InstanceSerializer { - - private static final String PROVIDER_FIELD = "provider"; - private static final String DOMAIN_FIELD = "domain"; - private static final String SERVICE_FIELD = "service"; - private static final String ATTESTATION_DATA_FIELD = "attestationData"; - private static final String CSR_FIELD = "csr"; - private static final String NAME_FIELD = "service"; - private static final String INSTANCE_ID_FIELD = "instanceId"; - private static final String X509_CERTIFICATE_FIELD = "x509Certificate"; - - private static final String IDD_SIGNATURE_FIELD = "signature"; - private static final String IDD_SIGNING_KEY_VERSION_FIELD = "signing-key-version"; - private static final String IDD_PROVIDER_UNIQUE_ID_FIELD = "provider-unique-id"; - private static final String IDD_PROVIDER_SERVICE_FIELD = "provider-service"; - private static final String IDD_DOCUMENT_VERSION_FIELD = "document-version"; - private static final String IDD_CONFIGSERVER_HOSTNAME_FIELD = "configserver-hostname"; - private static final String IDD_INSTANCE_HOSTNAME_FIELD = "instance-hostname"; - private static final String IDD_CREATED_AT_FIELD = "created-at"; - private static final String IDD_IPADDRESSES_FIELD = "ip-addresses"; - private static final String IDD_IDENTITY_TYPE_FIELD = "identity-type"; - private static final String IDD_CLUSTER_TYPE_FIELD = "cluster-type"; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - static { - objectMapper.registerModule(new JavaTimeModule()); - } - - private InstanceSerializer() {} - - public static InstanceRegistration registrationFromSlime(Slime slime) { - Cursor root = slime.get(); - return new InstanceRegistration(requireField(PROVIDER_FIELD, root).asString(), - requireField(DOMAIN_FIELD, root).asString(), - requireField(SERVICE_FIELD, root).asString(), - attestationDataToIdentityDocument(StringUtilities.unescape(requireField(ATTESTATION_DATA_FIELD, root).asString())), - Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString())); - } - - public static InstanceRefresh refreshFromSlime(Slime slime) { - Cursor root = slime.get(); - return new InstanceRefresh(Pkcs10CsrUtils.fromPem(requireField(CSR_FIELD, root).asString())); - } - - public static Slime identityToSlime(InstanceIdentity identity) { - Slime slime = new Slime(); - Cursor root = slime.setObject(); - root.setString(PROVIDER_FIELD, identity.provider()); - root.setString(NAME_FIELD, identity.service()); - root.setString(INSTANCE_ID_FIELD, identity.instanceId()); - identity.x509Certificate() - .map(X509CertificateUtils::toPem) - .ifPresent(pem -> root.setString(X509_CERTIFICATE_FIELD, pem)); - return slime; - } - - public static SignedIdentityDocument attestationDataToIdentityDocument(String attestationData) { - Slime slime = SlimeUtils.jsonToSlime(attestationData); - Cursor root = slime.get(); - String signature = requireField(IDD_SIGNATURE_FIELD, root).asString(); - long signingKeyVersion = requireField(IDD_SIGNING_KEY_VERSION_FIELD, root).asLong(); - VespaUniqueInstanceId providerUniqueId = VespaUniqueInstanceId.fromDottedString(requireField(IDD_PROVIDER_UNIQUE_ID_FIELD, root).asString()); - AthenzService athenzService = new AthenzService(requireField(IDD_PROVIDER_SERVICE_FIELD, root).asString()); - long documentVersion = requireField(IDD_DOCUMENT_VERSION_FIELD, root).asLong(); - String configserverHostname = requireField(IDD_CONFIGSERVER_HOSTNAME_FIELD, root).asString(); - String instanceHostname = requireField(IDD_INSTANCE_HOSTNAME_FIELD, root).asString(); - double createdAtTimestamp = requireField(IDD_CREATED_AT_FIELD, root).asDouble(); - Instant createdAt = getJsr310Instant(createdAtTimestamp); - Set<String> ips = new HashSet<>(); - requireField(IDD_IPADDRESSES_FIELD, root).traverse((ArrayTraverser) (__, entry) -> ips.add(entry.asString())); - IdentityType identityType = IdentityType.fromId(requireField(IDD_IDENTITY_TYPE_FIELD, root).asString()); - var clusterTypeField = root.field(IDD_CLUSTER_TYPE_FIELD); - var clusterType = clusterTypeField.valid() ? ClusterType.from(clusterTypeField.asString()) : null; - - - return new SignedIdentityDocument(signature, (int)signingKeyVersion, providerUniqueId, athenzService, (int)documentVersion, - configserverHostname, instanceHostname, createdAt, ips, identityType, clusterType); - } - - private static Instant getJsr310Instant(double v) { - try { - return objectMapper.readValue(Double.toString(v), Instant.class); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static Cursor requireField(String fieldName, Cursor root) { - var field = root.field(fieldName); - if (!field.valid()) throw new IllegalArgumentException("Missing required field '" + fieldName + "'"); - return field; - } - -} diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/package-info.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/package-info.java deleted file mode 100644 index 118f4b08c2a..00000000000 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author mpolden - */ -@ExportPackage -package com.yahoo.vespa.hosted.ca.restapi; - -import com.yahoo.osgi.annotation.ExportPackage; |