Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion conf/openmetadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -620,12 +620,13 @@ pipelineServiceClientConfiguration:
runAsGroup: ${K8S_RUN_AS_GROUP:-1000}
fsGroup: ${K8S_FS_GROUP:-1000}
runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"}
seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""}
seccompLocalhostProfile: ${K8S_SECCOMP_LOCALHOST_PROFILE:-""}
extraEnvVars: ${K8S_EXTRA_ENV_VARS:-[]}
podAnnotations: ${K8S_POD_ANNOTATIONS:-""}
tolerations: ${K8S_TOLERATIONS:-[]}
useOMJobOperator: ${USE_OMJOB_OPERATOR:-"true"}


# no_encryption_at_rest is the default value, and it does what it says. Please read the manual on how
# to secure your instance of OpenMetadata with TLS and encryption at rest.
fernetConfiguration:
Expand Down
2 changes: 2 additions & 0 deletions docker/development/distributed-test/local/server1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,8 @@ pipelineServiceClientConfiguration:
runAsGroup: ${K8S_RUN_AS_GROUP:-1000}
fsGroup: ${K8S_FS_GROUP:-1000}
runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"}
seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""}
seccompLocalhostProfile: ${K8S_SECCOMP_LOCALHOST_PROFILE:-""}
extraEnvVars: ${K8S_EXTRA_ENV_VARS:-[]}
podAnnotations: ${K8S_POD_ANNOTATIONS:-""}
useOMJobOperator: ${USE_OMJOB_OPERATOR:-"true"}
Expand Down
2 changes: 2 additions & 0 deletions docker/development/distributed-test/local/server2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,8 @@ pipelineServiceClientConfiguration:
runAsGroup: ${K8S_RUN_AS_GROUP:-1000}
fsGroup: ${K8S_FS_GROUP:-1000}
runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"}
seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""}
seccompLocalhostProfile: ${K8S_SECCOMP_LOCALHOST_PROFILE:-""}
extraEnvVars: ${K8S_EXTRA_ENV_VARS:-[]}
podAnnotations: ${K8S_POD_ANNOTATIONS:-""}
useOMJobOperator: ${USE_OMJOB_OPERATOR:-"true"}
Expand Down
2 changes: 2 additions & 0 deletions docker/development/distributed-test/local/server3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,8 @@ pipelineServiceClientConfiguration:
runAsGroup: ${K8S_RUN_AS_GROUP:-1000}
fsGroup: ${K8S_FS_GROUP:-1000}
runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"}
seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""}
seccompLocalhostProfile: ${K8S_SECCOMP_LOCALHOST_PROFILE:-""}
extraEnvVars: ${K8S_EXTRA_ENV_VARS:-[]}
podAnnotations: ${K8S_POD_ANNOTATIONS:-""}
useOMJobOperator: ${USE_OMJOB_OPERATOR:-"true"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import io.kubernetes.client.openapi.models.V1PodSpec;
import io.kubernetes.client.openapi.models.V1PodTemplateSpec;
import io.kubernetes.client.openapi.models.V1ResourceRequirements;
import io.kubernetes.client.openapi.models.V1SeccompProfile;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1SecurityContext;
import io.kubernetes.client.util.ClientBuilder;
Expand Down Expand Up @@ -1596,16 +1597,43 @@ private V1PodSecurityContext buildPodSecurityContext() {
if (k8sConfig.getFsGroup() != null) {
context.setFsGroup(k8sConfig.getFsGroup());
}
V1SeccompProfile seccompProfile = buildSeccompProfile();
if (seccompProfile != null) {
context.setSeccompProfile(seccompProfile);
}
return context;
}

private V1SecurityContext buildContainerSecurityContext() {
return new V1SecurityContext()
.runAsNonRoot(k8sConfig.isRunAsNonRoot())
.runAsUser(k8sConfig.getRunAsUser())
.allowPrivilegeEscalation(false)
.readOnlyRootFilesystem(false) // Ingestion may need to write temp files
.capabilities(new V1Capabilities().drop(List.of("ALL")));
V1SecurityContext context =
new V1SecurityContext()
.runAsNonRoot(k8sConfig.isRunAsNonRoot())
.runAsUser(k8sConfig.getRunAsUser())
.allowPrivilegeEscalation(false)
.readOnlyRootFilesystem(false) // Ingestion may need to write temp files
.capabilities(new V1Capabilities().drop(List.of("ALL")));
V1SeccompProfile seccompProfile = buildSeccompProfile();
if (seccompProfile != null) {
context.setSeccompProfile(seccompProfile);
}
return context;
Comment thread
BenStokmans marked this conversation as resolved.
}

/**
* Build a {@link V1SeccompProfile} from the configured seccompProfileType, or {@code null} when
* unset. Setting this is required for namespaces enforcing the "restricted" Pod Security
* Standard, which mandates an explicit seccompProfile on every pod and container.
*/
private V1SeccompProfile buildSeccompProfile() {
String type = k8sConfig.getSeccompProfileType();
if (StringUtils.isBlank(type)) {
return null;
}
V1SeccompProfile profile = new V1SeccompProfile().type(type);
if ("Localhost".equals(type)) {
profile.localhostProfile(k8sConfig.getSeccompLocalhostProfile());
}
return profile;
Comment thread
BenStokmans marked this conversation as resolved.
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public class K8sPipelineClientConfig {
private static final String RUN_AS_GROUP_KEY = "runAsGroup";
private static final String FS_GROUP_KEY = "fsGroup";
private static final String RUN_AS_NON_ROOT_KEY = "runAsNonRoot";
private static final String SECCOMP_PROFILE_TYPE_KEY = "seccompProfileType";
private static final String SECCOMP_LOCALHOST_PROFILE_KEY = "seccompLocalhostProfile";
Comment thread
BenStokmans marked this conversation as resolved.
private static final String EXTRA_ENV_VARS_KEY = "extraEnvVars";
private static final String POD_ANNOTATIONS_KEY = "podAnnotations";
private static final String TOLERATIONS_KEY = "tolerations";
Expand Down Expand Up @@ -83,6 +85,8 @@ public class K8sPipelineClientConfig {
private final Long runAsGroup;
private final Long fsGroup;
private final boolean runAsNonRoot;
private final String seccompProfileType;
private final String seccompLocalhostProfile;

// Extra configuration
private final Map<String, String> extraEnvVars;
Expand Down Expand Up @@ -122,6 +126,12 @@ public K8sPipelineClientConfig(Map<String, Object> params) {
this.runAsGroup = getLongParam(params, RUN_AS_GROUP_KEY, 1000L);
this.fsGroup = getLongParam(params, FS_GROUP_KEY, 1000L);
this.runAsNonRoot = Boolean.parseBoolean(getStringParam(params, RUN_AS_NON_ROOT_KEY, "true"));
String rawSeccompProfileType = getStringParam(params, SECCOMP_PROFILE_TYPE_KEY, "");
this.seccompProfileType =
StringUtils.isBlank(rawSeccompProfileType) ? null : rawSeccompProfileType.trim();
String rawSeccompLocalhostProfile = getStringParam(params, SECCOMP_LOCALHOST_PROFILE_KEY, "");
this.seccompLocalhostProfile =
StringUtils.isBlank(rawSeccompLocalhostProfile) ? null : rawSeccompLocalhostProfile.trim();

// Extra configuration - parse as list like Argo does
List<String> rawExtraEnvs = parseListSafely(params.get(EXTRA_ENV_VARS_KEY));
Expand Down Expand Up @@ -188,6 +198,22 @@ public void validateConfiguration() {
"startingDeadlineSeconds must be non-negative (0 = no catch-up, >0 = catch-up window)");
}

// Validate seccompProfileType against the values allowed by the Kubernetes API.
if (seccompProfileType != null
&& !List.of("RuntimeDefault", "Localhost", "Unconfined").contains(seccompProfileType)) {
errors.add(
String.format(
"seccompProfileType '%s' is invalid - must be one of RuntimeDefault, Localhost, Unconfined",
Comment thread
BenStokmans marked this conversation as resolved.
seccompProfileType));
}
if ("Localhost".equals(seccompProfileType) && seccompLocalhostProfile == null) {
errors.add(
"seccompLocalhostProfile must be set when seccompProfileType is 'Localhost'");
} else if (!"Localhost".equals(seccompProfileType) && seccompLocalhostProfile != null) {
errors.add(
"seccompLocalhostProfile may only be set when seccompProfileType is 'Localhost'");
}

if (!errors.isEmpty()) {
throw new PipelineServiceClientException(
"Invalid K8sPipelineClient configuration: " + String.join("; ", errors));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -46,6 +47,7 @@ void testDefaultConfiguration() {
assertEquals(Long.valueOf(1000), config.getRunAsGroup());
assertEquals(Long.valueOf(1000), config.getFsGroup());
assertTrue(config.isRunAsNonRoot());
assertNull(config.getSeccompProfileType());
assertTrue(config.getImagePullSecrets().isEmpty());
assertTrue(config.getNodeSelector().isEmpty());
assertTrue(config.getExtraEnvVars().isEmpty());
Expand Down Expand Up @@ -75,6 +77,7 @@ void testCustomConfiguration() {
params.put("runAsGroup", "2000");
params.put("fsGroup", "2000");
params.put("runAsNonRoot", "false");
params.put("seccompProfileType", "RuntimeDefault");

K8sPipelineClientConfig config = new K8sPipelineClientConfig(params);

Expand All @@ -91,6 +94,63 @@ void testCustomConfiguration() {
assertEquals(Long.valueOf(2000), config.getRunAsGroup());
assertEquals(Long.valueOf(2000), config.getFsGroup());
assertFalse(config.isRunAsNonRoot());
assertEquals("RuntimeDefault", config.getSeccompProfileType());
}

@Test
void testSeccompProfileTypeBlankIsTreatedAsUnset() {
Map<String, Object> params = new HashMap<>();
params.put("seccompProfileType", " ");

K8sPipelineClientConfig config = new K8sPipelineClientConfig(params);

assertNull(config.getSeccompProfileType());
}

@Test
void testInvalidSeccompProfileTypeIsRejected() {
Map<String, Object> params = new HashMap<>();
params.put("seccompProfileType", "NotAValidProfile");

PipelineServiceClientException ex =
assertThrows(
PipelineServiceClientException.class, () -> new K8sPipelineClientConfig(params));
assertTrue(ex.getMessage().contains("seccompProfileType"));
}

@Test
void testLocalhostSeccompRequiresProfilePath() {
Map<String, Object> params = new HashMap<>();
params.put("seccompProfileType", "Localhost");

PipelineServiceClientException ex =
assertThrows(
PipelineServiceClientException.class, () -> new K8sPipelineClientConfig(params));
assertTrue(ex.getMessage().contains("seccompLocalhostProfile"));
}

@Test
void testLocalhostSeccompWithProfilePathIsAccepted() {
Map<String, Object> params = new HashMap<>();
params.put("seccompProfileType", "Localhost");
params.put("seccompLocalhostProfile", "profiles/audit.json");

K8sPipelineClientConfig config = new K8sPipelineClientConfig(params);

assertEquals("Localhost", config.getSeccompProfileType());
assertEquals("profiles/audit.json", config.getSeccompLocalhostProfile());
}

@Test
void testSeccompLocalhostProfileWithoutLocalhostTypeIsRejected() {
Map<String, Object> params = new HashMap<>();
params.put("seccompProfileType", "RuntimeDefault");
params.put("seccompLocalhostProfile", "profiles/audit.json");

PipelineServiceClientException ex =
assertThrows(
PipelineServiceClientException.class, () -> new K8sPipelineClientConfig(params));
assertTrue(ex.getMessage().contains("seccompLocalhostProfile"));
}

@Test
Expand Down
Loading