Harden multi-tenant runtime: sandbox untrusted user JVMs (Docker + K8s) #152
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Threat model
Cameleer is marketed as an Apache Camel observability platform, but a Camel app is just a JVM — tenants can run arbitrary Java:
Runtime.exec,Unsafe, JNI, reflection, dynamic class loading. Camel itself ships components that turn a single header into shell:camel-exec— directRuntime.exec(CVE-2025-27636 / CVE-2025-29891, Mar 2025)camel-bean— header-driven reflective dispatchcamel-groovy,camel-joor,camel-mvel,camel-simple,camel-velocity,camel-mustache— Turing-complete or templating engines (CVE-2020-11994 SSTI→RCE)Java 17 has no
SecurityManager. The JVM is not a security boundary. All meaningful isolation must live at the OS / container / network layers.When we run as
cameleer-saas, every tenant container we launch is hostile by default. We need to keep tenants from attacking:Current gaps (from
DockerRuntimeOrchestrator,DeploymentExecutor)runconlyCAP_NET_RAW,CAP_SYS_PTRACE, etc. all grantedRuntimeDefaultforced)--pids-limitcameleer-traefikbridgeWhat's already OK:
--privilegedcameleer-tenant-{slug})cameleer-env-{tenantId}-{envSlug})P0 — ship before any external SaaS tenant runs untrusted code
1. Sandboxed container runtime
runsc) on Docker hosts. Pass--runtime=runscfor tenant containers viawithRuntime("runsc")onHostConfig.RuntimeClassnamedgvisor, force it on tenant namespaces via Kyverno policy.2. Harden every tenant container
Add to
DockerRuntimeOrchestrator.HostConfigbuild (gated bycameleer.server.runtime.hardened=true, default true for SaaS):Enforce cgroup v2 on hosts (
systemd.unified_cgroup_hierarchy=1).3. Operator-controlled JRE base image
Tenants upload only the JAR. Our CI builds the final image: pinned JRE 21, fixed entrypoint, JVM flags hard-coded:
Strip
-javaagent/-agentlibfrom any user-supplied manifest before launch. Our agent javaagent must be the only one.4. Default-deny egress per tenant
Today every tenant container can reach the internet and our control plane. Replace the shared
cameleer-traefikbridge model:tenant-bridge → RFC1918to anywhere except our egress proxy. Reverse the Traefik model: Traefik joins each tenant's network rather than exposing all tenants on a shared bridge.CiliumClusterwideNetworkPolicy— default-deny ingress + egress, allow only kube-dns + per-tenant L7 egress proxy. Block169.254.169.254(cloud IMDS), control-plane CIDR, K8s API service CIDR.5. Kyverno admission policies (when on K8s)
privileged,hostPath,hostNetwork,hostPID,hostIPC, docker.sock mounts, dangerous capabilities.runtimeClassName: gvisoron tenant namespaces.automountServiceAccountToken: falseon every tenant pod.restrictedenforced via namespace labelpod-security.kubernetes.io/enforce: restricted.6. Per-tenant K8s namespace
ResourceQuota+LimitRangeper tenant — one tenant cannot exhaust cluster.ServiceAccountwith no RBAC andautomountServiceAccountToken: false.7. runc / containerd patch monitoring
Subscribe to runc-security mailing list. Auto-deploy patches.
P1 — first quarter after launch
8. Falco + custom Camel rules
Stable Falco rule set plus our additions:
sh|bash|curl|wget|nc|nmap|python|perl→ catchescamel-execabuse instantly/proc/cpuinfofrom tenant container → mining recon9. CoreDNS sinkhole
NXDOMAIN for mining-pool wildcards (
*.minexmr.com,*.nanopool.org, ethermine, f2pool, supportxmr) and a published C2 feed.10. L7 egress proxy per tenant
Squid / Envoy with domain allowlist the tenant declares at deploy time. Default: deny all egress except curated platform allowlist (Maven Central, etc.). Gives us billable byte-rate per tenant.
11. Camel component allowlist at deploy admission
Reject JARs whose
META-INF/services/org/apache/camel/component/includesexec,groovy,joor,mvelunless tenant has explicitly opted in. ASM-scan bytecode forRuntime.exec,ProcessBuilder,Unsafe, JNI — fail-closed if found and tenant tier is "untrusted."12. Image scan + signing
Trivy in CI. Reject HIGH/CRITICAL CVEs at registry push gate. Cosign-sign every image. Kyverno
verifyImagesrejects unsigned at admission.13. SBOM scan tenant JARs
Typosquatting detection (Dec 2025
org.fasterxml.jackson→com.fasterxml.jacksonswap dropped a Cobalt Strike beacon). Dependency-Track or Trivy SBOM mode. Levenshtein-close GAV coords from non-allowlisted groups → alert.14. Camel CVE auto-patch policy
Refuse to deploy a JAR whose Camel version < our CVE-clean floor. Move floor monthly.
15. Per-tenant egress quota + flow logs
Cilium bandwidth manager. Hubble flow logs → ClickHouse for per-tenant byte-rate alerting (we already have ClickHouse — natural fit).
P2 — at scale or for regulated workloads
Smallest first PR (deployable today, no K8s required)
DockerRuntimeOrchestratorHostConfigwith:read_only,cap_drop ALL,no-new-privileges,seccomp=default,apparmor=docker-default,pids_limit,userns_mode,tmpfs /tmp. Behind feature flagcameleer.server.runtime.hardenedso we can ramp per-tenant.cameleer.server.runtime.dockerRuntimeconfig; passwithRuntime("runsc"). One-line opt-in for migration.cameleer-traefikbridge model: Traefik joins per-tenant networks rather than tenants joining a shared bridge. Kills cross-tenant TCP today.containerConfig.allowedEgress: List<String>to app config; default[]. Wire to host iptables rules (interim) until Cilium / K8s lands.These four are reversible, behind flags, no K8s required, and close the runc-host-takeover blast radius now.
Sub-issue tracking
This is an epic. File children for: gVisor rollout, container hardening flags, base-image control, Camel component allowlist, Kyverno policy bundle, Cilium NetworkPolicy bundle, Falco rule set, CoreDNS sinkhole, L7 egress proxy, Trivy/Cosign pipeline, JAR SBOM scanner, Tetragon enforcement, red-team exercise.
Key references