Skip to content

[PoC] SECCOMP profiles #198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
13 changes: 13 additions & 0 deletions pkg/container/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
85 changes: 85 additions & 0 deletions pkg/container/docker/seccomp.go
Original file line number Diff line number Diff line change
@@ -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))

Check failure on line 52 in pkg/container/docker/seccomp.go

View workflow job for this annotation

GitHub Actions / Linting / Lint

The line is 131 characters long, which exceeds the maximum of 130 characters. (lll)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have logger.Log.Warnf in main now, should enable you to avoid using fmt.Sprintf

}
}

// 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
}
47 changes: 47 additions & 0 deletions pkg/permissions/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"},
},
}
}

Expand Down Expand Up @@ -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"},
},
}
}

Expand All @@ -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"},
},
}
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/registry/data/registry.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -22,6 +29,11 @@
443
]
}
},
"seccomp": {
"enabled": false,
"denied_syscalls": ["ptrace", "reboot", "kexec_load"],
"allowed_syscalls": ["read", "write", "exit", "open", "close"]
}
},
"tools": [
Expand Down
Loading