Skip to content

Commit

Permalink
Add JWTProxy based implementation for SecureServerExposer
Browse files Browse the repository at this point in the history
  • Loading branch information
sleshchenko committed Jul 5, 2018
1 parent cff9ccc commit f1bf970
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -494,4 +494,6 @@ che.workspace.feature.api=NULL
# Suitable values:
# - 'default': no additionally authentication system will be enabled.
# So servers should authenticate requests themselves.
# - 'jwtproxy': jwtproxy will authenticate requests.
# So servers will receive only authenticated ones.
che.agents.auth.secure_server_exposer=default
4 changes: 4 additions & 0 deletions infrastructures/kubernetes/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@
<groupId>org.eclipse.che.infrastructure.docker</groupId>
<artifactId>docker-environment</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-machine-authentication</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposerStrategyProvider;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.MultiHostIngressExternalServerExposer;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.SingleHostIngressExternalServerExposer;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxySecureServerExposerFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.wsnext.KubernetesWorkspaceNextApplier;

/** @author Sergii Leshchenko */
Expand All @@ -70,6 +71,12 @@ protected void configure() {
install(new FactoryModuleBuilder().build(KubernetesRuntimeFactory.class));
install(new FactoryModuleBuilder().build(KubernetesBootstrapperFactory.class));
install(new FactoryModuleBuilder().build(StartSynchronizerFactory.class));

install(
new FactoryModuleBuilder()
.build(
new TypeLiteral<JwtProxySecureServerExposerFactory<KubernetesEnvironment>>() {}));

bind(WorkspacePVCCleaner.class).asEagerSingleton();
bind(RemoveNamespaceOnWorkspaceRemove.class).asEagerSingleton();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposerStrategy;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.SecureServerExposer.DefaultSecureServerExposer;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxySecureServerExposerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -27,15 +28,18 @@ public class SecureServerExposerFactory<T extends KubernetesEnvironment> {
private final String serverExposer;

private final ExternalServerExposerStrategy<T> exposerStrategy;
private final JwtProxySecureServerExposerFactory<T> jwtProxySecureServerExposerFactory;

@Inject
public SecureServerExposerFactory(
@Named("che.agents.auth_enabled") boolean agentsAuthEnabled,
@Named("che.agents.auth.secure_server_exposer") String serverExposer,
ExternalServerExposerStrategy<T> exposerStrategy) {
ExternalServerExposerStrategy<T> exposerStrategy,
JwtProxySecureServerExposerFactory<T> jwtProxySecureServerExposerFactory) {
this.agentsAuthEnabled = agentsAuthEnabled;
this.serverExposer = serverExposer;
this.exposerStrategy = exposerStrategy;
this.jwtProxySecureServerExposerFactory = jwtProxySecureServerExposerFactory;
}

/**
Expand All @@ -49,6 +53,8 @@ public SecureServerExposer<T> create(RuntimeIdentity identity) {
}

switch (serverExposer) {
case "jwtproxy":
return jwtProxySecureServerExposerFactory.create(identity);
case "default":
return new DefaultSecureServerExposer<>(exposerStrategy);
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy;

import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.JWT_PROXY_CONFIG_FOLDER;
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.JWT_PROXY_PUBLIC_KEY_FILE;

import java.util.ArrayList;
import java.util.List;

/** @author Sergii Leshchenko */
public class JwtProxyConfigBuilder {
private List<VerifierProxy> verifierProxies = new ArrayList<>();

public void addVerifierProxy(Integer listenPort, String upstream) {
verifierProxies.add(new VerifierProxy(listenPort, upstream));
}

// TODO Replace it with POJO and just serialize it to yaml
public String build() {
StringBuilder configBuilder = new StringBuilder();
configBuilder.append("jwtproxy:\n" + " verifier_proxies:\n");

for (VerifierProxy verifierProxy : verifierProxies) {
configBuilder.append(
String.format(
" - listen_addr: :%s #:4471\n"
+ " verifier:\n"
+ " upstream: %s # http://localhost:4401/\n"
+ " audience: http://nginx/\n"
+ " max_skew: 1m\n"
+ " max_ttl: 3h\n"
+ " key_server:\n"
+ " type: preshared\n"
+ " options:\n"
+ " issuer: wsmaster\n"
+ " key_id: mykey\n"
+ " public_key_path: "
+ JWT_PROXY_CONFIG_FOLDER
+ "/"
+ JWT_PROXY_PUBLIC_KEY_FILE
+ "\n"
+ " claims_verifiers:\n"
+ " - type: static\n"
+ " options:\n"
+ " iss: wsmaster\n",
verifierProxy.listenPort,
verifierProxy.upstream));
}
configBuilder.append(" signer_proxy:\n" + " enabled: false\n");
return configBuilder.toString();
}

private class VerifierProxy {
private Integer listenPort;
private String upstream;

VerifierProxy(Integer listenPort, String upstream) {
this.listenPort = listenPort;
this.upstream = upstream;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static org.eclipse.che.commons.lang.NameGenerator.generate;
import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL;
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_PREFIX;
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_UNIQUE_PART_SIZE;

import com.google.common.collect.ImmutableMap;
import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.ServicePortBuilder;
import io.fabric8.kubernetes.api.model.VolumeBuilder;
import io.fabric8.kubernetes.api.model.VolumeMount;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.che.api.core.model.workspace.config.MachineConfig;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.ServerServiceBuilder;

/** @author Sergii Leshchenko */
public class JwtProxyProvisioner {

private static final int FIRST_AVAILABLE_PORT = 4400;

private static final int JWT_PROXY_MEMORY_LIMIT_BYTES = 128 * 1024 * 1024; // 128mb

private static final String PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----\n";
private static final String PUBLIC_KEY_FOOTER = "\n-----END PUBLIC KEY-----";

private static final String JWTPROXY_IMAGE = "ksmster/jwtproxy";
private static final String JWT_PROXY_CONFIG_FILE = "config.yaml";
private static final String JWT_PROXY_MACHINE_NAME = "jwtproxy";

static final String JWT_PROXY_CONFIG_FOLDER = "/config";
static final String JWT_PROXY_PUBLIC_KEY_FILE = "mykey.pub";

private final SignatureKeyManager signatureKeyManager;

private final RuntimeIdentity identity;

private final JwtProxyConfigBuilder proxyConfigBuilder;

private final String serviceName;
private int availablePort;

public JwtProxyProvisioner(RuntimeIdentity identity, SignatureKeyManager signatureKeyManager) {
this.signatureKeyManager = signatureKeyManager;

this.identity = identity;

this.proxyConfigBuilder = new JwtProxyConfigBuilder();

this.serviceName = generate(SERVER_PREFIX, SERVER_UNIQUE_PART_SIZE) + "-jwtproxy";
this.availablePort = FIRST_AVAILABLE_PORT;
}

/**
* Modifies Kubernetes environment to expose the specified service port via JWTProxy.
*
* @param k8sEnv Kubernetes environment to modify
* @param backendServiceName service name that will be exposed
* @param backendServicePort service port that will be exposed
* @param protocol protocol that will be used for exposed port
* @return JWTProxy service port that expose the specified one
* @throws InfrastructureException if any exception occurs during port exposing
*/
public ServicePort expose(
KubernetesEnvironment k8sEnv,
String backendServiceName,
int backendServicePort,
String protocol)
throws InfrastructureException {
ensureJwtProxyInjected(k8sEnv);

int listenPort = availablePort++;

proxyConfigBuilder.addVerifierProxy(
listenPort, "http://" + backendServiceName + ":" + backendServicePort);
k8sEnv
.getSecrets()
.get(getSecretName())
.getStringData()
.put(JWT_PROXY_CONFIG_FILE, proxyConfigBuilder.build());

ServicePort exposedPort =
new ServicePortBuilder()
.withName(backendServiceName + "-" + listenPort)
.withPort(listenPort)
.withProtocol(protocol)
.withNewTargetPort(listenPort)
.build();

k8sEnv.getServices().get(getServiceName()).getSpec().getPorts().add(exposedPort);

return exposedPort;
}

/** Returns service name that exposed JWTProxy Pod. */
public String getServiceName() {
return serviceName;
}

/** Returns secret name that will be mounted into JWTProxy Pod. */
public String getSecretName() {
return "jwtproxy-config-" + identity.getWorkspaceId();
}

private void ensureJwtProxyInjected(KubernetesEnvironment k8sEnv) throws InfrastructureException {
if (!k8sEnv.getMachines().containsKey(JWT_PROXY_MACHINE_NAME)) {
k8sEnv.getMachines().put(JWT_PROXY_MACHINE_NAME, createJwtProxyMachine());
k8sEnv.getPods().put("jwtproxy", createJwtProxyPod(identity));

KeyPair keyPair = signatureKeyManager.getKeyPair();
if (keyPair == null) {
throw new InternalInfrastructureException(
"Key pair for machine authentication does not exist");
}
Map<String, String> initSecretData = new HashMap<>();
initSecretData.put(
JWT_PROXY_PUBLIC_KEY_FILE,
PUBLIC_KEY_HEADER
+ java.util.Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded())
+ PUBLIC_KEY_FOOTER);

initSecretData.put(JWT_PROXY_CONFIG_FILE, proxyConfigBuilder.build());

Secret jwtProxySecret =
new SecretBuilder()
.withNewMetadata()
.withName(getSecretName())
.endMetadata()
.withStringData(initSecretData)
.build();
k8sEnv.getSecrets().put(jwtProxySecret.getMetadata().getName(), jwtProxySecret);

Service jwtProxyService =
new ServerServiceBuilder()
.withName(serviceName)
.withSelectorEntry(CHE_ORIGINAL_NAME_LABEL, JWT_PROXY_MACHINE_NAME)
.withMachineName(JWT_PROXY_MACHINE_NAME)
.withPorts(emptyList())
.build();
k8sEnv.getServices().put(jwtProxyService.getMetadata().getName(), jwtProxyService);
}
}

private InternalMachineConfig createJwtProxyMachine() {
return new InternalMachineConfig(
null,
emptyMap(),
emptyMap(),
ImmutableMap.of(
MachineConfig.MEMORY_LIMIT_ATTRIBUTE, Integer.toString(JWT_PROXY_MEMORY_LIMIT_BYTES)),
null);
}

private Pod createJwtProxyPod(RuntimeIdentity identity) {
return new PodBuilder()
.withNewMetadata()
.withName("jwtproxy")
.withAnnotations(
ImmutableMap.of(
"org.eclipse.che.container.verifier.machine_name", JWT_PROXY_MACHINE_NAME))
.endMetadata()
.withNewSpec()
.withContainers(
new ContainerBuilder()
.withName("verifier")
.withImage(JWTPROXY_IMAGE)
.withVolumeMounts(
new VolumeMount(
JWT_PROXY_CONFIG_FOLDER + "/", "jwtproxy-config-volume", false, null))
.withArgs("-config", JWT_PROXY_CONFIG_FOLDER + "/" + JWT_PROXY_CONFIG_FILE)
.build())
.withVolumes(
new VolumeBuilder()
.withName("jwtproxy-config-volume")
.withNewSecret()
.withSecretName("jwtproxy-config-" + identity.getWorkspaceId())
.endSecret()
.build())
.endSpec()
.build();
}
}
Loading

0 comments on commit f1bf970

Please sign in to comment.