Post-1.0: Localhost seccomp on K8s, warm sandbox pool, server HPA

- K8sDriver.with_localhost_seccomp(profile): sandbox pods run under the
  STRICT allowlist instead of the runtime default. Proven live on kind:
  the harness installs the profile onto the node, a pod runs ordinary
  work as uid 10001, and unshare is kernel-denied inside the pod — the
  same probe the Docker suite uses, now passing on both targets
- Helm: sandbox.seccomp=localhost renders a DaemonSet that installs the
  chart-shipped profile into /var/lib/kubelet/seccomp on every node
  (ConfigMap + hostPath); ci/check-helm.sh enforces the chart copy stays
  byte-identical to images/seccomp/agent-profile.json and asserts the
  hardened render (DaemonSet + profile + HPA)
- server HPA (autoscaling/v2, CPU target) behind
  server.autoscaling.enabled
- SandboxManager.warm(n): a background warmer keeps n pre-provisioned
  sandboxes ready so an agent's first exec skips container startup;
  unhealthy pool entries are discarded, reuse never drains the pool,
  shutdown destroys assigned AND pooled. [sandbox] warm_pool config
  (default 0). Real-Docker test: prefill -> assign -> refill -> reuse ->
  clean shutdown

160 Rust tests + 4 live kind tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-10 11:15:49 -05:00
co-authored by Claude Fable 5
parent a26939d7bc
commit 05e9612688
11 changed files with 1055 additions and 19 deletions
+23
View File
@@ -35,4 +35,27 @@ require 'readOnlyRootFilesystem: true'
# Config wired through the ConfigMap. # Config wired through the ConfigMap.
require 'socket_path = "/run/teamclaw/broker.sock"' require 'socket_path = "/run/teamclaw/broker.sock"'
# The chart-shipped seccomp profile must BE the Docker driver's profile.
if ! diff -q "$ROOT/images/seccomp/agent-profile.json" \
"$CHART/files/agent-profile.json" >/dev/null; then
echo "FAIL: chart seccomp profile diverged from images/seccomp"
exit 1
fi
HARDENED=$(helm template teamclaw "$CHART" \
--set auth.issuerUrl=https://idp.example.com \
--set oauth.redirectBase=https://teamclaw.example.com \
--set sandbox.seccomp=localhost \
--set server.autoscaling.enabled=true)
for needle in \
'kind: DaemonSet' \
'teamclaw-agent-profile.json' \
'kind: HorizontalPodAutoscaler' \
'averageUtilization: 70'; do
if ! grep -qF -- "$needle" <<<"$HARDENED"; then
echo "FAIL: hardened render is missing: $needle"
exit 1
fi
done
echo "helm chart OK" echo "helm chart OK"
+10 -4
View File
@@ -90,11 +90,17 @@ async fn run() -> Result<(), String> {
Ok(driver) => { Ok(driver) => {
let driver: std::sync::Arc<dyn tc_sandbox::SandboxDriver> = let driver: std::sync::Arc<dyn tc_sandbox::SandboxDriver> =
std::sync::Arc::new(driver); std::sync::Arc::new(driver);
let agents = std::sync::Arc::new(tc_runtime::SandboxManager::new(
driver.clone(),
&config.sandbox.image,
));
let agents = if config.sandbox.warm_pool > 0 {
agents.warm(config.sandbox.warm_pool)
} else {
agents
};
( (
Some(std::sync::Arc::new(tc_runtime::SandboxManager::new( Some(agents),
driver.clone(),
&config.sandbox.image,
))),
Some(std::sync::Arc::new( Some(std::sync::Arc::new(
tc_runtime::SandboxManager::new(driver, &config.sandbox.browser_image) tc_runtime::SandboxManager::new(driver, &config.sandbox.browser_image)
.with_egress(), .with_egress(),
+3
View File
@@ -144,6 +144,8 @@ pub struct SandboxConfig {
pub browser_image: String, pub browser_image: String,
/// Disable to run without environment tools (shell.exec errors). /// Disable to run without environment tools (shell.exec errors).
pub enabled: bool, pub enabled: bool,
/// Pre-provisioned sandboxes kept ready (0 = provision on demand).
pub warm_pool: usize,
} }
impl Default for SandboxConfig { impl Default for SandboxConfig {
@@ -152,6 +154,7 @@ impl Default for SandboxConfig {
image: "teamclaw/agent-base:dev".into(), image: "teamclaw/agent-base:dev".into(),
browser_image: "teamclaw/agent-browser:dev".into(), browser_image: "teamclaw/agent-browser:dev".into(),
enabled: true, enabled: true,
warm_pool: 0,
} }
} }
} }
+76 -14
View File
@@ -22,6 +22,10 @@ pub struct SandboxManager {
image: String, image: String,
egress: bool, egress: bool,
handles: Mutex<HashMap<AgentId, SandboxHandle>>, handles: Mutex<HashMap<AgentId, SandboxHandle>>,
/// Pre-provisioned, unassigned sandboxes (the warm pool).
pool: Mutex<Vec<SandboxHandle>>,
/// Warm-pool target; the background warmer keeps `pool` at this size.
warm_target: std::sync::atomic::AtomicUsize,
} }
impl SandboxManager { impl SandboxManager {
@@ -31,9 +35,59 @@ impl SandboxManager {
image: image.to_owned(), image: image.to_owned(),
egress: false, egress: false,
handles: Mutex::new(HashMap::new()), handles: Mutex::new(HashMap::new()),
pool: Mutex::new(Vec::new()),
warm_target: std::sync::atomic::AtomicUsize::new(0),
} }
} }
/// Starts the background warmer: keeps `target` sandboxes
/// pre-provisioned so an agent's first exec skips container startup.
pub fn warm(self: Arc<Self>, target: usize) -> Arc<Self> {
self.warm_target
.store(target, std::sync::atomic::Ordering::Relaxed);
let manager = self.clone();
tokio::spawn(async move {
loop {
let target = manager
.warm_target
.load(std::sync::atomic::Ordering::Relaxed);
if target == 0 {
break;
}
let deficit = target.saturating_sub(manager.pool.lock().await.len());
for _ in 0..deficit {
match manager.provision_one().await {
Ok(handle) => manager.pool.lock().await.push(handle),
Err(_) => break,
}
}
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
}
});
self
}
/// Unassigned warm sandboxes currently ready.
pub async fn pool_size(&self) -> usize {
self.pool.lock().await.len()
}
async fn provision_one(&self) -> Result<SandboxHandle, String> {
let short = uuid::Uuid::now_v7().simple().to_string();
let spec = SandboxSpec {
name: format!("tc-agent-{}", &short[short.len() - 12..]),
image: self.image.clone(),
memory_bytes: 512 * 1024 * 1024,
nano_cpus: 1_000_000_000,
pids_limit: 256,
egress: self.egress,
};
self.driver
.provision(&spec)
.await
.map_err(|e| format!("sandbox provision failed: {e}"))
}
/// The browser-container variant: egress on, no credentials inside, /// The browser-container variant: egress on, no credentials inside,
/// all output tainted `web` by the calling tool. /// all output tainted `web` by the calling tool.
pub fn with_egress(mut self) -> SandboxManager { pub fn with_egress(mut self) -> SandboxManager {
@@ -51,20 +105,20 @@ impl SandboxManager {
None => false, None => false,
}; };
if !alive { if !alive {
let short = uuid::Uuid::now_v7().simple().to_string(); // A warm sandbox if one is ready (and still healthy);
let spec = SandboxSpec { // otherwise provision inline.
name: format!("tc-agent-{}", &short[short.len() - 12..]), let mut assigned = None;
image: self.image.clone(), while let Some(candidate) = self.pool.lock().await.pop() {
memory_bytes: 512 * 1024 * 1024, if self.driver.health(&candidate).await.unwrap_or(false) {
nano_cpus: 1_000_000_000, assigned = Some(candidate);
pids_limit: 256, break;
egress: self.egress, }
let _ = self.driver.destroy(&candidate).await;
}
let handle = match assigned {
Some(handle) => handle,
None => self.provision_one().await?,
}; };
let handle = self
.driver
.provision(&spec)
.await
.map_err(|e| format!("sandbox provision failed: {e}"))?;
handles.insert(agent_id, handle); handles.insert(agent_id, handle);
} }
let handle = handles.get(&agent_id).expect("just ensured"); let handle = handles.get(&agent_id).expect("just ensured");
@@ -74,11 +128,19 @@ impl SandboxManager {
.map_err(|e| format!("sandbox exec failed: {e}")) .map_err(|e| format!("sandbox exec failed: {e}"))
} }
/// Destroys every sandbox this manager provisioned. /// Destroys every sandbox this manager provisioned — assigned and
/// pooled — and stops the warmer.
pub async fn shutdown(&self) { pub async fn shutdown(&self) {
self.warm_target
.store(0, std::sync::atomic::Ordering::Relaxed);
let mut handles = self.handles.lock().await; let mut handles = self.handles.lock().await;
for (_, handle) in handles.drain() { for (_, handle) in handles.drain() {
let _ = self.driver.destroy(&handle).await; let _ = self.driver.destroy(&handle).await;
} }
drop(handles);
let mut pool = self.pool.lock().await;
for handle in pool.drain(..) {
let _ = self.driver.destroy(&handle).await;
}
} }
} }
+80
View File
@@ -0,0 +1,80 @@
//! Warm sandbox pool: pre-provisioned containers absorb the first-exec
//! latency. Real Docker — the pool fills in the background, an exec
//! takes a sandbox from it, and the warmer restores the target.
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use tc_domain::AgentId;
use tc_runtime::SandboxManager;
use tc_sandbox::DockerDriver;
const IMAGE: &str = "teamclaw/agent-base:dev";
fn ensure_image() {
let exists = Command::new("docker")
.args(["image", "inspect", IMAGE])
.output()
.expect("docker available")
.status
.success();
if !exists {
let root = env!("CARGO_MANIFEST_DIR");
let status = Command::new("docker")
.args([
"build",
"-t",
IMAGE,
"-f",
&format!("{root}/../../images/agent-base/Dockerfile"),
&format!("{root}/../../images/agent-base"),
])
.status()
.expect("docker build runs");
assert!(status.success());
}
}
async fn pool_reaches(manager: &SandboxManager, target: usize) {
for _ in 0..120 {
if manager.pool_size().await == target {
return;
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
panic!(
"pool never reached {target} (now {})",
manager.pool_size().await
);
}
#[tokio::test]
async fn the_pool_prefills_assigns_and_refills() {
ensure_image();
let driver: Arc<dyn tc_sandbox::SandboxDriver> =
Arc::new(DockerDriver::connect().expect("docker reachable"));
let manager = Arc::new(SandboxManager::new(driver, IMAGE)).warm(2);
// The warmer fills the pool without any exec happening.
pool_reaches(&manager, 2).await;
// An exec is served from the pool — and works.
let agent = AgentId::new();
let result = manager.exec(agent, "id -u").await.unwrap();
assert_eq!(result.stdout.trim(), "10001");
// The warmer restores the target while the agent keeps its sandbox.
pool_reaches(&manager, 2).await;
let again = manager.exec(agent, "echo still-mine").await.unwrap();
assert_eq!(again.stdout.trim(), "still-mine");
assert_eq!(
manager.pool_size().await,
2,
"reuse must not drain the pool"
);
// Shutdown destroys assigned AND pooled sandboxes.
manager.shutdown().await;
assert_eq!(manager.pool_size().await, 0);
}
+17 -1
View File
@@ -28,6 +28,10 @@ fn engine_err(e: impl std::fmt::Display) -> SandboxError {
pub struct K8sDriver { pub struct K8sDriver {
client: kube::Client, client: kube::Client,
namespace: String, namespace: String,
/// Kubelet-relative path of the strict allowlist profile when the
/// nodes carry it (installed by the Helm DaemonSet); RuntimeDefault
/// otherwise.
localhost_seccomp: Option<String>,
} }
impl K8sDriver { impl K8sDriver {
@@ -41,6 +45,7 @@ impl K8sDriver {
let driver = K8sDriver { let driver = K8sDriver {
client, client,
namespace: namespace.to_owned(), namespace: namespace.to_owned(),
localhost_seccomp: None,
}; };
driver.ensure_namespace().await?; driver.ensure_namespace().await?;
Ok(driver) Ok(driver)
@@ -100,7 +105,18 @@ impl K8sDriver {
Ok(()) Ok(())
} }
/// Uses the node-installed strict allowlist profile instead of the
/// runtime default (see deploy/helm templates/seccomp-installer).
pub fn with_localhost_seccomp(mut self, profile: &str) -> K8sDriver {
self.localhost_seccomp = Some(profile.to_owned());
self
}
fn pod_spec(&self, spec: &SandboxSpec) -> Pod { fn pod_spec(&self, spec: &SandboxSpec) -> Pod {
let seccomp = match &self.localhost_seccomp {
Some(profile) => json!({ "type": "Localhost", "localhostProfile": profile }),
None => json!({ "type": "RuntimeDefault" }),
};
serde_json::from_value(json!({ serde_json::from_value(json!({
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
@@ -116,7 +132,7 @@ impl K8sDriver {
"runAsNonRoot": true, "runAsNonRoot": true,
"runAsUser": 10001, "runAsUser": 10001,
"runAsGroup": 10001, "runAsGroup": 10001,
"seccompProfile": { "type": "RuntimeDefault" } "seccompProfile": seccomp
}, },
"containers": [{ "containers": [{
"name": "sandbox", "name": "sandbox",
+57
View File
@@ -145,3 +145,60 @@ async fn destroy_removes_the_pod_and_health_reflects_it() {
} }
panic!("pod never disappeared"); panic!("pod never disappeared");
} }
/// With the strict allowlist installed on the node (the Helm DaemonSet's
/// job; the harness drops it into the kind node), pods run under
/// `Localhost` seccomp and the kernel refuses what the profile removed.
#[tokio::test]
async fn localhost_seccomp_profile_denies_unshare_inside_pods() {
ensure_image_in_kind();
let status = Command::new("docker")
.args([
"exec",
"teamclaw-test-control-plane",
"mkdir",
"-p",
"/var/lib/kubelet/seccomp",
])
.status()
.expect("kind node reachable");
assert!(status.success());
let root = env!("CARGO_MANIFEST_DIR");
let status = Command::new("docker")
.args([
"cp",
&format!("{root}/../../images/seccomp/agent-profile.json"),
"teamclaw-test-control-plane:/var/lib/kubelet/seccomp/teamclaw-agent-profile.json",
])
.status()
.expect("docker cp");
assert!(status.success());
let driver = K8sDriver::connect(NAMESPACE)
.await
.expect("cluster reachable")
.with_localhost_seccomp("teamclaw-agent-profile.json");
let spec = SandboxSpec {
name: format!("tc-k8s-seccomp-{}", std::process::id()),
image: IMAGE.into(),
memory_bytes: 256 * 1024 * 1024,
nano_cpus: 1_000_000_000,
pids_limit: 128,
egress: false,
};
let handle = driver.provision(&spec).await.expect("pod provisions");
// Ordinary work runs...
let ok = driver.exec(&handle, &["id", "-u"]).await.unwrap();
assert_eq!(ok.stdout.trim(), "10001");
// ...but the syscalls stripped from the allowlist are gone — the
// same kernel probe the Docker suite uses.
let unshare = driver
.exec(&handle, &["unshare", "--user", "true"])
.await
.unwrap();
assert_ne!(unshare.exit_code, 0, "unshare must be denied: {unshare:?}");
driver.destroy(&handle).await.unwrap();
}
@@ -0,0 +1,705 @@
{
"_comment": "TeamClaw agent-sandbox seccomp profile: Docker's default ALLOWLIST (vendored from moby v27.5.1) minus syscalls an agent workload never needs \u2014 namespace/mount/trace/key/module/perf surface removed even where capabilities would otherwise permit them. defaultAction ERRNO.",
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": [
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
]
},
{
"architecture": "SCMP_ARCH_AARCH64",
"subArchitectures": [
"SCMP_ARCH_ARM"
]
}
],
"syscalls": [
{
"names": [
"accept",
"accept4",
"access",
"adjtimex",
"alarm",
"bind",
"brk",
"cachestat",
"capget",
"capset",
"chdir",
"chmod",
"chown",
"chown32",
"clock_adjtime",
"clock_adjtime64",
"clock_getres",
"clock_getres_time64",
"clock_gettime",
"clock_gettime64",
"clock_nanosleep",
"clock_nanosleep_time64",
"close",
"close_range",
"connect",
"copy_file_range",
"creat",
"dup",
"dup2",
"dup3",
"epoll_create",
"epoll_create1",
"epoll_ctl",
"epoll_ctl_old",
"epoll_pwait",
"epoll_pwait2",
"epoll_wait",
"epoll_wait_old",
"eventfd",
"eventfd2",
"execve",
"execveat",
"exit",
"exit_group",
"faccessat",
"faccessat2",
"fadvise64",
"fadvise64_64",
"fallocate",
"fanotify_mark",
"fchdir",
"fchmod",
"fchmodat",
"fchmodat2",
"fchown",
"fchown32",
"fchownat",
"fcntl",
"fcntl64",
"fdatasync",
"fgetxattr",
"flistxattr",
"flock",
"fork",
"fremovexattr",
"fsetxattr",
"fstat",
"fstat64",
"fstatat64",
"fstatfs",
"fstatfs64",
"fsync",
"ftruncate",
"ftruncate64",
"futex",
"futex_requeue",
"futex_time64",
"futex_wait",
"futex_waitv",
"futex_wake",
"futimesat",
"getcpu",
"getcwd",
"getdents",
"getdents64",
"getegid",
"getegid32",
"geteuid",
"geteuid32",
"getgid",
"getgid32",
"getgroups",
"getgroups32",
"getitimer",
"getpeername",
"getpgid",
"getpgrp",
"getpid",
"getppid",
"getpriority",
"getrandom",
"getresgid",
"getresgid32",
"getresuid",
"getresuid32",
"getrlimit",
"get_robust_list",
"getrusage",
"getsid",
"getsockname",
"getsockopt",
"get_thread_area",
"gettid",
"gettimeofday",
"getuid",
"getuid32",
"getxattr",
"inotify_add_watch",
"inotify_init",
"inotify_init1",
"inotify_rm_watch",
"io_cancel",
"ioctl",
"io_destroy",
"io_getevents",
"io_pgetevents",
"io_pgetevents_time64",
"ioprio_get",
"ioprio_set",
"io_setup",
"io_submit",
"ipc",
"kill",
"landlock_add_rule",
"landlock_create_ruleset",
"landlock_restrict_self",
"lchown",
"lchown32",
"lgetxattr",
"link",
"linkat",
"listen",
"listxattr",
"llistxattr",
"_llseek",
"lremovexattr",
"lseek",
"lsetxattr",
"lstat",
"lstat64",
"madvise",
"map_shadow_stack",
"membarrier",
"memfd_create",
"memfd_secret",
"mincore",
"mkdir",
"mkdirat",
"mknod",
"mknodat",
"mlock",
"mlock2",
"mlockall",
"mmap",
"mmap2",
"mprotect",
"mq_getsetattr",
"mq_notify",
"mq_open",
"mq_timedreceive",
"mq_timedreceive_time64",
"mq_timedsend",
"mq_timedsend_time64",
"mq_unlink",
"mremap",
"msgctl",
"msgget",
"msgrcv",
"msgsnd",
"msync",
"munlock",
"munlockall",
"munmap",
"name_to_handle_at",
"nanosleep",
"newfstatat",
"_newselect",
"open",
"openat",
"openat2",
"pause",
"pidfd_open",
"pidfd_send_signal",
"pipe",
"pipe2",
"pkey_alloc",
"pkey_free",
"pkey_mprotect",
"poll",
"ppoll",
"ppoll_time64",
"prctl",
"pread64",
"preadv",
"preadv2",
"prlimit64",
"process_mrelease",
"pselect6",
"pselect6_time64",
"pwrite64",
"pwritev",
"pwritev2",
"read",
"readahead",
"readlink",
"readlinkat",
"readv",
"recv",
"recvfrom",
"recvmmsg",
"recvmmsg_time64",
"recvmsg",
"remap_file_pages",
"removexattr",
"rename",
"renameat",
"renameat2",
"restart_syscall",
"rmdir",
"rseq",
"rt_sigaction",
"rt_sigpending",
"rt_sigprocmask",
"rt_sigqueueinfo",
"rt_sigreturn",
"rt_sigsuspend",
"rt_sigtimedwait",
"rt_sigtimedwait_time64",
"rt_tgsigqueueinfo",
"sched_getaffinity",
"sched_getattr",
"sched_getparam",
"sched_get_priority_max",
"sched_get_priority_min",
"sched_getscheduler",
"sched_rr_get_interval",
"sched_rr_get_interval_time64",
"sched_setaffinity",
"sched_setattr",
"sched_setparam",
"sched_setscheduler",
"sched_yield",
"seccomp",
"select",
"semctl",
"semget",
"semop",
"semtimedop",
"semtimedop_time64",
"send",
"sendfile",
"sendfile64",
"sendmmsg",
"sendmsg",
"sendto",
"setfsgid",
"setfsgid32",
"setfsuid",
"setfsuid32",
"setgid",
"setgid32",
"setgroups",
"setgroups32",
"setitimer",
"setpgid",
"setpriority",
"setregid",
"setregid32",
"setresgid",
"setresgid32",
"setresuid",
"setresuid32",
"setreuid",
"setreuid32",
"setrlimit",
"set_robust_list",
"setsid",
"setsockopt",
"set_thread_area",
"set_tid_address",
"setuid",
"setuid32",
"setxattr",
"shmat",
"shmctl",
"shmdt",
"shmget",
"shutdown",
"sigaltstack",
"signalfd",
"signalfd4",
"sigprocmask",
"sigreturn",
"socketcall",
"socketpair",
"splice",
"stat",
"stat64",
"statfs",
"statfs64",
"statx",
"symlink",
"symlinkat",
"sync",
"sync_file_range",
"syncfs",
"sysinfo",
"tee",
"tgkill",
"time",
"timer_create",
"timer_delete",
"timer_getoverrun",
"timer_gettime",
"timer_gettime64",
"timer_settime",
"timer_settime64",
"timerfd_create",
"timerfd_gettime",
"timerfd_gettime64",
"timerfd_settime",
"timerfd_settime64",
"times",
"tkill",
"truncate",
"truncate64",
"ugetrlimit",
"umask",
"uname",
"unlink",
"unlinkat",
"utime",
"utimensat",
"utimensat_time64",
"utimes",
"vfork",
"vmsplice",
"wait4",
"waitid",
"waitpid",
"write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
},
{
"names": [
"socket"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 40,
"op": "SCMP_CMP_NE"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 0,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 8,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 131072,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 131080,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 4294967295,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"sync_file_range2",
"swapcontext"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"ppc64le"
]
}
},
{
"names": [
"arm_fadvise64_64",
"arm_sync_file_range",
"sync_file_range2",
"breakpoint",
"cacheflush",
"set_tls"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"arm",
"arm64"
]
}
},
{
"names": [
"arch_prctl"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"amd64",
"x32"
]
}
},
{
"names": [
"modify_ldt"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"amd64",
"x32",
"x86"
]
}
},
{
"names": [
"s390_pci_mmio_read",
"s390_pci_mmio_write",
"s390_runtime_instr"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"s390",
"s390x"
]
}
},
{
"names": [
"riscv_flush_icache"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"riscv64"
]
}
},
{
"names": [
"clone",
"fanotify_init",
"fsconfig",
"fsmount",
"fsopen",
"fspick",
"lookup_dcookie",
"mount_setattr",
"open_tree",
"quotactl_fd",
"setdomainname",
"sethostname",
"syslog",
"umount"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_ADMIN"
]
}
},
{
"names": [
"clone"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 2114060288,
"op": "SCMP_CMP_MASKED_EQ"
}
],
"excludes": {
"caps": [
"CAP_SYS_ADMIN"
],
"arches": [
"s390",
"s390x"
]
}
},
{
"names": [
"clone"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 1,
"value": 2114060288,
"op": "SCMP_CMP_MASKED_EQ"
}
],
"comment": "s390 parameter ordering for clone is different",
"includes": {
"arches": [
"s390",
"s390x"
]
},
"excludes": {
"caps": [
"CAP_SYS_ADMIN"
]
}
},
{
"names": [
"clone3"
],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 38,
"excludes": {
"caps": [
"CAP_SYS_ADMIN"
]
}
},
{
"names": [
"chroot"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_CHROOT"
]
}
},
{
"names": [
"kcmp",
"pidfd_getfd",
"process_madvise"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_PTRACE"
]
}
},
{
"names": [
"iopl",
"ioperm"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_RAWIO"
]
}
},
{
"names": [
"settimeofday",
"stime",
"clock_settime",
"clock_settime64"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_TIME"
]
}
},
{
"names": [
"vhangup"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_TTY_CONFIG"
]
}
},
{
"names": [
"get_mempolicy",
"mbind",
"set_mempolicy",
"set_mempolicy_home_node"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_NICE"
]
}
},
{
"names": [
"syslog"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYSLOG"
]
}
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{{- if .Values.server.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: teamclaw-server
labels: {{- include "teamclaw.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: teamclaw-server
minReplicas: {{ .Values.server.autoscaling.min }}
maxReplicas: {{ .Values.server.autoscaling.max }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.server.autoscaling.targetCPU }}
{{- end }}
@@ -0,0 +1,53 @@
{{- if eq .Values.sandbox.seccomp "localhost" }}
# Installs the strict allowlist seccomp profile onto every node so
# sandbox pods can run with seccompProfile type Localhost. The profile is
# the SAME file the Docker driver embeds (ci/check-helm.sh enforces the
# copies stay identical).
apiVersion: v1
kind: ConfigMap
metadata:
name: teamclaw-seccomp-profile
labels: {{- include "teamclaw.labels" . | nindent 4 }}
data:
teamclaw-agent-profile.json: |-
{{ .Files.Get "files/agent-profile.json" | indent 4 }}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: teamclaw-seccomp-installer
labels: {{- include "teamclaw.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
app.kubernetes.io/name: teamclaw-seccomp-installer
template:
metadata:
labels:
app.kubernetes.io/name: teamclaw-seccomp-installer
spec:
initContainers:
- name: install
image: busybox:1.36
command:
- sh
- -c
- cp /profile/teamclaw-agent-profile.json /host-seccomp/
volumeMounts:
- { name: profile, mountPath: /profile, readOnly: true }
- { name: host-seccomp, mountPath: /host-seccomp }
containers:
- name: hold
image: busybox:1.36
command: ["sleep", "infinity"]
resources:
requests: { cpu: 5m, memory: 8Mi }
limits: { cpu: 10m, memory: 16Mi }
volumes:
- name: profile
configMap: { name: teamclaw-seccomp-profile }
- name: host-seccomp
hostPath:
path: /var/lib/kubelet/seccomp
type: DirectoryOrCreate
{{- end }}
+10
View File
@@ -7,6 +7,11 @@ image:
server: server:
replicas: 1 replicas: 1
autoscaling:
enabled: false
min: 1
max: 5
targetCPU: 70
resources: resources:
requests: { cpu: 250m, memory: 256Mi } requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: "1", memory: 512Mi } limits: { cpu: "1", memory: 512Mi }
@@ -62,6 +67,11 @@ oauth:
clientSecretName: teamclaw-oauth clientSecretName: teamclaw-oauth
redirectBase: "" redirectBase: ""
sandbox:
# runtimeDefault | localhost (localhost installs the strict allowlist
# profile onto every node via a DaemonSet and runs sandbox pods under it)
seccomp: runtimeDefault
ingress: ingress:
enabled: true enabled: true
className: nginx className: nginx