From b89d89f3faa151905c83170812d89351e6f2d50a Mon Sep 17 00:00:00 2001 From: Ben Stokmans Date: Tue, 28 Apr 2026 23:22:31 +0200 Subject: [PATCH 1/3] feat: k8s pipeline seccomp profile --- conf/openmetadata.yaml | 2 +- .../distributed-test/local/server1.yaml | 1 + .../distributed-test/local/server2.yaml | 1 + .../distributed-test/local/server3.yaml | 1 + .../pipeline/k8s/K8sPipelineClient.java | 36 +++++++++++++++---- .../pipeline/k8s/K8sPipelineClientConfig.java | 14 ++++++++ .../k8s/K8sPipelineClientConfigTest.java | 25 +++++++++++++ 7 files changed, 73 insertions(+), 7 deletions(-) diff --git a/conf/openmetadata.yaml b/conf/openmetadata.yaml index 0878b27266ac..e211e0837a2f 100644 --- a/conf/openmetadata.yaml +++ b/conf/openmetadata.yaml @@ -620,12 +620,12 @@ pipelineServiceClientConfiguration: runAsGroup: ${K8S_RUN_AS_GROUP:-1000} fsGroup: ${K8S_FS_GROUP:-1000} runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"} + seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""} 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: diff --git a/docker/development/distributed-test/local/server1.yaml b/docker/development/distributed-test/local/server1.yaml index d2bd886791a2..5502537a6a9f 100644 --- a/docker/development/distributed-test/local/server1.yaml +++ b/docker/development/distributed-test/local/server1.yaml @@ -491,6 +491,7 @@ pipelineServiceClientConfiguration: runAsGroup: ${K8S_RUN_AS_GROUP:-1000} fsGroup: ${K8S_FS_GROUP:-1000} runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"} + seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""} extraEnvVars: ${K8S_EXTRA_ENV_VARS:-[]} podAnnotations: ${K8S_POD_ANNOTATIONS:-""} useOMJobOperator: ${USE_OMJOB_OPERATOR:-"true"} diff --git a/docker/development/distributed-test/local/server2.yaml b/docker/development/distributed-test/local/server2.yaml index ef1f1e00010c..e4b323cf0d5d 100644 --- a/docker/development/distributed-test/local/server2.yaml +++ b/docker/development/distributed-test/local/server2.yaml @@ -491,6 +491,7 @@ pipelineServiceClientConfiguration: runAsGroup: ${K8S_RUN_AS_GROUP:-1000} fsGroup: ${K8S_FS_GROUP:-1000} runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"} + seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""} extraEnvVars: ${K8S_EXTRA_ENV_VARS:-[]} podAnnotations: ${K8S_POD_ANNOTATIONS:-""} useOMJobOperator: ${USE_OMJOB_OPERATOR:-"true"} diff --git a/docker/development/distributed-test/local/server3.yaml b/docker/development/distributed-test/local/server3.yaml index c80666349d65..5d0151f7d53e 100644 --- a/docker/development/distributed-test/local/server3.yaml +++ b/docker/development/distributed-test/local/server3.yaml @@ -492,6 +492,7 @@ pipelineServiceClientConfiguration: runAsGroup: ${K8S_RUN_AS_GROUP:-1000} fsGroup: ${K8S_FS_GROUP:-1000} runAsNonRoot: ${K8S_RUN_AS_NON_ROOT:-"true"} + seccompProfileType: ${K8S_SECCOMP_PROFILE_TYPE:-""} extraEnvVars: ${K8S_EXTRA_ENV_VARS:-[]} podAnnotations: ${K8S_POD_ANNOTATIONS:-""} useOMJobOperator: ${USE_OMJOB_OPERATOR:-"true"} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java index e08d5b4ac5ae..d089eea07a89 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java @@ -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; @@ -1596,16 +1597,39 @@ 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; + } + + /** + * 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; + } + return new V1SeccompProfile().type(type); } @VisibleForTesting diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java index 0bbef065dd94..cdd03907ccf6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java @@ -52,6 +52,7 @@ 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 EXTRA_ENV_VARS_KEY = "extraEnvVars"; private static final String POD_ANNOTATIONS_KEY = "podAnnotations"; private static final String TOLERATIONS_KEY = "tolerations"; @@ -83,6 +84,7 @@ public class K8sPipelineClientConfig { private final Long runAsGroup; private final Long fsGroup; private final boolean runAsNonRoot; + private final String seccompProfileType; // Extra configuration private final Map extraEnvVars; @@ -122,6 +124,9 @@ public K8sPipelineClientConfig(Map 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(); // Extra configuration - parse as list like Argo does List rawExtraEnvs = parseListSafely(params.get(EXTRA_ENV_VARS_KEY)); @@ -188,6 +193,15 @@ 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", + seccompProfileType)); + } + if (!errors.isEmpty()) { throw new PipelineServiceClientException( "Invalid K8sPipelineClient configuration: " + String.join("; ", errors)); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java index 09f4a7b7747c..37071276c938 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java @@ -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; @@ -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()); @@ -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); @@ -91,6 +94,28 @@ 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 params = new HashMap<>(); + params.put("seccompProfileType", " "); + + K8sPipelineClientConfig config = new K8sPipelineClientConfig(params); + + assertNull(config.getSeccompProfileType()); + } + + @Test + void testInvalidSeccompProfileTypeIsRejected() { + Map params = new HashMap<>(); + params.put("seccompProfileType", "NotAValidProfile"); + + PipelineServiceClientException ex = + assertThrows( + PipelineServiceClientException.class, () -> new K8sPipelineClientConfig(params)); + assertTrue(ex.getMessage().contains("seccompProfileType")); } @Test From cc7845e64639bc2a6d5f0a153062be9cb036b42e Mon Sep 17 00:00:00 2001 From: Ben Stokmans Date: Wed, 29 Apr 2026 00:48:33 +0200 Subject: [PATCH 2/3] feat: add localHostProfile option for when seccompProfileType is set to Localhost --- conf/openmetadata.yaml | 1 + .../distributed-test/local/server1.yaml | 1 + .../distributed-test/local/server2.yaml | 1 + .../distributed-test/local/server3.yaml | 1 + .../pipeline/k8s/K8sPipelineClient.java | 6 +++- .../pipeline/k8s/K8sPipelineClientConfig.java | 12 +++++++ .../k8s/K8sPipelineClientConfigTest.java | 35 +++++++++++++++++++ 7 files changed, 56 insertions(+), 1 deletion(-) diff --git a/conf/openmetadata.yaml b/conf/openmetadata.yaml index e211e0837a2f..e71af378ef81 100644 --- a/conf/openmetadata.yaml +++ b/conf/openmetadata.yaml @@ -621,6 +621,7 @@ pipelineServiceClientConfiguration: 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:-[]} diff --git a/docker/development/distributed-test/local/server1.yaml b/docker/development/distributed-test/local/server1.yaml index 5502537a6a9f..dab9ec4fc840 100644 --- a/docker/development/distributed-test/local/server1.yaml +++ b/docker/development/distributed-test/local/server1.yaml @@ -492,6 +492,7 @@ pipelineServiceClientConfiguration: 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"} diff --git a/docker/development/distributed-test/local/server2.yaml b/docker/development/distributed-test/local/server2.yaml index e4b323cf0d5d..52b56d0175e9 100644 --- a/docker/development/distributed-test/local/server2.yaml +++ b/docker/development/distributed-test/local/server2.yaml @@ -492,6 +492,7 @@ pipelineServiceClientConfiguration: 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"} diff --git a/docker/development/distributed-test/local/server3.yaml b/docker/development/distributed-test/local/server3.yaml index 5d0151f7d53e..1c6b877df0b5 100644 --- a/docker/development/distributed-test/local/server3.yaml +++ b/docker/development/distributed-test/local/server3.yaml @@ -493,6 +493,7 @@ pipelineServiceClientConfiguration: 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"} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java index d089eea07a89..1cb1c77f732c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java @@ -1629,7 +1629,11 @@ private V1SeccompProfile buildSeccompProfile() { if (StringUtils.isBlank(type)) { return null; } - return new V1SeccompProfile().type(type); + V1SeccompProfile profile = new V1SeccompProfile().type(type); + if ("Localhost".equals(type)) { + profile.localhostProfile(k8sConfig.getSeccompLocalhostProfile()); + } + return profile; } @VisibleForTesting diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java index cdd03907ccf6..3e7f32f2cee8 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfig.java @@ -53,6 +53,7 @@ public class K8sPipelineClientConfig { 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"; private static final String EXTRA_ENV_VARS_KEY = "extraEnvVars"; private static final String POD_ANNOTATIONS_KEY = "podAnnotations"; private static final String TOLERATIONS_KEY = "tolerations"; @@ -85,6 +86,7 @@ public class K8sPipelineClientConfig { private final Long fsGroup; private final boolean runAsNonRoot; private final String seccompProfileType; + private final String seccompLocalhostProfile; // Extra configuration private final Map extraEnvVars; @@ -127,6 +129,9 @@ public K8sPipelineClientConfig(Map params) { 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 rawExtraEnvs = parseListSafely(params.get(EXTRA_ENV_VARS_KEY)); @@ -201,6 +206,13 @@ public void validateConfiguration() { "seccompProfileType '%s' is invalid - must be one of RuntimeDefault, Localhost, Unconfined", 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( diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java index 37071276c938..6fa937a29b2b 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientConfigTest.java @@ -118,6 +118,41 @@ void testInvalidSeccompProfileTypeIsRejected() { assertTrue(ex.getMessage().contains("seccompProfileType")); } + @Test + void testLocalhostSeccompRequiresProfilePath() { + Map params = new HashMap<>(); + params.put("seccompProfileType", "Localhost"); + + PipelineServiceClientException ex = + assertThrows( + PipelineServiceClientException.class, () -> new K8sPipelineClientConfig(params)); + assertTrue(ex.getMessage().contains("seccompLocalhostProfile")); + } + + @Test + void testLocalhostSeccompWithProfilePathIsAccepted() { + Map 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 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 void testResourceConfiguration() { Map params = new HashMap<>(); From 92c6ee896b6113cd7b9fd922696b8cedad19c536 Mon Sep 17 00:00:00 2001 From: Ben Stokmans Date: Wed, 29 Apr 2026 11:13:09 +0200 Subject: [PATCH 3/3] fix: add missing security context to other podspec builders --- .../pipeline/k8s/K8sPipelineClient.java | 4 + .../pipeline/k8s/K8sPipelineClientTest.java | 142 ++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java index 1cb1c77f732c..eba316b209b7 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClient.java @@ -1745,6 +1745,7 @@ private V1Job buildAutomationJob(Workflow workflow, String runId, String jobName new V1PodSpec() .serviceAccountName(k8sConfig.getServiceAccountName()) .restartPolicy(RESTART_POLICY_NEVER) + .securityContext(buildPodSecurityContext()) .imagePullSecrets( k8sConfig.getImagePullSecrets().isEmpty() ? null @@ -1755,6 +1756,7 @@ private V1Job buildAutomationJob(Workflow workflow, String runId, String jobName .name(CONTAINER_NAME_AUTOMATION) .image(k8sConfig.getIngestionImage()) .imagePullPolicy(k8sConfig.getImagePullPolicy()) + .securityContext(buildContainerSecurityContext()) .command(List.of(PYTHON_MAIN_PY, RUN_AUTOMATION_PY)) .env(envVars) .resources( @@ -1783,6 +1785,7 @@ private V1Job buildApplicationJob(App application, String runId, String jobName) new V1PodSpec() .serviceAccountName(k8sConfig.getServiceAccountName()) .restartPolicy(RESTART_POLICY_NEVER) + .securityContext(buildPodSecurityContext()) .imagePullSecrets( k8sConfig.getImagePullSecrets().isEmpty() ? null @@ -1793,6 +1796,7 @@ private V1Job buildApplicationJob(App application, String runId, String jobName) .name(CONTAINER_NAME_APPLICATION) .image(k8sConfig.getIngestionImage()) .imagePullPolicy(k8sConfig.getImagePullPolicy()) + .securityContext(buildContainerSecurityContext()) .command( List.of( PYTHON_MAIN_PY, diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientTest.java index 06e873b55282..3873ded5f165 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/clients/pipeline/k8s/K8sPipelineClientTest.java @@ -46,6 +46,7 @@ import io.kubernetes.client.openapi.models.V1Pod; import io.kubernetes.client.openapi.models.V1PodList; import io.kubernetes.client.openapi.models.V1PodSpec; +import io.kubernetes.client.openapi.models.V1SeccompProfile; import io.kubernetes.client.openapi.models.V1Secret; import io.kubernetes.client.openapi.models.V1Status; import java.lang.reflect.Field; @@ -1686,6 +1687,147 @@ void testSanitizeName_WithPrefixFitsK8sLimit() { "ConfigMap name should fit Kubernetes limits"); } + @Test + void testSeccompProfileRuntimeDefaultIsAppliedToIngestionJob() throws Exception { + K8sPipelineClient seccompClient = + createClientWithSeccompProfile("RuntimeDefault", null); + + IngestionPipeline pipeline = createTestPipeline("test-pipeline", null); + when(batchApi.createNamespacedJob(eq(NAMESPACE), any())).thenReturn(createJobRequest); + when(createJobRequest.execute()).thenReturn(new V1Job()); + + seccompClient.runPipeline(pipeline, testService); + + ArgumentCaptor jobCaptor = ArgumentCaptor.forClass(V1Job.class); + verify(batchApi).createNamespacedJob(eq(NAMESPACE), jobCaptor.capture()); + + V1PodSpec podSpec = jobCaptor.getValue().getSpec().getTemplate().getSpec(); + V1SeccompProfile podProfile = podSpec.getSecurityContext().getSeccompProfile(); + assertNotNull(podProfile, "Pod-level seccompProfile should be set"); + assertEquals("RuntimeDefault", podProfile.getType()); + assertNull(podProfile.getLocalhostProfile()); + + V1SeccompProfile containerProfile = + podSpec.getContainers().get(0).getSecurityContext().getSeccompProfile(); + assertNotNull(containerProfile, "Container-level seccompProfile should be set"); + assertEquals("RuntimeDefault", containerProfile.getType()); + } + + @Test + void testSeccompProfileLocalhostIncludesLocalhostProfilePath() throws Exception { + K8sPipelineClient seccompClient = + createClientWithSeccompProfile("Localhost", "profiles/audit.json"); + + IngestionPipeline pipeline = createTestPipeline("test-pipeline", null); + when(batchApi.createNamespacedJob(eq(NAMESPACE), any())).thenReturn(createJobRequest); + when(createJobRequest.execute()).thenReturn(new V1Job()); + + seccompClient.runPipeline(pipeline, testService); + + ArgumentCaptor jobCaptor = ArgumentCaptor.forClass(V1Job.class); + verify(batchApi).createNamespacedJob(eq(NAMESPACE), jobCaptor.capture()); + + V1SeccompProfile podProfile = + jobCaptor + .getValue() + .getSpec() + .getTemplate() + .getSpec() + .getSecurityContext() + .getSeccompProfile(); + assertEquals("Localhost", podProfile.getType()); + assertEquals("profiles/audit.json", podProfile.getLocalhostProfile()); + } + + @Test + void testSeccompProfileUnsetByDefault() throws Exception { + // The default client (no seccompProfileType configured) should not emit a seccompProfile. + IngestionPipeline pipeline = createTestPipeline("test-pipeline", null); + when(batchApi.createNamespacedJob(eq(NAMESPACE), any())).thenReturn(createJobRequest); + when(createJobRequest.execute()).thenReturn(new V1Job()); + + client.runPipeline(pipeline, testService); + + ArgumentCaptor jobCaptor = ArgumentCaptor.forClass(V1Job.class); + verify(batchApi).createNamespacedJob(eq(NAMESPACE), jobCaptor.capture()); + + assertNull( + jobCaptor + .getValue() + .getSpec() + .getTemplate() + .getSpec() + .getSecurityContext() + .getSeccompProfile(), + "Default client should not set a seccompProfile"); + } + + @Test + void testSeccompProfileAppliedToAutomationAndApplicationJobs() throws Exception { + K8sPipelineClient seccompClient = + createClientWithSeccompProfile("RuntimeDefault", null); + + when(batchApi.createNamespacedJob(eq(NAMESPACE), any())).thenReturn(createJobRequest); + when(createJobRequest.execute()).thenReturn(new V1Job()); + + seccompClient.runAutomationsWorkflow(createTestWorkflow("Nightly Cleanup")); + ArgumentCaptor automationCaptor = ArgumentCaptor.forClass(V1Job.class); + verify(batchApi).createNamespacedJob(eq(NAMESPACE), automationCaptor.capture()); + V1PodSpec automationPod = automationCaptor.getValue().getSpec().getTemplate().getSpec(); + assertNotNull( + automationPod.getSecurityContext(), + "Automation job pod must have a securityContext"); + assertEquals( + "RuntimeDefault", + automationPod.getSecurityContext().getSeccompProfile().getType()); + assertEquals( + "RuntimeDefault", + automationPod.getContainers().get(0).getSecurityContext().getSeccompProfile().getType()); + + reset(batchApi, createJobRequest); + when(batchApi.createNamespacedJob(eq(NAMESPACE), any())).thenReturn(createJobRequest); + when(createJobRequest.execute()).thenReturn(new V1Job()); + + seccompClient.runApplicationFlow(createTestApplication("Query Runner")); + ArgumentCaptor applicationCaptor = ArgumentCaptor.forClass(V1Job.class); + verify(batchApi).createNamespacedJob(eq(NAMESPACE), applicationCaptor.capture()); + V1PodSpec applicationPod = applicationCaptor.getValue().getSpec().getTemplate().getSpec(); + assertNotNull( + applicationPod.getSecurityContext(), + "Application job pod must have a securityContext"); + assertEquals( + "RuntimeDefault", + applicationPod.getSecurityContext().getSeccompProfile().getType()); + assertEquals( + "RuntimeDefault", + applicationPod.getContainers().get(0).getSecurityContext().getSeccompProfile().getType()); + } + + private K8sPipelineClient createClientWithSeccompProfile( + String seccompProfileType, String seccompLocalhostProfile) { + Parameters params = new Parameters(); + params.setAdditionalProperty("namespace", NAMESPACE); + params.setAdditionalProperty("inCluster", "false"); + params.setAdditionalProperty("skipInit", "true"); + params.setAdditionalProperty("ingestionImage", "openmetadata/ingestion:test"); + params.setAdditionalProperty("serviceAccountName", "test-sa"); + params.setAdditionalProperty("seccompProfileType", seccompProfileType); + if (seccompLocalhostProfile != null) { + params.setAdditionalProperty("seccompLocalhostProfile", seccompLocalhostProfile); + } + + PipelineServiceClientConfiguration config = new PipelineServiceClientConfiguration(); + config.setEnabled(true); + config.setMetadataApiEndpoint("http://localhost:8585/api"); + config.setParameters(params); + + K8sPipelineClient seccompClient = new K8sPipelineClient(config); + seccompClient.setBatchApi(batchApi); + seccompClient.setCoreApi(coreApi); + setField(seccompClient, "customObjectsApi", customObjectsApi); + return seccompClient; + } + private static Map toEnvMap(List envVars) { return envVars.stream() .collect(Collectors.toMap(V1EnvVar::getName, V1EnvVar::getValue, (a, b) -> b));