Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kubernetes support of SecurityContext configuration #24089

Merged
merged 1 commit into from
Mar 21, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: Kubernetes support of SecurityContext configuration
Allow to configure the security context section in the pods for Kubernetes, OpenShift and Knative.

Fix #23866
  • Loading branch information
Sgitario committed Mar 4, 2022
commit d3a366b709e8f9dc53ce8b87c6642c6e0e018954
35 changes: 35 additions & 0 deletions docs/src/main/asciidoc/deploying-to-kubernetes.adoc
Original file line number Diff line number Diff line change
@@ -712,6 +712,7 @@ The table below describe all the available configuration options.
| quarkus.kubernetes.resources.requests.memory | String | |
| quarkus.kubernetes.resources.limits.cpu | String | |
| quarkus.kubernetes.resources.limits.memory | String | |
| quarkus.kubernetes.security-context | SecurityContext | | ( see SecurityContext )
|====

Properties that use non-standard types, can be referenced by expanding the property.
@@ -796,6 +797,38 @@ Allowed values: `cluster-ip`, `node-port`, `load-balancer`, `external-name`
| hostnames | String[] | list of hostnames |
|====

.SecurityContext
|====
| Property | Type | Description | Default Value
| se-linux-options | SeLinuxOptions | |
| windows-options | WindowsOptions | |
| run-as-user | long | |
| run-as-group | long | |
| run-as-non-root | boolean | |
| supplemental-groups | long[] | |
| fs-group | long | |
| sysctls | Map<String, String> | |
| fs-group-change-policy | String | |
|====

.SeLinuxOptions
|====
| Property | Type | Description | Default Value
| level | String | |
| role | String | |
| user | String | |
| type | String | |
|====

.WindowsOptions
|====
| Property | Type | Description | Default Value
| gmsa-credential-spec-name | String | |
| gmsa-credential-spec | String | |
| run-as-user-name | String | |
| host-process | boolean | |
|====

==== Mounts and Volumes

.Mount
@@ -994,6 +1027,7 @@ The OpenShift resources can be customized in a similar approach with Kubernetes.
| quarkus.openshift.route.host | String | |
| quarkus.openshift.route.annotations | Map<String, String> | |
| quarkus.openshift.headless | boolean | | false
| quarkus.openshift.security-context | SecurityContext | | ( see SecurityContext )
|====

[#knative]
@@ -1097,6 +1131,7 @@ The generated service can be customized using the following properties:
| quarkus.knative.scale-to-zero-enabled | boolean | See link:https://knative.dev/docs/serving/autoscaling/scale-to-zero/#enable-scale-to-zero[link] | true
| quarkus.knative.revision-auto-scaling | AutoScalingConfig | | ( see AutoScalingConfig )
| quarkus.knative.global-auto-scaling | GlobalAutoScalingConfig | | ( see GlobalAutoScalingConfig )
| quarkus.knative.security-context | SecurityContext | | ( see SecurityContext )
|====

.Traffic
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.quarkus.kubernetes.deployment;

import java.util.Optional;

import io.dekorate.kubernetes.decorator.Decorator;
import io.dekorate.kubernetes.decorator.NamedResourceDecorator;
import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder;
import io.fabric8.kubernetes.api.model.PodSpecFluent;
import io.fabric8.kubernetes.api.model.SELinuxOptions;
import io.fabric8.kubernetes.api.model.SELinuxOptionsBuilder;
import io.fabric8.kubernetes.api.model.SysctlBuilder;
import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptions;
import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptionsBuilder;

public class ApplySecuritySettingsDecorator extends NamedResourceDecorator<PodSpecFluent> {

private final SecurityContextConfig securityContext;

public ApplySecuritySettingsDecorator(String resourceName, SecurityContextConfig securityContext) {
super(resourceName);
this.securityContext = securityContext;
}

@Override
public void andThenVisit(PodSpecFluent podSpec, ObjectMeta resourceMeta) {
PodSecurityContextBuilder securityContextBuilder = new PodSecurityContextBuilder();

securityContext.runAsUser.ifPresent(securityContextBuilder::withRunAsUser);
securityContext.runAsGroup.ifPresent(securityContextBuilder::withRunAsGroup);
securityContext.runAsNonRoot.ifPresent(securityContextBuilder::withRunAsNonRoot);
securityContext.supplementalGroups.ifPresent(securityContextBuilder::addAllToSupplementalGroups);
securityContext.fsGroup.ifPresent(securityContextBuilder::withFsGroup);
securityContext.sysctls.ifPresent(map -> map.entrySet().stream()
.map(entry -> new SysctlBuilder().withName(entry.getKey()).withValue(entry.getValue()).build())
.forEach(securityContextBuilder::addToSysctls));
securityContext.fsGroupChangePolicy.map(e -> e.name()).ifPresent(securityContextBuilder::withFsGroupChangePolicy);
buildSeLinuxOptions().ifPresent(securityContextBuilder::withSeLinuxOptions);
buildWindowsOptions().ifPresent(securityContextBuilder::withWindowsOptions);

podSpec.withSecurityContext(securityContextBuilder.build());
}

@Override
public Class<? extends Decorator>[] after() {
return new Class[] { ResourceProvidingDecorator.class };
}

private Optional<WindowsSecurityContextOptions> buildWindowsOptions() {
WindowsSecurityContextOptions item = null;
if (securityContext.windowsOptions.isAnyPropertySet()) {
WindowsSecurityContextOptionsBuilder builder = new WindowsSecurityContextOptionsBuilder();
securityContext.windowsOptions.gmsaCredentialSpec.ifPresent(builder::withGmsaCredentialSpec);
securityContext.windowsOptions.gmsaCredentialSpecName.ifPresent(builder::withGmsaCredentialSpecName);
securityContext.windowsOptions.hostProcess.ifPresent(builder::withHostProcess);
securityContext.windowsOptions.runAsUserName.ifPresent(builder::withRunAsUserName);
item = builder.build();
}

return Optional.ofNullable(item);
}

private Optional<SELinuxOptions> buildSeLinuxOptions() {
SELinuxOptions item = null;
if (securityContext.seLinuxOptions.isAnyPropertySet()) {
SELinuxOptionsBuilder builder = new SELinuxOptionsBuilder();
securityContext.seLinuxOptions.user.ifPresent(builder::withUser);
securityContext.seLinuxOptions.role.ifPresent(builder::withRole);
securityContext.seLinuxOptions.level.ifPresent(builder::withLevel);
securityContext.seLinuxOptions.type.ifPresent(builder::withType);
item = builder.build();
}

return Optional.ofNullable(item);
}

}
Original file line number Diff line number Diff line change
@@ -435,11 +435,22 @@ public EnvVarsConfig getEnv() {
@ConfigItem
Optional<String> appConfigMap;

/**
* If set, it will copy the security context configuration provided into the generated pod settings.
*/
@ConfigItem
SecurityContextConfig securityContext;

public Optional<String> getAppSecret() {
return this.appSecret;
}

public Optional<String> getAppConfigMap() {
return this.appConfigMap;
}

@Override
public SecurityContextConfig getSecurityContext() {
return securityContext;
}
}
Original file line number Diff line number Diff line change
@@ -317,6 +317,10 @@ private static List<DecoratorBuildItem> createPodDecorators(Optional<Project> pr
result.add(new DecoratorBuildItem(target, new ApplyRequestsMemoryDecorator(name, m)));
});

if (config.getSecurityContext().isAnyPropertySet()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hack ... But I tried to make the security context optional (which it is, so it makes sense), but the configuration does not deal well with having a complex and nested configuration with optional. Concretely, if I use Optional instead of the hack isAnyPropertySet, it fails with:

java.lang.RuntimeException: java.lang.RuntimeException: java.lang.IllegalArgumentException: Can not set io.quarkus.kubernetes.deployment.SecurityContextConfig$WindowsOptions field io.quarkus.kubernetes.deployment.SecurityContextConfig.windowsOptions to java.util.Optional

This is related to #7862, which was recently closed with a workaround, but not to really give support of optional configuration groups.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my point of view this is clear as it communicates the intention and there is no room for interpretations.
Going the Optional way it would be unclear how default values are meant to be handled.
So, +1 for this.

result.add(new DecoratorBuildItem(target, new ApplySecuritySettingsDecorator(name, config.getSecurityContext())));
}

return result;
}

Original file line number Diff line number Diff line change
@@ -286,12 +286,18 @@ public enum DeploymentResourceKind {
Optional<String> appSecret;

/**
* If set, the config amp will mounted to the application container and its contents will be used for application
* If set, the config map will be mounted to the application container and its contents will be used for application
* configuration.
*/
@ConfigItem
Optional<String> appConfigMap;

/**
* If set, it will copy the security context configuration provided into the generated pod settings.
*/
@ConfigItem
SecurityContextConfig securityContext;

public Optional<String> getPartOf() {
return partOf;
}
@@ -485,4 +491,9 @@ public Optional<String> getAppConfigMap() {
public Optional<ExpositionConfig> getExposition() {
return Optional.of(ingress);
}

@Override
public SecurityContextConfig getSecurityContext() {
return securityContext;
}
}
Original file line number Diff line number Diff line change
@@ -497,6 +497,12 @@ public EnvVarsConfig getEnv() {
@ConfigItem
Optional<String> appConfigMap;

/**
* If set, it will copy the security context configuration provided into the generated pod settings.
*/
@ConfigItem
SecurityContextConfig securityContext;

public Optional<String> getAppSecret() {
return this.appSecret;
}
@@ -510,6 +516,11 @@ public Optional<ExpositionConfig> getExposition() {
return Optional.of(route);
}

@Override
public SecurityContextConfig getSecurityContext() {
return securityContext;
}

public static boolean isOpenshiftBuildEnabled(ContainerImageConfig containerImageConfig, Capabilities capabilities) {
boolean implictlyEnabled = ContainerImageCapabilitiesUtil.getActiveContainerImageCapability(capabilities)
.filter(c -> c.contains(OPENSHIFT) || c.contains(S2I)).isPresent();
Original file line number Diff line number Diff line change
@@ -84,8 +84,10 @@ default String getConfigName() {
return getClass().getSimpleName().replaceAll("Config$", "").toLowerCase();
}

public Optional<String> getAppSecret();
Optional<String> getAppSecret();

public Optional<String> getAppConfigMap();
Optional<String> getAppConfigMap();

SecurityContextConfig getSecurityContext();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package io.quarkus.kubernetes.deployment;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;

@ConfigGroup
public class SecurityContextConfig {

/**
* SELinuxOptions to be applied to the container.
*/
SeLinuxOptions seLinuxOptions;

/**
* The Windows specific settings applied to all containers.
*/
WindowsOptions windowsOptions;

/**
* The UID to run the entrypoint of the container process.
*/
@ConfigItem
Optional<Long> runAsUser;

/**
* The GID to run the entrypoint of the container process.
*/
@ConfigItem
Optional<Long> runAsGroup;

/**
* Indicates that the container must run as a non-root user.
*/
@ConfigItem
Optional<Boolean> runAsNonRoot;

/**
* A list of groups applied to the first process run in each container, in addition to the container's primary GID.
* If unspecified, no groups will be added to any container.
*/
@ConfigItem
Optional<List<Long>> supplementalGroups;

/**
* A special supplemental group that applies to all containers in a pod.
*/
@ConfigItem
Optional<Long> fsGroup;

/**
* Sysctls hold a list of namespaced sysctls used for the pod.
*/
@ConfigItem
Optional<Map<String, String>> sysctls;

/**
* It holds policies that will be used for applying fsGroup to a volume when volume is mounted.
* Values: OnRootMismatch, Always
*/
@ConfigItem
Optional<PodFSGroupChangePolicy> fsGroupChangePolicy;

protected boolean isAnyPropertySet() {
return seLinuxOptions.isAnyPropertySet() || windowsOptions.isAnyPropertySet() || runAsUser.isPresent()
|| runAsGroup.isPresent() || runAsNonRoot.isPresent() || supplementalGroups.isPresent()
|| fsGroup.isPresent() || sysctls.isPresent() || fsGroupChangePolicy.isPresent();
}

@ConfigGroup
public static class SeLinuxOptions {

/**
* The SELinux level label that applies to the container.
*/
@ConfigItem
Optional<String> level;

/**
* The SELinux role label that applies to the container.
*/
@ConfigItem
Optional<String> role;

/**
* The SELinux type label that applies to the container.
*/
@ConfigItem
Optional<String> type;

/**
* The SELinux user label that applies to the container.
*/
@ConfigItem
Optional<String> user;

protected boolean isAnyPropertySet() {
return level.isPresent() || role.isPresent() || type.isPresent() || user.isPresent();
}
}

@ConfigGroup
public static class WindowsOptions {

/**
* The name of the GMSA credential spec to use.
*/
@ConfigItem
Optional<String> gmsaCredentialSpecName;

/**
* GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the
* contents of the GMSA credential spec named by the GMSACredentialSpecName field.
*/
@ConfigItem
Optional<String> gmsaCredentialSpec;

/**
* The UserName in Windows to run the entrypoint of the container process.
*/
@ConfigItem
Optional<String> runAsUserName;

/**
* HostProcess determines if a container should be run as a 'Host Process' container.
*/
@ConfigItem
Optional<Boolean> hostProcess;

protected boolean isAnyPropertySet() {
return gmsaCredentialSpecName.isPresent() || gmsaCredentialSpec.isPresent() || runAsUserName.isPresent()
|| hostProcess.isPresent();
}
}

public enum PodFSGroupChangePolicy {
/**
* It indicates that volume's ownership and permissions will be changed only when permission and ownership of root
* directory does not match with expected permissions on the volume.
*/
OnRootMismatch,
/**
* It indicates that volume's ownership and permissions should always be changed whenever volume is mounted inside a
* Pod. This the default behavior.
*/
Always;
}
}
Loading