Skip to content

Commit

Permalink
Merge pull request #1571 from Vlatombe/restricted-pss-all-containers
Browse files Browse the repository at this point in the history
Apply restricted PSS to all containers when enabled
  • Loading branch information
jglick authored Jun 6, 2024
2 parents b211060 + 1dcf358 commit 4fbb009
Show file tree
Hide file tree
Showing 10 changed files with 445 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,33 @@
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");
}

@Test
public void existingSecurityContext() throws IOException {
test("existingSecurityContext");
}
}
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: true
capabilities:
drop:
- "ALL"
runAsNonRoot: true
seccompProfile:
type: "Localhost"
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,42 @@
---
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: true
seccompProfile:
type: "Localhost"
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,65 @@
---
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
runAsUser: 1000
seccompProfile:
type: "RuntimeDefault"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
volumes:
- emptyDir:
medium: ""
name: "workspace-volume"
Loading

0 comments on commit 4fbb009

Please sign in to comment.