Do not mount ssh keys as secrets if the name is not a valid Kubernetes config map key name and not a valid host name (#19031)

* Do not mount ssh keys as secrets if the name is not a valid Kubernetes config map key name and not a valid hostname

Signed-off-by: Sergii Kabashniuk <skabashniuk@redhat.com>
7.28.x
Sergii Kabashniuk 2021-02-15 11:54:19 +02:00 committed by GitHub
parent f652d93881
commit 5f3c188c59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 168 additions and 13 deletions

View File

@ -68,5 +68,9 @@ public final class Warnings {
NOT_ABLE_TO_PROVISION_WORKSPACE_DEPLOYMENT_LABELS_OR_ANNOTATIONS_MESSAGE =
"Not able to provision workspace %s deployment labels or annotations because of invalid configuration. Reason: '%s'";
public static final int SSH_KEYS_WILL_NOT_BE_MOUNTED = 4300;
public static final String SSH_KEYS_WILL_NOT_BE_MOUNTED_MESSAGE =
"Ssh keys %s have invalid names and can't be mounted to workspace %s.";
private Warnings() {}
}

View File

@ -36,6 +36,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentSpec;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations;
/**
@ -46,6 +47,9 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.Annotations;
public class KubernetesObjectUtil {
private static final String STORAGE_PARAM = "storage";
private static final String VALID_CONFIG_MAP_KEY_NAME = "[-._a-zA-Z0-9]+";
private static final Pattern VALID_CONFIG_MAP_KEY_NAME_PATTERN =
Pattern.compile(VALID_CONFIG_MAP_KEY_NAME);
/** Checks if the specified object has the specified label. */
public static boolean isLabeled(HasMetadata source, String key, String value) {
@ -253,4 +257,9 @@ public class KubernetesObjectUtil {
.getOrDefault(CREATE_IN_CHE_INSTALLATION_NAMESPACE, FALSE.toString())
.equals(TRUE.toString());
}
/** Checks the provided name is a valid kubernetes config map key name */
public static boolean isValidConfigMapKeyName(String configMapKeyName) {
return VALID_CONFIG_MAP_KEY_NAME_PATTERN.matcher(configMapKeyName).matches();
}
}

View File

@ -14,10 +14,13 @@ package org.eclipse.che.workspace.infrastructure.kubernetes.provision;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.NOT_ABLE_TO_PROVISION_SSH_KEYS;
import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.NOT_ABLE_TO_PROVISION_SSH_KEYS_MESSAGE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesObjectUtil.isValidConfigMapKeyName;
import com.google.common.annotations.VisibleForTesting;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder;
@ -29,12 +32,14 @@ import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder;
import io.fabric8.kubernetes.api.model.VolumeBuilder;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.validation.constraints.NotNull;
import org.eclipse.che.api.core.ConflictException;
@ -47,8 +52,10 @@ import org.eclipse.che.api.workspace.server.model.impl.WarningImpl;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.commons.annotation.Traced;
import org.eclipse.che.commons.tracing.TracingTags;
import org.eclipse.che.workspace.infrastructure.kubernetes.Warnings;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodRole;
import org.eclipse.che.workspace.infrastructure.kubernetes.util.RuntimeEventsPublisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -89,13 +96,18 @@ public class SshKeysProvisioner implements ConfigurationProvisioner<KubernetesEn
private static final String SSH_SECRET_TYPE = "opaque";
public static final Pattern VALID_DOMAIN_PATTERN =
Pattern.compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$");
private static final Logger LOG = LoggerFactory.getLogger(SshKeysProvisioner.class);
private final SshManager sshManager;
private final RuntimeEventsPublisher runtimeEventsPublisher;
@Inject
public SshKeysProvisioner(SshManager sshManager) {
public SshKeysProvisioner(SshManager sshManager, RuntimeEventsPublisher runtimeEventsPublisher) {
this.sshManager = sshManager;
this.runtimeEventsPublisher = runtimeEventsPublisher;
}
@Override
@ -108,11 +120,35 @@ public class SshKeysProvisioner implements ConfigurationProvisioner<KubernetesEn
List<SshPairImpl> vcsSshPairs = getVcsSshPairs(k8sEnv, identity);
List<SshPairImpl> systemSshPairs = getSystemSshPairs(k8sEnv, identity);
List<SshPairImpl> all = new ArrayList<>(vcsSshPairs);
all.addAll(systemSshPairs);
List<SshPairImpl> allSshPairs = new ArrayList<>(vcsSshPairs);
allSshPairs.addAll(systemSshPairs);
doProvisionSshKeys(all, k8sEnv, workspaceId);
doProvisionVcsSshConfig(vcsSshPairs, k8sEnv, workspaceId);
List<String> invalidSshKeyNames =
allSshPairs
.stream()
.filter(keyPair -> !isValidSshKeyPair(keyPair))
.map(SshPairImpl::getName)
.collect(toList());
if (!invalidSshKeyNames.isEmpty()) {
String message =
format(
Warnings.SSH_KEYS_WILL_NOT_BE_MOUNTED_MESSAGE,
invalidSshKeyNames.toString(),
identity.getWorkspaceId());
LOG.warn(message);
k8sEnv.addWarning(new WarningImpl(Warnings.SSH_KEYS_WILL_NOT_BE_MOUNTED, message));
runtimeEventsPublisher.sendRuntimeLogEvent(message, ZonedDateTime.now().toString(), identity);
}
doProvisionSshKeys(
allSshPairs.stream().filter(this::isValidSshKeyPair).collect(toList()),
k8sEnv,
workspaceId);
doProvisionVcsSshConfig(
vcsSshPairs.stream().filter(this::isValidSshKeyPair).collect(toList()),
k8sEnv,
workspaceId);
}
/**
@ -298,6 +334,12 @@ public class SshKeysProvisioner implements ConfigurationProvisioner<KubernetesEn
});
}
@VisibleForTesting
boolean isValidSshKeyPair(SshPairImpl keyPair) {
return isValidConfigMapKeyName(keyPair.getName())
&& VALID_DOMAIN_PATTERN.matcher(keyPair.getName()).matches();
}
/**
* Returns the ssh configuration entry which includes host, identity file location and Host Key
* checking policy

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class KubernetesObjectUtilTest {
@Test(dataProvider = "testNames")
public void shouldTestisValidConfigMapKeyName(String nameToTest, boolean isValid) {
assertEquals(KubernetesObjectUtil.isValidConfigMapKeyName(nameToTest), isValid);
}
@DataProvider
public static Object[][] testNames() {
return new Object[][] {
new Object[] {"foo.bar", true},
new Object[] {"https://foo.bar", false},
new Object[] {"_fef_123-ah_*zz**", false},
new Object[] {"_fef_123-ah_z.z", true},
new Object[] {"a-b#-hello", false},
new Object[] {"a---------b", true},
new Object[] {"--ab--", true}
};
}
}

View File

@ -18,6 +18,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
@ -40,9 +41,11 @@ import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.ssh.server.SshManager;
import org.eclipse.che.api.ssh.server.model.impl.SshPairImpl;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.util.RuntimeEventsPublisher;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@ -66,23 +69,25 @@ public class SshKeySecretProvisionerTest {
@Mock private Container container;
@Mock RuntimeEventsPublisher runtimeEventsPublisher;
private final String someUser = "someuser";
private SshKeysProvisioner sshKeysProvisioner;
@BeforeMethod
public void setup() {
when(runtimeIdentity.getOwnerId()).thenReturn(someUser);
when(runtimeIdentity.getWorkspaceId()).thenReturn("wksp");
lenient().when(runtimeIdentity.getOwnerId()).thenReturn(someUser);
lenient().when(runtimeIdentity.getWorkspaceId()).thenReturn("wksp");
k8sEnv = KubernetesEnvironment.builder().build();
ObjectMeta podMeta = new ObjectMetaBuilder().withName("wksp").build();
when(pod.getMetadata()).thenReturn(podMeta);
when(pod.getSpec()).thenReturn(podSpec);
when(podSpec.getVolumes()).thenReturn(new ArrayList<>());
when(podSpec.getContainers()).thenReturn(Collections.singletonList(container));
when(container.getVolumeMounts()).thenReturn(new ArrayList<>());
lenient().when(pod.getMetadata()).thenReturn(podMeta);
lenient().when(pod.getSpec()).thenReturn(podSpec);
lenient().when(podSpec.getVolumes()).thenReturn(new ArrayList<>());
lenient().when(podSpec.getContainers()).thenReturn(Collections.singletonList(container));
lenient().when(container.getVolumeMounts()).thenReturn(new ArrayList<>());
k8sEnv.addPod(pod);
sshKeysProvisioner = new SshKeysProvisioner(sshManager);
sshKeysProvisioner = new SshKeysProvisioner(sshManager, runtimeEventsPublisher);
}
@Test
@ -98,6 +103,64 @@ public class SshKeySecretProvisionerTest {
assertEquals(k8sEnv.getSecrets().size(), 1);
}
@Test(dataProvider = "invalidNames")
public void shouldNotMountKeysWithInvalidKeyNames(String invalidName) throws Exception {
// given
when(sshManager.getPairs(someUser, "vcs"))
.thenReturn(
ImmutableList.of(
new SshPairImpl(someUser, "vcs", invalidName, "public", "private"),
new SshPairImpl(someUser, "vcs", "gothub.mk", "public", "private"),
new SshPairImpl(someUser, "vcs", "boombaket.barabaket.com", "public", "private")));
// when
sshKeysProvisioner.provision(k8sEnv, runtimeIdentity);
// then
Secret secret = k8sEnv.getSecrets().get("wksp-sshprivatekeys");
assertNotNull(secret);
assertEquals(secret.getData().size(), 2);
assertTrue(secret.getData().containsKey("gothub.mk"));
assertTrue(secret.getData().containsKey("boombaket.barabaket.com"));
assertFalse(secret.getData().containsKey(invalidName));
verify(runtimeEventsPublisher, times(1))
.sendRuntimeLogEvent(anyString(), anyString(), eq(runtimeIdentity));
}
@Test(dataProvider = "invalidNames")
public void shouldIgnoreInvalidKeyNames(String invalidName) throws Exception {
assertFalse(
sshKeysProvisioner.isValidSshKeyPair(
new SshPairImpl(someUser, "vcs", invalidName, "public", "private")));
}
@Test(dataProvider = "validNames")
public void shouldAcceptValidKeyNames(String validName) throws Exception {
assertTrue(
sshKeysProvisioner.isValidSshKeyPair(
new SshPairImpl(someUser, "vcs", validName, "public", "private")));
}
@DataProvider
public static Object[][] invalidNames() {
return new Object[][] {
new Object[] {"https://foo.bar"},
new Object[] {"_fef_123-ah_*zz**"},
new Object[] {"_fef_123-ah_z.z"},
new Object[] {"a-b#-hello"},
new Object[] {"--ab--"}
};
}
@DataProvider
public static Object[][] validNames() {
return new Object[][] {
new Object[] {"foo.bar"},
new Object[] {"fef-123-ahzz"},
new Object[] {"fef0123-ahz.z"},
new Object[] {"a-b.hello"},
new Object[] {"a--a----------b"}
};
}
@Test
public void addSshKeysConfigInPod() throws Exception {
String keyName1 = UUID.randomUUID().toString();