Skip to content

Commit

Permalink
Add TLS encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
tkohn authored Oct 8, 2021
1 parent cc53939 commit 57cd637
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 2 deletions.
61 changes: 61 additions & 0 deletions src/main/java/com/instana/operator/AgentDeployer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);

Expand All @@ -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() {
Expand All @@ -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);
Expand Down Expand Up @@ -333,6 +347,8 @@ DaemonSet newDaemonSet(InstanaAgent owner,
.build());
}

configureTlsEncryption(container, owner, daemonSet, config);

return daemonSet;
}

Expand Down Expand Up @@ -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<Secret, SecretList, DoneableSecret, Resource<Secret, DoneableSecret>> 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<String, String> data = new HashMap<String, String>() {{
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ public class InstanaAgentSpec {
private String clusterName;
@JsonProperty(value = "agent.env")
private Map<String, String> 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<String, String> getConfigFiles() {
if (configFiles == null) {
Expand Down Expand Up @@ -265,6 +271,30 @@ public void setAgentEnv(Map<String, String> 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.
Expand Down
16 changes: 16 additions & 0 deletions src/main/resources/instana-agent-tls.secret.yaml
Original file line number Diff line number Diff line change
@@ -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
66 changes: 64 additions & 2 deletions src/test/java/com/instana/operator/AgentDeployerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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<VolumeMount> 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<Secret, SecretList> 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<VolumeMount> 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);
}
Expand Down
39 changes: 39 additions & 0 deletions src/test/java/com/instana/operator/cache/StubCache.java
Original file line number Diff line number Diff line change
@@ -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<T extends HasMetadata, L extends KubernetesResourceList<T>> extends Cache<T, L> {

public StubCache() {
super(null, null);
}

@Override
public Observable<CacheEvent> listThenWatch(FilterWatchListDeletable<T, L, Boolean, Watch> op) {
return new Observable<CacheEvent>() {
@Override
protected void subscribeActual(Observer<? super CacheEvent> observer) {
}

};
}

@Override
public Observable<CacheEvent> listThenWatch(ListerWatcher<T, L> op) {
return new Observable<CacheEvent>() {
@Override
protected void subscribeActual(Observer<? super CacheEvent> observer) {

}
};
}
}

0 comments on commit 57cd637

Please sign in to comment.