Skip to content
Open
Show file tree
Hide file tree
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
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;
}

/**
* 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 Expand Up @@ -1717,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
Expand All @@ -1727,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(
Expand Down Expand Up @@ -1755,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
Expand All @@ -1765,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,
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
Loading