From 57cd637ad7cef75b925286cf146128e098189941 Mon Sep 17 00:00:00 2001 From: Torsten Kohn Date: Fri, 8 Oct 2021 09:21:48 +0200 Subject: [PATCH] Add TLS encryption --- .../com/instana/operator/AgentDeployer.java | 61 +++++++++++++++++ .../customresource/InstanaAgentSpec.java | 30 +++++++++ .../resources/instana-agent-tls.secret.yaml | 16 +++++ .../instana/operator/AgentDeployerTest.java | 66 ++++++++++++++++++- .../com/instana/operator/cache/StubCache.java | 39 +++++++++++ 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/instana-agent-tls.secret.yaml create mode 100644 src/test/java/com/instana/operator/cache/StubCache.java diff --git a/src/main/java/com/instana/operator/AgentDeployer.java b/src/main/java/com/instana/operator/AgentDeployer.java index 35374c88..e4baad42 100644 --- a/src/main/java/com/instana/operator/AgentDeployer.java +++ b/src/main/java/com/instana/operator/AgentDeployer.java @@ -47,6 +47,7 @@ @ApplicationScoped public class AgentDeployer { + private static final String DEFAULT_NAME_TLS = "instana-agent-tls"; private static final String DAEMON_SET_NAME = "instana-agent"; private static final String VERSION_LABEL = "app.kubernetes.io/version"; @@ -77,6 +78,7 @@ public class AgentDeployer { // We will watch all created resources so that we can re-create them if they are deleted. private Disposable[] watchers = null; + private Disposable tlsSecretWatcher = null; private static final Logger LOGGER = LoggerFactory.getLogger(AgentDeployer.class); @@ -99,6 +101,12 @@ public void customResourceAdded(InstanaAgent customResource) { create(DaemonSet.class, DaemonSetList.class, this::newDaemonSet, c.apps().daemonSets()), watchDaemonSets(targetNamespace) }; + + // additional watcher only exists if TLS is configured with certificate and private key + // if TLS is configured with an existing secret, it does not have to create a new secret + if (isTlsEncryptionConfigured(owner.getSpec()) && isBlank(owner.getSpec().getAgentTlsSecretName())) { + tlsSecretWatcher = create(Secret.class, SecretList.class, this::createTlsSecret, c.secrets()); + } } void customResourceDeleted() { @@ -108,6 +116,12 @@ void customResourceDeleted() { } } watchers = null; + + if (tlsSecretWatcher != null) { + tlsSecretWatcher.dispose(); + } + tlsSecretWatcher = null; + owner = null; daemonSetDeletedEvent.fireAsync(new DaemonSetDeleted(), asyncSerial) .exceptionally(fatalErrorHandler::logAndExit); @@ -333,6 +347,8 @@ DaemonSet newDaemonSet(InstanaAgent owner, .build()); } + configureTlsEncryption(container, owner, daemonSet, config); + return daemonSet; } @@ -368,6 +384,51 @@ private boolean isOpenTelemetryEnabled(InstanaAgentSpec config) { return otelActiveFromCustomResource || getBoolean(otelActiveFromEnvVar); } + private void configureTlsEncryption(Container container, InstanaAgent owner, DaemonSet daemonSet, InstanaAgentSpec config) { + if (isTlsEncryptionConfigured(config)) { + LOGGER.debug("Configure TLS encryption"); + final String secretName = isBlank(config.getAgentTlsSecretName()) ? DEFAULT_NAME_TLS : config.getAgentTlsSecretName(); + final SecretVolumeSource secretVolumeSource = new SecretVolumeSource(0440, new ArrayList<>(), false, secretName); + + daemonSet + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .add(new VolumeBuilder().withName(DEFAULT_NAME_TLS).withSecret(secretVolumeSource).build()); + + container + .getVolumeMounts() + .add(new VolumeMountBuilder() + .withName(DEFAULT_NAME_TLS) + .withReadOnly(true) + .withMountPath("/opt/instana/agent/etc/certs") + .build()); + } + } + + private boolean isTlsEncryptionConfigured(InstanaAgentSpec config) { + if (isBlank(config.getAgentTlsSecretName()) && (isBlank(config.getAgentTlsCertificate()) || isBlank(config.getAgentTlsKey()))) { + return false; + } + return true; + } + + private Secret createTlsSecret(InstanaAgent owner, + MixedOperation> op) { + final InstanaAgentSpec config = owner.getSpec(); + + LOGGER.debug("Create TLS secret with provided certificate and private key"); + Secret secret = load("instana-agent-tls.secret.yaml", owner, op); + final Map data = new HashMap() {{ + put("tls.crt", config.getAgentTlsCertificate()); + put("tls.key", config.getAgentTlsKey()); + }}; + secret.setData(data); + return secret; + + } + private Quantity mem(int value, String format) { // For some reason the format doesn't work. If we create a Quantity for "512Mi", // the resulting YAML contains only "512", which is 512 Bytes. diff --git a/src/main/java/com/instana/operator/customresource/InstanaAgentSpec.java b/src/main/java/com/instana/operator/customresource/InstanaAgentSpec.java index a0e4487f..41fd30a4 100644 --- a/src/main/java/com/instana/operator/customresource/InstanaAgentSpec.java +++ b/src/main/java/com/instana/operator/customresource/InstanaAgentSpec.java @@ -83,6 +83,12 @@ public class InstanaAgentSpec { private String clusterName; @JsonProperty(value = "agent.env") private Map agentEnv; + @JsonProperty(value = "agent.tls.secretName") + private String agentTlsSecretName; + @JsonProperty(value = "agent.tls.certificate") + private String agentTlsCertificate; + @JsonProperty(value = "agent.tls.key") + private String agentTlsKey; public Map getConfigFiles() { if (configFiles == null) { @@ -265,6 +271,30 @@ public void setAgentEnv(Map env) { agentEnv = env; } + public String getAgentTlsSecretName() { + return agentTlsSecretName; + } + + public void setAgentTlsSecretName(String agentTlsSecretName) { + this.agentTlsSecretName = agentTlsSecretName; + } + + public String getAgentTlsCertificate() { + return agentTlsCertificate; + } + + public void setAgentTlsCertificate(String agentTlsCertificate) { + this.agentTlsCertificate = agentTlsCertificate; + } + + public String getAgentTlsKey() { + return agentTlsKey; + } + + public void setAgentTlsKey(String agentTlsKey) { + this.agentTlsKey = agentTlsKey; + } + // We call equals() to check if the Spec was updated. // We serialize to YAML and compare the Strings, because this works even if somebody // adds a field and forgets to update the equals method. diff --git a/src/main/resources/instana-agent-tls.secret.yaml b/src/main/resources/instana-agent-tls.secret.yaml new file mode 100644 index 00000000..8a887c4d --- /dev/null +++ b/src/main/resources/instana-agent-tls.secret.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Secret +metadata: + name: instana-agent-tls + namespace: placeholder + ownerReferences: + - apiVersion: apps/v1 + kind: InstanaAgent + name: placeholder + uid: placeholder + labels: + app.kubernetes.io/managed-by: instana-agent-operator +type: kubernetes.io/tls +data: + tls.crt: placeholder + tls.key: placeholder diff --git a/src/test/java/com/instana/operator/AgentDeployerTest.java b/src/test/java/com/instana/operator/AgentDeployerTest.java index a7dcf444..aa6e64e5 100644 --- a/src/test/java/com/instana/operator/AgentDeployerTest.java +++ b/src/test/java/com/instana/operator/AgentDeployerTest.java @@ -5,11 +5,13 @@ package com.instana.operator; import com.google.common.collect.ImmutableMap; +import com.instana.operator.cache.Cache; +import com.instana.operator.cache.CacheService; +import com.instana.operator.cache.StubCache; import com.instana.operator.customresource.InstanaAgent; import com.instana.operator.customresource.InstanaAgentSpec; import com.instana.operator.env.Environment; -import io.fabric8.kubernetes.api.model.Container; -import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinitionBuilder; import io.fabric8.kubernetes.api.model.apps.DaemonSet; import io.fabric8.kubernetes.client.Config; @@ -23,6 +25,7 @@ import java.util.Collections; import java.util.Map; +import java.util.Optional; import static com.instana.operator.env.Environment.RELATED_IMAGE_INSTANA_AGENT; import static io.fabric8.kubernetes.client.utils.HttpClientUtils.createHttpClientForMockServer; @@ -31,6 +34,9 @@ import static okhttp3.TlsVersion.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class AgentDeployerTest { @@ -182,6 +188,62 @@ public void daemonset_must_not_include_version_label_if_not_specified_on_crd() { assertThat(labels, not(hasKey("app.kubernetes.io/version"))); } + @Test + public void daemonset_must_include_tls_mount_if_tls_secret_name_is_provided() { + AgentDeployer deployer = new AgentDeployer(); + deployer.setDefaultClient(client); + + InstanaAgentSpec agentSpec = new InstanaAgentSpec(); + deployer.setEnvironment(singleVar(RELATED_IMAGE_INSTANA_AGENT, "other/image:some-tag")); + + agentSpec.setAgentTlsSecretName("secret-name"); + + InstanaAgent customResource = new InstanaAgent(); + customResource.setSpec(agentSpec); + + DaemonSet daemonSet = deployer.newDaemonSet( + customResource, + client.inNamespace("instana-agent").apps().daemonSets()); + + Container agentContainer = getAgentContainer(daemonSet); + + Optional certsVolumeMount = agentContainer.getVolumeMounts().stream().filter(vm -> vm.getName().equals("instana-agent-tls")).findFirst(); + assertThat(certsVolumeMount.isPresent(), is(true)); + assertThat(certsVolumeMount.get().getReadOnly(), is(true)); + } + + @Test + public void daemonset_must_include_tls_mount_if_certificate_and_key_are_provided() { + AgentDeployer deployer = new AgentDeployer(); + deployer.setDefaultClient(client); + + Cache cacheMock = new StubCache<>(); + CacheService cacheServiceMock = mock(CacheService.class); + when(cacheServiceMock.newCache(eq(Secret.class), eq(SecretList.class))) + .thenReturn(cacheMock); + + deployer.cacheService = cacheServiceMock; + + InstanaAgentSpec agentSpec = new InstanaAgentSpec(); + deployer.setEnvironment(singleVar(RELATED_IMAGE_INSTANA_AGENT, "other/image:some-tag")); + + agentSpec.setAgentTlsCertificate("some-certificate"); + agentSpec.setAgentTlsKey("some-key"); + + InstanaAgent customResource = new InstanaAgent(); + customResource.setSpec(agentSpec); + + DaemonSet daemonSet = deployer.newDaemonSet( + customResource, + client.inNamespace("instana-agent").apps().daemonSets()); + + Container agentContainer = getAgentContainer(daemonSet); + + Optional certsVolumeMount = agentContainer.getVolumeMounts().stream().filter(vm -> vm.getName().equals("instana-agent-tls")).findFirst(); + assertThat(certsVolumeMount.isPresent(), is(true)); + assertThat(certsVolumeMount.get().getReadOnly(), is(true)); + } + private Container getAgentContainer(DaemonSet daemonSet) { return daemonSet.getSpec().getTemplate().getSpec().getContainers().get(0); } diff --git a/src/test/java/com/instana/operator/cache/StubCache.java b/src/test/java/com/instana/operator/cache/StubCache.java new file mode 100644 index 00000000..1c5da38e --- /dev/null +++ b/src/test/java/com/instana/operator/cache/StubCache.java @@ -0,0 +1,39 @@ +/* + * (c) Copyright IBM Corp. 2021 + * (c) Copyright Instana Inc. 2021 + */ +package com.instana.operator.cache; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.reactivex.Observable; +import io.reactivex.Observer; + +public class StubCache> extends Cache { + + public StubCache() { + super(null, null); + } + + @Override + public Observable listThenWatch(FilterWatchListDeletable op) { + return new Observable() { + @Override + protected void subscribeActual(Observer observer) { + } + + }; + } + + @Override + public Observable listThenWatch(ListerWatcher op) { + return new Observable() { + @Override + protected void subscribeActual(Observer observer) { + + } + }; + } +}