diff options
3 files changed, 113 insertions, 71 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/CloudSslProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/CloudSslProvider.java index ae60ed77a7a..5fa893e9599 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/CloudSslProvider.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/CloudSslProvider.java @@ -2,6 +2,8 @@ package com.yahoo.vespa.model.container.http.ssl; import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ssl.impl.CloudSslContextProvider; +import com.yahoo.jdisc.http.ssl.impl.ConfiguredSslContextFactoryProvider; import java.util.Optional; @@ -15,6 +17,8 @@ import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.ClientAuth; */ public class CloudSslProvider extends SslProvider { public static final String COMPONENT_ID_PREFIX = "configured-ssl-provider@"; + public static final String MTLSONLY_COMPONENT_CLASS = ConfiguredSslContextFactoryProvider.class.getName(); + public static final String TOKEN_COMPONENT_CLASS = CloudSslContextProvider.class.getName(); private final String privateKey; private final String certificate; @@ -22,9 +26,8 @@ public class CloudSslProvider extends SslProvider { private final String caCertificate; private final ClientAuth.Enum clientAuthentication; - public CloudSslProvider(String servername, String privateKey, String certificate, String caCertificatePath, - String caCertificate, ClientAuth.Enum clientAuthentication) { - super(COMPONENT_ID_PREFIX, servername, "com.yahoo.jdisc.http.ssl.impl.CloudSslContextProvider", null); + public CloudSslProvider(String servername, String privateKey, String certificate, String caCertificatePath, String caCertificate, ClientAuth.Enum clientAuthentication, boolean enableTokenSupport) { + super(COMPONENT_ID_PREFIX, servername, componentClass(enableTokenSupport), null); this.privateKey = privateKey; this.certificate = certificate; this.caCertificatePath = caCertificatePath; @@ -32,6 +35,10 @@ public class CloudSslProvider extends SslProvider { this.clientAuthentication = clientAuthentication; } + private static String componentClass(boolean enableTokenSupport) { + return enableTokenSupport ? TOKEN_COMPONENT_CLASS : MTLSONLY_COMPONENT_CLASS; + } + @Override public void amendConnectorConfig(ConnectorConfig.Builder builder) { builder.ssl.enabled(true); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/HostedSslConnectorFactory.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/HostedSslConnectorFactory.java index 4f11611541d..5bf348e5bb5 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/HostedSslConnectorFactory.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/HostedSslConnectorFactory.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.model.container.http.ssl; import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ConnectorConfig.Ssl.ClientAuth; import com.yahoo.security.tls.TlsContext; import com.yahoo.vespa.model.container.http.ConnectorFactory; @@ -17,71 +18,96 @@ import java.util.List; */ public class HostedSslConnectorFactory extends ConnectorFactory { - boolean requireTlsClientAuthDuringTlsHandshake; - private final List<String> tlsCiphersOverride; + private static final List<String> INSECURE_WHITELISTED_PATHS = List.of("/status.html"); + private static final String DEFAULT_HOSTED_TRUSTSTORE = "/opt/yahoo/share/ssl/certs/athenz_certificate_bundle.pem"; + + private final boolean enforceClientAuth; + private final boolean enforceHandshakeClientAuth; + private final Collection<String> tlsCiphersOverride; private final boolean enableProxyProtocolMixedMode; private final Duration endpointConnectionTtl; - public static Builder builder(String name, int listenPort) { return new Builder(name, listenPort); } + /** + * Create connector factory that uses a certificate provided by the config-model / configserver and default hosted Vespa truststore. + */ + public static HostedSslConnectorFactory withProvidedCertificate( + String serverName, EndpointCertificateSecrets endpointCertificateSecrets, boolean enforceHandshakeClientAuth, + Collection<String> tlsCiphersOverride, boolean enableProxyProtocolMixedMode, int port, + Duration endpointConnectionTtl, boolean enableTokenSupport) { + CloudSslProvider sslProvider = createConfiguredDirectSslProvider( + serverName, endpointCertificateSecrets, DEFAULT_HOSTED_TRUSTSTORE, /*tlsCaCertificates*/null, enforceHandshakeClientAuth, enableTokenSupport); + return new HostedSslConnectorFactory(sslProvider, false, enforceHandshakeClientAuth, tlsCiphersOverride, + enableProxyProtocolMixedMode, port, endpointConnectionTtl); + } + + /** + * Create connector factory that uses a certificate provided by the config-model / configserver and a truststore configured by the application. + */ + public static HostedSslConnectorFactory withProvidedCertificateAndTruststore( + String serverName, EndpointCertificateSecrets endpointCertificateSecrets, String tlsCaCertificates, + Collection<String> tlsCiphersOverride, boolean enableProxyProtocolMixedMode, int port, + Duration endpointConnectionTtl, boolean enableTokenSupport) { + CloudSslProvider sslProvider = createConfiguredDirectSslProvider( + serverName, endpointCertificateSecrets, /*tlsCaCertificatesPath*/null, tlsCaCertificates, false, enableTokenSupport); + return new HostedSslConnectorFactory(sslProvider, true, false, tlsCiphersOverride, enableProxyProtocolMixedMode, + port, endpointConnectionTtl); + } + + /** + * Create connector factory that uses the default certificate and truststore provided by Vespa (through Vespa-global TLS configuration). + */ + public static HostedSslConnectorFactory withDefaultCertificateAndTruststore(String serverName, Collection<String> tlsCiphersOverride, + boolean enableProxyProtocolMixedMode, int port, + Duration endpointConnectionTtl) { + return new HostedSslConnectorFactory(new DefaultSslProvider(serverName), true, false, tlsCiphersOverride, + enableProxyProtocolMixedMode, port, endpointConnectionTtl); + } - private HostedSslConnectorFactory(Builder builder) { - super(new ConnectorFactory.Builder("tls"+builder.port, builder.port).sslProvider(createSslProvider(builder))); - this.requireTlsClientAuthDuringTlsHandshake = builder.requireTlsClientAuthDuringTlsHandshake; - this.tlsCiphersOverride = List.copyOf(builder.tlsCiphersOverride); - this.enableProxyProtocolMixedMode = builder.enableProxyProtocolMixedMode; - this.endpointConnectionTtl = builder.endpointConnectionTtl; + private HostedSslConnectorFactory(SslProvider sslProvider, boolean enforceClientAuth, + boolean enforceHandshakeClientAuth, Collection<String> tlsCiphersOverride, + boolean enableProxyProtocolMixedMode, int port, Duration endpointConnectionTtl) { + super(new Builder("tls"+port, port).sslProvider(sslProvider)); + this.enforceClientAuth = enforceClientAuth; + this.enforceHandshakeClientAuth = enforceHandshakeClientAuth; + this.tlsCiphersOverride = tlsCiphersOverride; + this.enableProxyProtocolMixedMode = enableProxyProtocolMixedMode; + this.endpointConnectionTtl = endpointConnectionTtl; } - private static SslProvider createSslProvider(Builder builder) { - if (builder.endpointCertificate == null) return new DefaultSslProvider(builder.name); - var clientAuthentication = builder.requireTlsClientAuthDuringTlsHandshake - ? ConnectorConfig.Ssl.ClientAuth.Enum.NEED_AUTH : ConnectorConfig.Ssl.ClientAuth.Enum.WANT_AUTH; + private static CloudSslProvider createConfiguredDirectSslProvider( + String serverName, EndpointCertificateSecrets endpointCertificateSecrets, String tlsCaCertificatesPath, String tlsCaCertificates, boolean enforceHandshakeClientAuth, boolean enableTokenSupport) { + var clientAuthentication = enforceHandshakeClientAuth ? ClientAuth.Enum.NEED_AUTH : ClientAuth.Enum.WANT_AUTH; return new CloudSslProvider( - builder.name, builder.endpointCertificate.key(), builder.endpointCertificate.certificate(), - builder.tlsCaCertificatesPath, builder.tlsCaCertificatesPem, clientAuthentication); + serverName, + endpointCertificateSecrets.key(), + endpointCertificateSecrets.certificate(), + tlsCaCertificatesPath, + tlsCaCertificates, + clientAuthentication, + enableTokenSupport); } @Override public void getConfig(ConnectorConfig.Builder connectorBuilder) { super.getConfig(connectorBuilder); - if (! requireTlsClientAuthDuringTlsHandshake) { - connectorBuilder.tlsClientAuthEnforcer( - new ConnectorConfig.TlsClientAuthEnforcer.Builder() - .pathWhitelist(List.of("/status.html")).enable(true)); + if (! enforceHandshakeClientAuth) { + connectorBuilder + .tlsClientAuthEnforcer(new ConnectorConfig.TlsClientAuthEnforcer.Builder() + .pathWhitelist(INSECURE_WHITELISTED_PATHS) + .enable(enforceClientAuth)); } // Disables TLSv1.3 as it causes some browsers to prompt user for client certificate (when connector has 'want' auth) connectorBuilder.ssl.enabledProtocols(List.of("TLSv1.2")); + if (!tlsCiphersOverride.isEmpty()) { connectorBuilder.ssl.enabledCipherSuites(tlsCiphersOverride.stream().sorted().toList()); } else { connectorBuilder.ssl.enabledCipherSuites(TlsContext.ALLOWED_CIPHER_SUITES.stream().sorted().toList()); } + connectorBuilder .proxyProtocol(new ConnectorConfig.ProxyProtocol.Builder().enabled(true).mixedMode(enableProxyProtocolMixedMode)) .idleTimeout(Duration.ofSeconds(30).toSeconds()) .maxConnectionLife(endpointConnectionTtl != null ? endpointConnectionTtl.toSeconds() : 0); } - - public static class Builder { - final String name; - final int port; - boolean requireTlsClientAuthDuringTlsHandshake; - List<String> tlsCiphersOverride; - boolean enableProxyProtocolMixedMode; - Duration endpointConnectionTtl; - EndpointCertificateSecrets endpointCertificate; - String tlsCaCertificatesPem; - String tlsCaCertificatesPath; - - private Builder(String name, int port) { this.name = name; this.port = port; } - public Builder requireTlsClientAuthDuringTlsHandshake(boolean enable) {this.requireTlsClientAuthDuringTlsHandshake = enable; return this; } - public Builder endpointConnectionTtl(Duration ttl) { endpointConnectionTtl = ttl; return this; } - public Builder tlsCiphersOverride(Collection<String> ciphers) { tlsCiphersOverride = List.copyOf(ciphers); return this; } - public Builder proxyProtocolMixedMode(boolean enable) { enableProxyProtocolMixedMode = enable; return this; } - public Builder endpointCertificate(EndpointCertificateSecrets cert) { this.endpointCertificate = cert; return this; } - public Builder tlsCaCertificatesPath(String path) { this.tlsCaCertificatesPath = path; return this; } - public Builder tlsCaCertificatesPem(String pem) { this.tlsCaCertificatesPem = pem; return this; } - - public HostedSslConnectorFactory build() { return new HostedSslConnectorFactory(this); } - } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 3318138ebd7..00feb0a1c76 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -16,6 +16,7 @@ import com.yahoo.config.model.ConfigModelContext; import com.yahoo.config.model.api.ApplicationClusterEndpoint; import com.yahoo.config.model.api.ConfigServerSpec; import com.yahoo.config.model.api.ContainerEndpoint; +import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.model.application.provider.IncludeDirs; import com.yahoo.config.model.builder.xml.ConfigModelBuilder; @@ -108,6 +109,7 @@ import java.io.IOException; import java.io.Reader; import java.net.URI; import java.security.cert.X509Certificate; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -598,35 +600,31 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { .ifPresent(accessControl -> accessControl.configureDefaultHostedConnector(cluster.getHttp())); ; } - private void addAdditionalHostedConnector(DeployState state, ApplicationContainerCluster cluster) { + private void addAdditionalHostedConnector(DeployState deployState, ApplicationContainerCluster cluster) { JettyHttpServer server = cluster.getHttp().getHttpServer().get(); String serverName = server.getComponentId().getName(); // If the deployment contains certificate/private key reference, setup TLS port - var builder = HostedSslConnectorFactory.builder(serverName, getDataplanePort(state)) - .proxyProtocolMixedMode(state.getProperties().featureFlags().enableProxyProtocolMixedMode()) - .tlsCiphersOverride(state.getProperties().tlsCiphersOverride()) - .endpointConnectionTtl(state.getProperties().endpointConnectionTtl()); - var endpointCert = state.endpointCertificateSecrets().orElse(null); - if (endpointCert != null) { - builder.endpointCertificate(endpointCert); - boolean isPublic = state.zone().system().isPublic(); + HostedSslConnectorFactory connectorFactory; + Collection<String> tlsCiphersOverride = deployState.getProperties().tlsCiphersOverride(); + boolean proxyProtocolMixedMode = deployState.getProperties().featureFlags().enableProxyProtocolMixedMode(); + Duration endpointConnectionTtl = deployState.getProperties().endpointConnectionTtl(); + var port = getDataplanePort(deployState); + if (deployState.endpointCertificateSecrets().isPresent()) { + boolean authorizeClient = deployState.zone().system().isPublic(); List<X509Certificate> clientCertificates = getClientCertificates(cluster); - if (isPublic) { - if (clientCertificates.isEmpty()) - throw new IllegalArgumentException("Client certificate authority security/clients.pem is missing - " + - "see: https://cloud.vespa.ai/en/security/guide#data-plane"); - builder.tlsCaCertificatesPem(X509CertificateUtils.toPem(clientCertificates)); - } else { - builder.tlsCaCertificatesPath("/opt/yahoo/share/ssl/certs/athenz_certificate_bundle.pem"); + if (authorizeClient && clientCertificates.isEmpty()) { + throw new IllegalArgumentException("Client certificate authority security/clients.pem is missing - " + + "see: https://cloud.vespa.ai/en/security/guide#data-plane"); } - builder.requireTlsClientAuthDuringTlsHandshake( - cluster.getHttp().getAccessControl() - .map(accessControl -> accessControl.clientAuthentication) - .map(clientAuth -> clientAuth == AccessControl.ClientAuthentication.need) - .orElse(false)); + EndpointCertificateSecrets endpointCertificateSecrets = deployState.endpointCertificateSecrets().get(); + + boolean enforceHandshakeClientAuth = cluster.getHttp().getAccessControl() + .map(accessControl -> accessControl.clientAuthentication) + .map(clientAuth -> clientAuth == AccessControl.ClientAuthentication.need) + .orElse(false); - boolean enableTokenSupport = state.featureFlags().enableDataplaneProxy() + boolean enableTokenSupport = deployState.featureFlags().enableDataplaneProxy() && cluster.getClients().stream().anyMatch(c -> !c.tokens().isEmpty()); // Set up component to generate proxy cert if token support is enabled @@ -635,13 +633,24 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { cluster.addSimpleComponent(DataplaneProxyService.class); var dataplaneProxy = new DataplaneProxy( - getDataplanePort(state), - endpointCert.certificate(), - endpointCert.key()); + getDataplanePort(deployState), + endpointCertificateSecrets.certificate(), + endpointCertificateSecrets.key()); cluster.addComponent(dataplaneProxy); } + + connectorFactory = authorizeClient + ? HostedSslConnectorFactory.withProvidedCertificateAndTruststore( + serverName, endpointCertificateSecrets, X509CertificateUtils.toPem(clientCertificates), + tlsCiphersOverride, proxyProtocolMixedMode, port, endpointConnectionTtl, enableTokenSupport) + : HostedSslConnectorFactory.withProvidedCertificate( + serverName, endpointCertificateSecrets, enforceHandshakeClientAuth, tlsCiphersOverride, + proxyProtocolMixedMode, port, endpointConnectionTtl, enableTokenSupport); + } else { + connectorFactory = HostedSslConnectorFactory.withDefaultCertificateAndTruststore( + serverName, tlsCiphersOverride, proxyProtocolMixedMode, port, + endpointConnectionTtl); } - var connectorFactory = builder.build(); cluster.getHttp().getAccessControl().ifPresent(accessControl -> accessControl.configureHostedConnector(connectorFactory)); server.addConnector(connectorFactory); } |