From 0c6c62bc9893271967e218ebd6ef9c4664ea1f3e Mon Sep 17 00:00:00 2001 From: Diego Molina Date: Wed, 28 Oct 2020 13:30:41 +0100 Subject: [PATCH] [grid] Securing 0mq when user provides a certificate Fixes #7856 The initial idea was to use `Curve` security and reuse the certificate(s) provided by the user when they want HTTPS in the Grid. However, the docs for 0mq and Curve with XPUB/XSUB and security are almost non-existent. Therefore, the approach is to create a small but hard to guess registration secret based on the provided certificate, given that the user provides did not provide a registration secret. --- .../events/zeromq/ZeroMqEventBus.java | 5 +- .../openqa/selenium/grid/commands/Hub.java | 4 +- .../selenium/grid/commands/Standalone.java | 4 +- .../distributor/local/LocalDistributor.java | 6 +- .../selenium/grid/node/k8s/OneShotNode.java | 4 +- .../grid/node/local/LocalNodeFactory.java | 4 +- .../selenium/grid/router/httpd/BUILD.bazel | 1 + .../grid/router/httpd/RouterServer.java | 4 +- .../openqa/selenium/grid/security/BUILD.bazel | 1 + .../selenium/grid/security/SecretOptions.java | 71 +++++++++++++++++++ .../openqa/selenium/grid/server/BUILD.bazel | 1 - .../grid/server/BaseServerOptions.java | 5 -- 12 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 java/server/src/org/openqa/selenium/grid/security/SecretOptions.java diff --git a/java/server/src/org/openqa/selenium/events/zeromq/ZeroMqEventBus.java b/java/server/src/org/openqa/selenium/events/zeromq/ZeroMqEventBus.java index fbcf07e229c87..8b7951ec835cf 100644 --- a/java/server/src/org/openqa/selenium/events/zeromq/ZeroMqEventBus.java +++ b/java/server/src/org/openqa/selenium/events/zeromq/ZeroMqEventBus.java @@ -22,6 +22,7 @@ import org.openqa.selenium.events.EventName; import org.openqa.selenium.grid.config.Config; import org.openqa.selenium.grid.security.Secret; +import org.openqa.selenium.grid.security.SecretOptions; import org.openqa.selenium.internal.Require; import org.openqa.selenium.json.JsonInput; import org.zeromq.ZContext; @@ -59,9 +60,9 @@ public static EventBus create(Config config) { boolean bind = config.getBool(EVENTS_SECTION, "bind").orElse(false); - Secret secret = config.get("server", "registration-secret").map(Secret::new).orElse(new Secret("")); + SecretOptions secretOptions = new SecretOptions(config); - return create(new ZContext(), publish, subscribe, bind, secret); + return create(new ZContext(), publish, subscribe, bind, secretOptions.getRegistrationSecret()); } public static EventListener onRejectedEvent(Consumer handler) { diff --git a/java/server/src/org/openqa/selenium/grid/commands/Hub.java b/java/server/src/org/openqa/selenium/grid/commands/Hub.java index 16a0411cadfa7..45a08b62930d2 100644 --- a/java/server/src/org/openqa/selenium/grid/commands/Hub.java +++ b/java/server/src/org/openqa/selenium/grid/commands/Hub.java @@ -31,6 +31,7 @@ import org.openqa.selenium.grid.log.LoggingOptions; import org.openqa.selenium.grid.router.ProxyCdpIntoGrid; import org.openqa.selenium.grid.router.Router; +import org.openqa.selenium.grid.security.SecretOptions; import org.openqa.selenium.grid.server.BaseServerOptions; import org.openqa.selenium.grid.server.EventBusOptions; import org.openqa.selenium.grid.server.NetworkOptions; @@ -116,6 +117,7 @@ protected Handlers createHandlers(Config config) { handler.addHandler(sessions); BaseServerOptions serverOptions = new BaseServerOptions(config); + SecretOptions secretOptions = new SecretOptions(config); URL externalUrl; try { @@ -135,7 +137,7 @@ protected Handlers createHandlers(Config config) { bus, clientFactory, sessions, - serverOptions.getRegistrationSecret()); + secretOptions.getRegistrationSecret()); handler.addHandler(distributor); Router router = new Router(tracer, clientFactory, sessions, distributor); diff --git a/java/server/src/org/openqa/selenium/grid/commands/Standalone.java b/java/server/src/org/openqa/selenium/grid/commands/Standalone.java index 380697e3cdb54..6550c72faaf38 100644 --- a/java/server/src/org/openqa/selenium/grid/commands/Standalone.java +++ b/java/server/src/org/openqa/selenium/grid/commands/Standalone.java @@ -34,6 +34,7 @@ import org.openqa.selenium.grid.node.local.LocalNodeFactory; import org.openqa.selenium.grid.router.Router; import org.openqa.selenium.grid.security.Secret; +import org.openqa.selenium.grid.security.SecretOptions; import org.openqa.selenium.grid.server.BaseServerOptions; import org.openqa.selenium.grid.server.EventBusOptions; import org.openqa.selenium.grid.server.NetworkOptions; @@ -115,7 +116,8 @@ protected Handlers createHandlers(Config config) { EventBus bus = events.getEventBus(); BaseServerOptions serverOptions = new BaseServerOptions(config); - Secret registrationSecret = serverOptions.getRegistrationSecret(); + SecretOptions secretOptions = new SecretOptions(config); + Secret registrationSecret = secretOptions.getRegistrationSecret(); URI localhost = serverOptions.getExternalUri(); URL localhostUrl; diff --git a/java/server/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java b/java/server/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java index 762b5d224ef63..03dbbc7727edd 100644 --- a/java/server/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java +++ b/java/server/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java @@ -43,7 +43,7 @@ import org.openqa.selenium.grid.node.Node; import org.openqa.selenium.grid.node.remote.RemoteNode; import org.openqa.selenium.grid.security.Secret; -import org.openqa.selenium.grid.server.BaseServerOptions; +import org.openqa.selenium.grid.security.SecretOptions; import org.openqa.selenium.grid.server.EventBusOptions; import org.openqa.selenium.grid.server.NetworkOptions; import org.openqa.selenium.grid.sessionmap.SessionMap; @@ -113,9 +113,9 @@ public static Distributor create(Config config) { EventBus bus = new EventBusOptions(config).getEventBus(); HttpClient.Factory clientFactory = new NetworkOptions(config).getHttpClientFactory(tracer); SessionMap sessions = new SessionMapOptions(config).getSessionMap(); - BaseServerOptions serverOptions = new BaseServerOptions(config); + SecretOptions secretOptions = new SecretOptions(config); - return new LocalDistributor(tracer, bus, clientFactory, sessions, serverOptions.getRegistrationSecret()); + return new LocalDistributor(tracer, bus, clientFactory, sessions, secretOptions.getRegistrationSecret()); } @Override diff --git a/java/server/src/org/openqa/selenium/grid/node/k8s/OneShotNode.java b/java/server/src/org/openqa/selenium/grid/node/k8s/OneShotNode.java index 5d008d0c09de8..5aae9fb7163b2 100644 --- a/java/server/src/org/openqa/selenium/grid/node/k8s/OneShotNode.java +++ b/java/server/src/org/openqa/selenium/grid/node/k8s/OneShotNode.java @@ -43,6 +43,7 @@ import org.openqa.selenium.grid.node.Node; import org.openqa.selenium.grid.node.config.NodeOptions; import org.openqa.selenium.grid.security.Secret; +import org.openqa.selenium.grid.security.SecretOptions; import org.openqa.selenium.grid.server.BaseServerOptions; import org.openqa.selenium.grid.server.EventBusOptions; import org.openqa.selenium.internal.Require; @@ -117,6 +118,7 @@ public static Node create(Config config) { LoggingOptions loggingOptions = new LoggingOptions(config); EventBusOptions eventOptions = new EventBusOptions(config); BaseServerOptions serverOptions = new BaseServerOptions(config); + SecretOptions secretOptions = new SecretOptions(config); NodeOptions nodeOptions = new NodeOptions(config); Map raw = new Json().toType( @@ -142,7 +144,7 @@ public static Node create(Config config) { return new OneShotNode( loggingOptions.getTracer(), eventOptions.getEventBus(), - serverOptions.getRegistrationSecret(), + secretOptions.getRegistrationSecret(), new NodeId(UUID.randomUUID()), serverOptions.getExternalUri(), nodeOptions.getPublicGridUri().orElseThrow(() -> new ConfigException("Unable to determine public grid address")), diff --git a/java/server/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java b/java/server/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java index 4504e26ea0b19..728dad2867669 100644 --- a/java/server/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java +++ b/java/server/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java @@ -27,6 +27,7 @@ import org.openqa.selenium.grid.node.SessionFactory; import org.openqa.selenium.grid.node.config.DriverServiceSessionFactory; import org.openqa.selenium.grid.node.config.NodeOptions; +import org.openqa.selenium.grid.security.SecretOptions; import org.openqa.selenium.grid.server.BaseServerOptions; import org.openqa.selenium.grid.server.EventBusOptions; import org.openqa.selenium.grid.server.NetworkOptions; @@ -47,6 +48,7 @@ public static Node create(Config config) { BaseServerOptions serverOptions = new BaseServerOptions(config); NodeOptions nodeOptions = new NodeOptions(config); NetworkOptions networkOptions = new NetworkOptions(config); + SecretOptions secretOptions = new SecretOptions(config); Tracer tracer = loggingOptions.getTracer(); HttpClient.Factory clientFactory = networkOptions.getHttpClientFactory(tracer); @@ -56,7 +58,7 @@ public static Node create(Config config) { eventOptions.getEventBus(), serverOptions.getExternalUri(), nodeOptions.getPublicGridUri().orElseGet(serverOptions::getExternalUri), - serverOptions.getRegistrationSecret()); + secretOptions.getRegistrationSecret()); List> builders = new ArrayList<>(); ServiceLoader.load(DriverService.Builder.class).forEach(builders::add); diff --git a/java/server/src/org/openqa/selenium/grid/router/httpd/BUILD.bazel b/java/server/src/org/openqa/selenium/grid/router/httpd/BUILD.bazel index 269330f08ddcc..69a1936db687b 100644 --- a/java/server/src/org/openqa/selenium/grid/router/httpd/BUILD.bazel +++ b/java/server/src/org/openqa/selenium/grid/router/httpd/BUILD.bazel @@ -21,6 +21,7 @@ java_library( "//java/server/src/org/openqa/selenium/grid/graphql", "//java/server/src/org/openqa/selenium/grid/log", "//java/server/src/org/openqa/selenium/grid/router", + "//java/server/src/org/openqa/selenium/grid/security", "//java/server/src/org/openqa/selenium/grid/server", "//java/server/src/org/openqa/selenium/grid/sessionmap", "//java/server/src/org/openqa/selenium/grid/sessionmap/config", diff --git a/java/server/src/org/openqa/selenium/grid/router/httpd/RouterServer.java b/java/server/src/org/openqa/selenium/grid/router/httpd/RouterServer.java index 2545f23fde2a9..3e7da1f464565 100644 --- a/java/server/src/org/openqa/selenium/grid/router/httpd/RouterServer.java +++ b/java/server/src/org/openqa/selenium/grid/router/httpd/RouterServer.java @@ -33,6 +33,7 @@ import org.openqa.selenium.grid.log.LoggingOptions; import org.openqa.selenium.grid.router.ProxyCdpIntoGrid; import org.openqa.selenium.grid.router.Router; +import org.openqa.selenium.grid.security.SecretOptions; import org.openqa.selenium.grid.server.BaseServerOptions; import org.openqa.selenium.grid.server.NetworkOptions; import org.openqa.selenium.grid.server.Server; @@ -104,6 +105,7 @@ protected Handlers createHandlers(Config config) { SessionMap sessions = sessionsOptions.getSessionMap(); BaseServerOptions serverOptions = new BaseServerOptions(config); + SecretOptions secretOptions = new SecretOptions(config); DistributorOptions distributorOptions = new DistributorOptions(config); URL distributorUrl = fromUri(distributorOptions.getDistributorUri()); @@ -111,7 +113,7 @@ protected Handlers createHandlers(Config config) { tracer, clientFactory, distributorUrl, - serverOptions.getRegistrationSecret()); + secretOptions.getRegistrationSecret()); GraphqlHandler graphqlHandler = new GraphqlHandler(distributor, serverOptions.getExternalUri()); diff --git a/java/server/src/org/openqa/selenium/grid/security/BUILD.bazel b/java/server/src/org/openqa/selenium/grid/security/BUILD.bazel index ef2d14a7f025e..112d9fe6d8826 100644 --- a/java/server/src/org/openqa/selenium/grid/security/BUILD.bazel +++ b/java/server/src/org/openqa/selenium/grid/security/BUILD.bazel @@ -12,6 +12,7 @@ java_library( deps = [ "//java/client/src/org/openqa/selenium:core", "//java/client/src/org/openqa/selenium/json", + "//java/server/src/org/openqa/selenium/grid/config", "//java/client/src/org/openqa/selenium/remote/http", ], ) diff --git a/java/server/src/org/openqa/selenium/grid/security/SecretOptions.java b/java/server/src/org/openqa/selenium/grid/security/SecretOptions.java new file mode 100644 index 0000000000000..2898f997218cc --- /dev/null +++ b/java/server/src/org/openqa/selenium/grid/security/SecretOptions.java @@ -0,0 +1,71 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.grid.security; + +import org.openqa.selenium.grid.config.Config; +import org.openqa.selenium.grid.config.ConfigException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.logging.Logger; + +import static java.util.Base64.getEncoder; + +public class SecretOptions { + + private static final String SERVER_SECTION = "server"; + + private static final Logger LOG = Logger.getLogger(SecretOptions.class.getName()); + private final Config config; + + public SecretOptions(Config config) { + this.config = config; + } + + public Secret getRegistrationSecret() { + String secret = ""; + if ((isSecure() || isSelfSigned()) && config.get(SERVER_SECTION, "registration-secret").isEmpty()) { + try { + secret = getEncoder().encodeToString(Arrays.copyOfRange(Files.readAllBytes(getCertificate().toPath()), 0, 32)); + return new Secret(secret); + } catch (IOException e) { + throw new ConfigException("Cannot read the certificate file: " + e.getMessage()); + } + } + return config.get(SERVER_SECTION, "registration-secret").map(Secret::new).orElse(new Secret(secret)); + } + + private boolean isSecure() { + return config.get(SERVER_SECTION, "https-private-key").isPresent() && config.get(SERVER_SECTION, "https-certificate").isPresent(); + } + + private boolean isSelfSigned() { + return config.getBool(SERVER_SECTION, "https-self-signed").orElse(false); + } + + private File getCertificate() { + String certificatePath = config.get(SERVER_SECTION, "https-certificate").orElse(null); + if (certificatePath != null) { + return new File(certificatePath); + } + throw new ConfigException("you must provide a certificate via --https-certificate when using --https"); + } + +} diff --git a/java/server/src/org/openqa/selenium/grid/server/BUILD.bazel b/java/server/src/org/openqa/selenium/grid/server/BUILD.bazel index dc99280b12a98..08ad4b6f100c6 100644 --- a/java/server/src/org/openqa/selenium/grid/server/BUILD.bazel +++ b/java/server/src/org/openqa/selenium/grid/server/BUILD.bazel @@ -19,7 +19,6 @@ java_library( "//java/server/src/org/openqa/selenium/events", "//java/server/src/org/openqa/selenium/grid/component", "//java/server/src/org/openqa/selenium/grid/config", - "//java/server/src/org/openqa/selenium/grid/security", "//java/server/src/org/openqa/selenium/grid/web", artifact("com.beust:jcommander"), artifact("com.google.guava:guava"), diff --git a/java/server/src/org/openqa/selenium/grid/server/BaseServerOptions.java b/java/server/src/org/openqa/selenium/grid/server/BaseServerOptions.java index 82f73d2d228bb..f7b31245b566f 100644 --- a/java/server/src/org/openqa/selenium/grid/server/BaseServerOptions.java +++ b/java/server/src/org/openqa/selenium/grid/server/BaseServerOptions.java @@ -20,7 +20,6 @@ import org.openqa.selenium.WebDriverException; import org.openqa.selenium.grid.config.Config; import org.openqa.selenium.grid.config.ConfigException; -import org.openqa.selenium.grid.security.Secret; import org.openqa.selenium.net.HostIdentifier; import org.openqa.selenium.net.NetworkUtils; import org.openqa.selenium.net.PortProber; @@ -119,10 +118,6 @@ public File getCertificate() { throw new ConfigException("you must provide a certificate via --https-certificate when using --https"); } - public Secret getRegistrationSecret() { - return config.get(SERVER_SECTION, "registration-secret").map(Secret::new).orElse(new Secret("")); - } - public boolean isSelfSigned() { return config.getBool(SERVER_SECTION, "https-self-signed").orElse(false); }