From 587bb8e4c64a53cc372495792f4db1e69ab9f7d0 Mon Sep 17 00:00:00 2001 From: Jakub Cechacek Date: Thu, 26 Oct 2023 16:56:57 +0200 Subject: [PATCH] DBZ-7087 Ability to protect JMX with password credentials --- k8/debeziumservers.debezium.io-v1.yml | 23 +++- .../dependent/DeploymentDependent.java | 126 +++++++++++++++++- .../operator/model/JmxAuthentication.java | 57 ++++++++ .../io/debezium/operator/model/JmxConfig.java | 21 ++- 4 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 src/main/java/io/debezium/operator/model/JmxAuthentication.java diff --git a/k8/debeziumservers.debezium.io-v1.yml b/k8/debeziumservers.debezium.io-v1.yml index bd9bd1d..7e2f8f6 100644 --- a/k8/debeziumservers.debezium.io-v1.yml +++ b/k8/debeziumservers.debezium.io-v1.yml @@ -24,13 +24,30 @@ spec: jmx: description: JMX configuration. properties: + port: + description: JMX port. + type: integer enabled: description: Whether JMX should be enabled for this Debezium Server instance. type: boolean - port: - description: JMX port. - type: integer + authentication: + description: JMX authentication config. + properties: + passwordFile: + description: JMX password file name and secret key + type: string + accessFile: + description: JMX access file name and secret key + type: string + enabled: + description: Whether JMX authentication should be enabled + for this Debezium Server instance. + type: boolean + secret: + description: Secret providing credential files + type: string + type: object type: object volumes: description: Additional volumes mounted to containers. diff --git a/src/main/java/io/debezium/operator/dependent/DeploymentDependent.java b/src/main/java/io/debezium/operator/dependent/DeploymentDependent.java index bee16b6..b1c28c4 100644 --- a/src/main/java/io/debezium/operator/dependent/DeploymentDependent.java +++ b/src/main/java/io/debezium/operator/dependent/DeploymentDependent.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -37,6 +38,7 @@ import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; import io.fabric8.kubernetes.api.model.ProbeBuilder; +import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.ServiceAccount; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeBuilder; @@ -52,12 +54,18 @@ public class DeploymentDependent extends CRUDKubernetesDependentResource con addTemplateConfigurationToPod(templates.getPod(), pod); addExternalVolumesToPod(runtime, pod); + addJmxConfigurationToPod(primary, pod); return new DeploymentBuilder() .withMetadata(new ObjectMetaBuilder() @@ -149,6 +158,45 @@ private void addExternalVolumesToPod(Runtime runtime, PodTemplateSpec pod) { volumes.addAll(runtime.getVolumes()); } + /** + * Adds JMX configuration to pod if required + * + * @param primary primary resource + * @param pod target pod + */ + private void addJmxConfigurationToPod(DebeziumServer primary, PodTemplateSpec pod) { + var jmx = primary.getSpec().getRuntime().getJmx(); + var auth = jmx.getAuthentication(); + + if (!auth.isEnabled()) { + return; + } + + var volumes = pod.getSpec().getVolumes(); + var initContainers = pod.getSpec().getInitContainers(); + + // Add JMX volumes to pod + var jmxInitVolume = new VolumeBuilder() + .withName(JMX_CONFIG_VOLUME_INIT_NAME) + .withSecret(new SecretVolumeSourceBuilder() + .withSecretName(auth.getSecret()) + .build()) + .build(); + + var jmxConfigVolume = new VolumeBuilder() + .withName(JMX_CONFIG_VOLUME_NAME) + .withEmptyDir(new EmptyDirVolumeSourceBuilder().build()) + .build(); + + volumes.add(jmxInitVolume); + volumes.add(jmxConfigVolume); + + // Add JMX init container + var image = getTaggedImage(primary); + var container = desiredJmxInitContainer(jmx, image); + container.ifPresent(initContainers::add); + } + private void addTemplateConfigurationToContainer(ContainerTemplate template, Container container) { var containerEnv = template.getEnv() .stream() @@ -232,6 +280,36 @@ private Container desiredServerContainer(DebeziumServer primary) { return container; } + /** + * Creates desired JMX init container + * + * @param jmx jmx configuration + * @return init container or empty optional + */ + private Optional desiredJmxInitContainer(JmxConfig jmx, String image) { + var auth = jmx.getAuthentication(); + + if (!auth.isEnabled()) { + return Optional.empty(); + } + + var initContainer = new ContainerBuilder() + .withName("server-init") + .withImage(image) + .withCommand("sh", "-c", JmxCmd.of(auth.getAccessFile()) + " && " + JmxCmd.of(auth.getPasswordFile())) + .addToVolumeMounts(new VolumeMountBuilder() + .withName(JMX_CONFIG_VOLUME_INIT_NAME) + .withMountPath(JMX_CONFIG_VOLUME_INIT_PATH) + .build()) + .addToVolumeMounts(new VolumeMountBuilder() + .withName(JMX_CONFIG_VOLUME_NAME) + .withMountPath(JMX_CONFIG_VOLUME_PATH) + .build()) + .build(); + + return Optional.of(initContainer); + } + /** * Adds external volume mounts to container if required * @@ -278,14 +356,33 @@ private void addJmxConfigurationToContainer(JmxConfig jmx, Container container) .withContainerPort(jmx.getPort()) .build()); - var opts = Map.of( + var opts = new HashMap<>(Map.of( "-Dcom.sun.management.jmxremote.ssl", false, "-Dcom.sun.management.jmxremote.port", jmx.getPort(), "-Dcom.sun.management.jmxremote.rmi.port", jmx.getPort(), "-Dcom.sun.management.jmxremote.local.only", false, "-Djava.rmi.server.hostname", "0.0.0.0", "-Dcom.sun.management.jmxremote.verbose", true, - "-Dcom.sun.management.jmxremote.authenticate", false); + "-Dcom.sun.management.jmxremote.authenticate", false)); + + var auth = jmx.getAuthentication(); + + // If JMX authentication is enabled + // Add JVM options and mount config files + if (auth.isEnabled()) { + opts.putAll(Map.of( + "-Dcom.sun.management.jmxremote.authenticate", true, + "-Dcom.sun.management.jmxremote.access.file", JMX_CONFIG_VOLUME_PATH + "/" + auth.getAccessFile(), + "-Dcom.sun.management.jmxremote.password.file", JMX_CONFIG_VOLUME_PATH + "/" + auth.getPasswordFile())); + + var mount = new VolumeMountBuilder() + .withName(JMX_CONFIG_VOLUME_NAME) + .withMountPath(JMX_CONFIG_VOLUME_PATH) + .withReadOnly(true) + .build(); + + container.getVolumeMounts().add(mount); + } // If JAVA_OPTS is already set (e.g. from container template) we don't want to override it mergeJavaOptsEnvVar(opts, container); @@ -342,4 +439,27 @@ private String getTaggedImage(DebeziumServer primary) { return image; } + + /** + * JMX auth file copy and permission command representation + */ + private static final class JmxCmd { + + private final String source; + private final String target; + + private JmxCmd(String file) { + source = JMX_CONFIG_VOLUME_INIT_PATH + "/" + file; + target = JMX_CONFIG_VOLUME_PATH + "/" + file; + } + + @Override + public String toString() { + return "cp '%s' '%s' && chmod 600 '%s'".formatted(source, target, target); + } + + public static JmxCmd of(String file) { + return new JmxCmd(file); + } + } } diff --git a/src/main/java/io/debezium/operator/model/JmxAuthentication.java b/src/main/java/io/debezium/operator/model/JmxAuthentication.java new file mode 100644 index 0000000..73ab41a --- /dev/null +++ b/src/main/java/io/debezium/operator/model/JmxAuthentication.java @@ -0,0 +1,57 @@ +/* + * Copyright Debezium Authors. + * + * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package io.debezium.operator.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ "enabled", "secret", "accessFile", "secretFile" }) +@JsonInclude(JsonInclude.Include.NON_DEFAULT) +public class JmxAuthentication { + @JsonPropertyDescription("Whether JMX authentication should be enabled for this Debezium Server instance.") + private boolean enabled = false; + @JsonPropertyDescription("Secret providing credential files") + @JsonProperty(required = true) + private String secret; + @JsonPropertyDescription("JMX access file name and secret key") + private String accessFile = "jmxremote.access"; + @JsonPropertyDescription("JMX password file name and secret key") + private String passwordFile = "jmxremote.password"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public String getAccessFile() { + return accessFile; + } + + public void setAccessFile(String accessFile) { + this.accessFile = accessFile; + } + + public String getPasswordFile() { + return passwordFile; + } + + public void setPasswordFile(String passwordFile) { + this.passwordFile = passwordFile; + } +} diff --git a/src/main/java/io/debezium/operator/model/JmxConfig.java b/src/main/java/io/debezium/operator/model/JmxConfig.java index f92e22d..387207a 100644 --- a/src/main/java/io/debezium/operator/model/JmxConfig.java +++ b/src/main/java/io/debezium/operator/model/JmxConfig.java @@ -9,15 +9,20 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -@JsonPropertyOrder({ "enabled", "port" }) +@JsonPropertyOrder({ "enabled", "port", "auth" }) @JsonInclude(JsonInclude.Include.NON_DEFAULT) public class JmxConfig { @JsonPropertyDescription("Whether JMX should be enabled for this Debezium Server instance.") - boolean enabled = false; - + private boolean enabled = false; @JsonPropertyDescription("JMX port.") - int port = 1099; + private int port = 1099; + @JsonPropertyDescription("JMX authentication config.") + private JmxAuthentication authentication; + + public JmxConfig() { + this.authentication = new JmxAuthentication(); + } public boolean isEnabled() { return enabled; @@ -34,4 +39,12 @@ public int getPort() { public void setPort(int port) { this.port = port; } + + public JmxAuthentication getAuthentication() { + return authentication; + } + + public void setAuthentication(JmxAuthentication authentication) { + this.authentication = authentication; + } }