Skip to content

Commit

Permalink
Apply restricted PSS to all containers when enabled
Browse files Browse the repository at this point in the history
When restrictedPssSecurityContext = true in a kubernetes cloud, all created containers will be augmented with a security context compatible with "restricted" Pod Security Admission.

If a security context has been manually set earlier, this won't be overridden. In such case, the pod will be rejected and the user should correct its pod definition.
  • Loading branch information
Vlatombe committed Jun 6, 2024
1 parent b211060 commit 4018ef2
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.TcpSlaveAgentListener;
import hudson.Util;
import hudson.slaves.SlaveComputer;
Expand All @@ -51,7 +50,6 @@
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder;
import io.fabric8.kubernetes.api.model.SecurityContextBuilder;
import io.fabric8.kubernetes.api.model.Volume;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
Expand Down Expand Up @@ -671,59 +669,4 @@ private List<Long> parseSupplementalGroupList(String gids) {
}
return Collections.unmodifiableList(builder);
}

/**
* <p>
* {@link PodDecorator} allowing to inject in {@code jnlp} containers definition a {@code securityContext} definition allowing to use the
* {@code restricted} <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">Pod Security Standard</a>.
* </p>
* <p>
* See <a href="https://issues.jenkins.io/browse/JENKINS-71639">JENKINS-71639</a> for more details.
* </p>
*/
@Extension
public static class RestrictedPssSecurityContextInjector implements PodDecorator {

@NonNull
@Override
public Pod decorate(@NonNull KubernetesCloud kubernetesCloud, @NonNull Pod pod) {
if (kubernetesCloud.isRestrictedPssSecurityContext()) {
Optional<Container> maybeJNLP = pod.getSpec().getContainers().stream()
.filter(container -> JNLP_NAME.equals(container.getName()))
.findFirst();

maybeJNLP.ifPresentOrElse(
jnlp -> {
SecurityContextBuilder securityContextBuilder = null;
if (jnlp.getSecurityContext() != null) {
LOGGER.info(
() ->
"Updating the existing JNLP container Security Context due to the configured restricted PSP injection");
securityContextBuilder = new SecurityContextBuilder(jnlp.getSecurityContext());
} else {
LOGGER.fine(
() ->
"Injecting restricted PSP configuration in the JNLP container security context");
securityContextBuilder = new SecurityContextBuilder();
}
jnlp.setSecurityContext(
securityContextBuilder //
.withAllowPrivilegeEscalation(false) //
.withNewCapabilities() //
.withDrop("ALL") //
.endCapabilities() //
.withRunAsNonRoot() //
.editOrNewSeccompProfile() //
.withType("RuntimeDefault") //
.endSeccompProfile() //
.build()); //
},
() -> {
throw new IllegalStateException(
"Cannot find the jnlp container when trying configuring its securityContext for restricted PSS.");
});
}
return pod;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.csanchez.jenkins.plugins.kubernetes;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import io.fabric8.kubernetes.api.model.CapabilitiesBuilder;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.SeccompProfileBuilder;
import io.fabric8.kubernetes.api.model.SecurityContext;
import java.util.List;
import java.util.logging.Logger;
import org.csanchez.jenkins.plugins.kubernetes.pod.decorator.PodDecorator;

/**
* <p>
* {@link PodDecorator} allowing to inject in all containers a {@code securityContext} allowing to use the
* {@code restricted} <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/">Pod Security Standard</a>.
* </p>
* <p>
* See <a href="https://issues.jenkins.io/browse/JENKINS-71639">JENKINS-71639</a> for more details.
* </p>
*/
@Extension
public class RestrictedPssSecurityContextInjector implements PodDecorator {
private static final Logger LOGGER = Logger.getLogger(RestrictedPssSecurityContextInjector.class.getName());
private static final String SECCOMP_RUNTIME_DEFAULT = "RuntimeDefault";
private static final String CAPABILITIES_ALL = "ALL";

@NonNull
@Override
public Pod decorate(@NonNull KubernetesCloud kubernetesCloud, @NonNull Pod pod) {
if (kubernetesCloud.isRestrictedPssSecurityContext()) {
var metadata = pod.getMetadata();
if (metadata == null) {
// be defensive, this won't happen in real usage
LOGGER.warning("No metadata found in the pod, skipping the security context update");
return pod;
}
var ns = metadata.getNamespace();
var name = metadata.getName();
LOGGER.fine(() -> "Updating pod + " + ns + "/" + name
+ " containers security context due to the configured restricted Pod Security Admission");
var spec = pod.getSpec();
if (spec == null) {
// be defensive, this won't happen in real usage
LOGGER.warning("No spec found in the pod, skipping the security context update");
return pod;
}
var containers = spec.getContainers();
if (containers != null) {
for (var container : containers) {
var securityContext = container.getSecurityContext();
if (securityContext == null) {
securityContext = new SecurityContext();
container.setSecurityContext(securityContext);
}
if (securityContext.getAllowPrivilegeEscalation() == null) {
securityContext.setAllowPrivilegeEscalation(false);
}
if (securityContext.getRunAsNonRoot() == null) {
securityContext.setRunAsNonRoot(true);
}
var seccompProfile = securityContext.getSeccompProfile();
if (seccompProfile == null) {
securityContext.setSeccompProfile(new SeccompProfileBuilder()
.withType(SECCOMP_RUNTIME_DEFAULT)
.build());
}
var capabilities = securityContext.getCapabilities();
if (capabilities == null) {
securityContext.setCapabilities(new CapabilitiesBuilder()
.withDrop(List.of(CAPABILITIES_ALL))
.build());
}
}
}
}
return pod;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.csanchez.jenkins.plugins.kubernetes;

import static org.junit.Assert.assertEquals;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.utils.Serialization;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.csanchez.jenkins.plugins.kubernetes.pod.decorator.PodDecorator;
import org.junit.Before;

abstract class AbstractGoldenFileTest {

protected KubernetesCloud cloud;
protected PodDecorator decorator;

@Before
public void setUpCloud() {
decorator = newDecorator();
cloud = new KubernetesCloud("test");
}

protected abstract PodDecorator newDecorator();

protected void test(String name) throws IOException {
var beforeYAML = loadFileAsStream(name + "-before.yaml");
var before = Serialization.unmarshal(beforeYAML, Pod.class);
assertEquals(name + "-before.yaml is not normalized", beforeYAML, Serialization.asYaml(before));
var afterYAML = loadFileAsStream(name + "-after.yaml");
var after = decorator.decorate(cloud, before);
assertEquals(name + "-after.yaml processed", afterYAML, Serialization.asYaml(after));
}

@NonNull
private String loadFileAsStream(String name) throws IOException {
var is = getClass().getResourceAsStream(getClass().getSimpleName() + "/" + name);
if (is == null) {
throw new IllegalStateException("Test file \"src/test/resources/"
+ getClass().getPackageName().replace(".", "/") + "/"
+ getClass().getSimpleName() + "/" + name + "\" not found");
}
return IOUtils.toString(is, StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.csanchez.jenkins.plugins.kubernetes;

import java.io.IOException;
import org.csanchez.jenkins.plugins.kubernetes.pod.decorator.PodDecorator;
import org.junit.Before;
import org.junit.Test;

public class RestrictedPssSecurityInjectorTest extends AbstractGoldenFileTest {
@Before
public void configureCloud() {
cloud.setRestrictedPssSecurityContext(true);
}

@Override
protected PodDecorator newDecorator() {
return new RestrictedPssSecurityContextInjector();
}

@Test
public void simple() throws IOException {
test("simple");
}

@Test
public void multiContainer() throws IOException {
test("multiContainer");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
apiVersion: "v1"
kind: "Pod"
metadata:
name: "simple"
namespace: "jenkins"
spec:
containers:
- env:
- name: "JENKINS_SECRET"
value: "my-little-secret"
- name: "JENKINS_AGENT_NAME"
value: "my-lovely-agent"
- name: "REMOTING_OPTS"
value: "-noReconnectAfter 1d"
- name: "JENKINS_NAME"
value: "my-lovely-agent"
- name: "JENKINS_AGENT_WORKDIR"
value: "/home/jenkins/agent"
- name: "JENKINS_URL"
value: "http://localhost/"
image: "jenkins/inbound-agent"
name: "jnlp"
resources:
limits:
cpu: "1"
memory: "768Mi"
requests:
cpu: "1"
memory: "768Mi"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
runAsNonRoot: true
seccompProfile:
type: "RuntimeDefault"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
- args:
- "infinity"
command:
- "sleep"
image: "maven"
name: "maven"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
runAsNonRoot: true
seccompProfile:
type: "RuntimeDefault"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
volumes:
- emptyDir:
medium: ""
name: "workspace-volume"
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
apiVersion: "v1"
kind: "Pod"
metadata:
name: "simple"
namespace: "jenkins"
spec:
containers:
- env:
- name: "JENKINS_SECRET"
value: "my-little-secret"
- name: "JENKINS_AGENT_NAME"
value: "my-lovely-agent"
- name: "REMOTING_OPTS"
value: "-noReconnectAfter 1d"
- name: "JENKINS_NAME"
value: "my-lovely-agent"
- name: "JENKINS_AGENT_WORKDIR"
value: "/home/jenkins/agent"
- name: "JENKINS_URL"
value: "http://localhost/"
image: "jenkins/inbound-agent"
name: "jnlp"
resources:
limits:
cpu: "1"
memory: "768Mi"
requests:
cpu: "1"
memory: "768Mi"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
- args:
- "infinity"
command:
- "sleep"
image: "maven"
name: "maven"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
volumes:
- emptyDir:
medium: ""
name: "workspace-volume"
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
apiVersion: "v1"
kind: "Pod"
metadata:
name: "simple"
namespace: "jenkins"
spec:
containers:
- env:
- name: "JENKINS_SECRET"
value: "my-little-secret"
- name: "JENKINS_AGENT_NAME"
value: "my-lovely-agent"
- name: "REMOTING_OPTS"
value: "-noReconnectAfter 1d"
- name: "JENKINS_NAME"
value: "my-lovely-agent"
- name: "JENKINS_AGENT_WORKDIR"
value: "/home/jenkins/agent"
- name: "JENKINS_URL"
value: "http://localhost/"
image: "jenkins/inbound-agent"
name: "jnlp"
resources:
limits:
cpu: "1"
memory: "768Mi"
requests:
cpu: "1"
memory: "768Mi"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
runAsNonRoot: true
seccompProfile:
type: "RuntimeDefault"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
volumes:
- emptyDir:
medium: ""
name: "workspace-volume"
Loading

0 comments on commit 4018ef2

Please sign in to comment.