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
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ func (a *adkApiTranslator) buildManifest(
Name: env.KagentSkillsFolder.Name(),
Value: "/skills",
}
// Skills use the BashTool which calls srt (Anthropic Sandbox Runtime) → bubblewrap.
// Mark that a sandbox is needed so Privileged is set when possible.
// Exception: if the user explicitly set AllowPrivilegeEscalation=false (PSS Restricted),
// we respect their security context and let srt fall back to user-namespace sandboxing.
needSandbox = true
volumes = append(volumes, corev1.Volume{
Name: "kagent-skills",
Expand Down Expand Up @@ -515,12 +519,16 @@ func (a *adkApiTranslator) buildManifest(
if dep.SecurityContext != nil {
// Deep copy the user-provided security context
securityContext = dep.SecurityContext.DeepCopy()
// If sandbox is needed, ensure Privileged is set (may override user setting)
if needSandbox {
// Set Privileged for sandbox ONLY if it won't create an invalid securityContext.
// Kubernetes rejects {Privileged:true, AllowPrivilegeEscalation:false} as contradictory.
// When the user explicitly sets AllowPrivilegeEscalation=false (PSS Restricted namespace),
// we respect their choice: srt will use unprivileged user-namespace sandboxing instead.
// On modern kernels (EKS, GKE) unprivileged_userns_clone is enabled by default.
if needSandbox && !allowPrivilegeEscalationExplicitlyFalse(securityContext) {
securityContext.Privileged = new(true)
}
} else if needSandbox {
// Only create security context if sandbox is needed
// No user-provided securityContext: create one with Privileged for full sandbox
securityContext = &corev1.SecurityContext{
Privileged: new(true),
}
Expand Down Expand Up @@ -1800,3 +1808,11 @@ func (a *adkApiTranslator) runPlugins(ctx context.Context, agent *v1alpha2.Agent
}
return errs
}

// allowPrivilegeEscalationExplicitlyFalse reports whether the security context
// has AllowPrivilegeEscalation explicitly set to false (PSS Restricted profile).
// This is used to detect when adding Privileged:true would create an invalid
// securityContext that Kubernetes refuses to admit.
func allowPrivilegeEscalationExplicitlyFalse(sc *corev1.SecurityContext) bool {
return sc != nil && sc.AllowPrivilegeEscalation != nil && !*sc.AllowPrivilegeEscalation
}
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,83 @@ func TestSecurityContext_OnlyContainerSecurityContext(t *testing.T) {
assert.Equal(t, int64(3000), *containerSecurityContext.RunAsGroup)
}

func TestSecurityContext_WithSandbox(t *testing.T) {
// TestSecurityContext_SkillsDefaultPrivilegedSandbox verifies that when skills are
// configured and the user has NOT set any securityContext (i.e., no PSS restriction),
// the controller sets Privileged=true so that srt/bubblewrap can fully sandbox the BashTool.
func TestSecurityContext_SkillsDefaultPrivilegedSandbox(t *testing.T) {
ctx := context.Background()

agent := &v1alpha2.Agent{
ObjectMeta: metav1.ObjectMeta{
Name: "test-agent",
Namespace: "test",
},
Spec: v1alpha2.AgentSpec{
Type: v1alpha2.AgentType_Declarative,
Skills: &v1alpha2.SkillForAgent{
Refs: []string{"test-skill:latest"},
},
Declarative: &v1alpha2.DeclarativeAgentSpec{
SystemMessage: "Test agent",
ModelConfig: "test-model",
// No Deployment.SecurityContext set — default behaviour
},
},
}

modelConfig := &v1alpha2.ModelConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-model",
Namespace: "test",
},
Spec: v1alpha2.ModelConfigSpec{
Provider: "OpenAI",
Model: "gpt-4o",
},
}

scheme := schemev1.Scheme
err := v1alpha2.AddToScheme(scheme)
require.NoError(t, err)

kubeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(agent, modelConfig).
Build()

defaultModel := types.NamespacedName{
Namespace: "test",
Name: "test-model",
}
translatorInstance := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "")

result, err := translatorInstance.TranslateAgent(ctx, agent)
require.NoError(t, err)

var deployment *appsv1.Deployment
for _, obj := range result.Manifest {
if dep, ok := obj.(*appsv1.Deployment); ok {
deployment = dep
break
}
}
require.NotNil(t, deployment)
podTemplate := &deployment.Spec.Template

containerSecurityContext := podTemplate.Spec.Containers[0].SecurityContext
require.NotNil(t, containerSecurityContext, "SecurityContext should be created for sandbox")
// Without an explicit AllowPrivilegeEscalation=false constraint, skills trigger Privileged=true
// so that srt/bubblewrap can use kernel namespaces for full BashTool sandboxing.
require.NotNil(t, containerSecurityContext.Privileged, "Privileged should be set when no securityContext restriction")
assert.True(t, *containerSecurityContext.Privileged, "Privileged should be true for skills without PSS restrictions")
}

// TestSecurityContext_SkillsPSSRestricted verifies that when a user explicitly sets
// AllowPrivilegeEscalation=false (PSS Restricted profile), adding skills does NOT
// force Privileged=true — which Kubernetes rejects as an invalid combination.
// srt (Anthropic Sandbox Runtime) falls back to unprivileged user-namespace sandboxing
// on modern kernels (EKS, GKE) that have unprivileged_userns_clone enabled.
func TestSecurityContext_SkillsPSSRestricted(t *testing.T) {
ctx := context.Background()

agent := &v1alpha2.Agent{
Expand All @@ -294,8 +370,12 @@ func TestSecurityContext_WithSandbox(t *testing.T) {
Deployment: &v1alpha2.DeclarativeDeploymentSpec{
SharedDeploymentSpec: v1alpha2.SharedDeploymentSpec{
SecurityContext: &corev1.SecurityContext{
RunAsUser: new(int64(1000)),
RunAsGroup: new(int64(1000)),
RunAsUser: new(int64(1000)),
RunAsGroup: new(int64(1000)),
AllowPrivilegeEscalation: new(false),
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
},
},
},
},
Expand Down Expand Up @@ -342,9 +422,10 @@ func TestSecurityContext_WithSandbox(t *testing.T) {
require.NotNil(t, deployment)
podTemplate := &deployment.Spec.Template

// When sandbox is needed, Privileged should be set even if user provided securityContext
containerSecurityContext := podTemplate.Spec.Containers[0].SecurityContext
require.NotNil(t, containerSecurityContext)
assert.True(t, *containerSecurityContext.Privileged, "Privileged should be true when sandbox is needed")
assert.Equal(t, int64(1000), *containerSecurityContext.RunAsUser, "User-provided runAsUser should still be set")
// AllowPrivilegeEscalation=false prevents Privileged=true (invalid Kubernetes combination)
assert.Nil(t, containerSecurityContext.Privileged, "Privileged must not be set when AllowPrivilegeEscalation=false")
assert.Equal(t, int64(1000), *containerSecurityContext.RunAsUser, "User-provided runAsUser should be preserved")
assert.False(t, *containerSecurityContext.AllowPrivilegeEscalation, "AllowPrivilegeEscalation should be preserved as false")
}
Loading