diff --git a/pkg/container/docker/client.go b/pkg/container/docker/client.go index 1e7ae9ab..7cdcaf9e 100644 --- a/pkg/container/docker/client.go +++ b/pkg/container/docker/client.go @@ -869,6 +869,19 @@ func (c *Client) getPermissionConfigFromProfile( config.NetworkMode = "bridge" } + // Add seccomp profile if present + if profile.Seccomp != nil { + seccompProfile, err := generateSeccompProfile(profile) + if err != nil { + logger.Log.Warn(fmt.Sprintf("Warning: Failed to generate seccomp profile: %v", err)) + } else if seccompProfile != "" { + // For Docker, seccomp profiles are passed as seccomp=profile content + config.SecurityOpt = append(config.SecurityOpt, "seccomp="+seccompProfile) + logger.Log.Info("Applied seccomp profile with denied syscalls: " + + strings.Join(profile.Seccomp.DeniedSyscalls, ", ")) + } + } + // Validate transport type if transportType != "sse" && transportType != "stdio" { return nil, fmt.Errorf("unsupported transport type: %s", transportType) diff --git a/pkg/container/docker/seccomp.go b/pkg/container/docker/seccomp.go new file mode 100644 index 00000000..7273b264 --- /dev/null +++ b/pkg/container/docker/seccomp.go @@ -0,0 +1,85 @@ +package docker + +import ( + "encoding/json" + "fmt" + + "github.com/StacklokLabs/toolhive/pkg/logger" + "github.com/StacklokLabs/toolhive/pkg/permissions" +) + +// Docker JSON format (should be compat with podman too) +type seccompProfileTemplate struct { + DefaultAction string `json:"defaultAction"` + Architectures []string `json:"architectures,omitempty"` + Syscalls []seccompSyscallRuleTemplate `json:"syscalls"` +} + +type seccompSyscallRuleTemplate struct { + Names []string `json:"names"` + Action string `json:"action"` +} + +func generateSeccompProfile(profile *permissions.Profile) (string, error) { + if profile.Seccomp == nil { + return "", nil + } + + // Check if seccomp is explicitly disabled for this profile + // This basically returns empty profile, meaning, but does not mean no profiles + // at all, as Docker and Podman have pretty good profiles out of the box + // https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile + if !profile.Seccomp.Enabled { + logger.Log.Info("Seccomp profile generation skipped as profile.Seccomp.Enabled is false.") + return "", nil + } + + // What sort of action to take on a profile match + defaultAction := "SCMP_ACT_ERRNO" + if profile.Seccomp.DefaultAction != "" { + switch profile.Seccomp.DefaultAction { + case "allow": + defaultAction = "SCMP_ACT_ALLOW" + case "errno": + defaultAction = "SCMP_ACT_ERRNO" + case "kill": + defaultAction = "SCMP_ACT_KILL" + case "trap": + defaultAction = "SCMP_ACT_TRAP" + case "trace": + defaultAction = "SCMP_ACT_TRACE" + default: + logger.Log.Warn(fmt.Sprintf("Warning: Unknown seccomp default action: %s, using SCMP_ACT_ERRNO", profile.Seccomp.DefaultAction)) + } + } + + // seccomp profile template + seccompProfile := seccompProfileTemplate{ + DefaultAction: defaultAction, + Architectures: profile.Seccomp.Architectures, + Syscalls: []seccompSyscallRuleTemplate{}, + } + + // Add denied syscalls with ERRNO action + if len(profile.Seccomp.DeniedSyscalls) > 0 { + seccompProfile.Syscalls = append(seccompProfile.Syscalls, seccompSyscallRuleTemplate{ + Names: profile.Seccomp.DeniedSyscalls, + Action: "SCMP_ACT_ERRNO", + }) + } + + // Add allowed syscalls with ALLOW action + if len(profile.Seccomp.AllowedSyscalls) > 0 { + seccompProfile.Syscalls = append(seccompProfile.Syscalls, seccompSyscallRuleTemplate{ + Names: profile.Seccomp.AllowedSyscalls, + Action: "SCMP_ACT_ALLOW", + }) + } + + seccompJSON, err := json.Marshal(seccompProfile) + if err != nil { + return "", fmt.Errorf("failed to marshal seccomp profile: %w", err) + } + + return string(seccompJSON), nil +} diff --git a/pkg/permissions/profile.go b/pkg/permissions/profile.go index 8c4e9c2d..a5766784 100644 --- a/pkg/permissions/profile.go +++ b/pkg/permissions/profile.go @@ -34,6 +34,32 @@ type Profile struct { // Network defines network permissions Network *NetworkPermissions `json:"network,omitempty"` + + // Seccomp defines seccomp profile configuration for system call filtering + Seccomp *SeccompProfile `json:"seccomp,omitempty"` +} + +// SeccompProfile defines seccomp syscall filtering configuration +type SeccompProfile struct { + // Enabled controls whether this seccomp profile is applied at all. Defaults to true. + Enabled bool `json:"enabled"` + + // DeniedSyscalls is a list of syscalls to deny (will return EPERM) + DeniedSyscalls []string `json:"denied_syscalls,omitempty"` + + // AllowedSyscalls is a list of syscalls to explicitly allow + AllowedSyscalls []string `json:"allowed_syscalls,omitempty"` + + // DefaultAction defines the action for syscalls not in DeniedSyscalls or AllowedSyscalls + // Valid values: "allow", "errno" (default), "kill", "trap", "trace" + DefaultAction string `json:"default_action,omitempty"` + + // Architectures is a list of architectures to apply the seccomp profile to + // If not specified, the profile will apply to all architectures + // This should support all Docker architectures + // Valid values: "x86_64", "386", "arm", "arm64", "mips", "mips64", "ppc64le", "s390x" + // Note from Luke: I only checked x86_64 and arm64 so far + Architectures []string `json:"architectures,omitempty"` } // NetworkPermissions defines network permissions for a container @@ -70,6 +96,13 @@ func NewProfile() *Profile { AllowPort: []int{}, }, }, + Seccomp: &SeccompProfile{ + Enabled: true, + DeniedSyscalls: []string{"ptrace", "reboot", "kexec_load"}, + AllowedSyscalls: []string{"read", "write", "exit", "open", "close"}, + DefaultAction: "errno", + Architectures: []string{"x86_64"}, + }, } } @@ -104,6 +137,13 @@ func BuiltinNoneProfile() *Profile { AllowPort: []int{}, }, }, + Seccomp: &SeccompProfile{ + Enabled: true, + DeniedSyscalls: []string{"ptrace", "reboot", "kexec_load"}, + AllowedSyscalls: []string{"read", "write", "exit", "open", "close"}, + DefaultAction: "errno", + Architectures: []string{"x86_64"}, + }, } } @@ -120,6 +160,13 @@ func BuiltinNetworkProfile() *Profile { AllowPort: []int{}, }, }, + Seccomp: &SeccompProfile{ + Enabled: true, + DeniedSyscalls: []string{"ptrace", "reboot", "kexec_load"}, + AllowedSyscalls: []string{"read", "write", "exit", "open", "close"}, + DefaultAction: "errno", + Architectures: []string{"x86_64"}, + }, } } diff --git a/pkg/registry/data/registry.json b/pkg/registry/data/registry.json index 9dffd2e3..47329fea 100644 --- a/pkg/registry/data/registry.json +++ b/pkg/registry/data/registry.json @@ -1,6 +1,13 @@ { "version": "1.0.0", "last_updated": "2025-03-25 16:58:54", + "seccomp_defaults": { + "enabled": false, + "denied_syscalls": ["ptrace", "reboot", "kexec_load"], + "allowed_syscalls": ["read", "write", "exit", "open", "close"], + "default_action": "errno", + "architectures": ["x86_64", "arm64"] + }, "servers": { "fetch": { "image": "mcp/fetch:latest", @@ -22,6 +29,11 @@ 443 ] } + }, + "seccomp": { + "enabled": false, + "denied_syscalls": ["ptrace", "reboot", "kexec_load"], + "allowed_syscalls": ["read", "write", "exit", "open", "close"] } }, "tools": [