From f6b76b2d5e3b6173550b99f6b114c9797264e200 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:06:10 +0200 Subject: [PATCH] docs(runtime): document hardening contract and runtime override (#152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces the multi-tenant container hardening contract introduced in the prior commit so operators and integrators know what is enforced and why. - application.yml: declare `cameleer.server.runtime.dockerruntime` alongside the other runtime properties (empty = auto-detect runsc). - HOWTO.md: add the override row to the Runtime config table. - SERVER-CAPABILITIES.md: new "Multi-Tenant Runtime Sandboxing" section describing the cap_drop, no-new-privileges, AppArmor, read-only rootfs, pids_limit, /tmp tmpfs, and runsc auto-detect contract — plus the on-disk state caveat that motivates issue #153. Co-Authored-By: Claude Opus 4.7 (1M context) --- HOWTO.md | 1 + .../src/main/resources/application.yml | 5 +++++ docs/SERVER-CAPABILITIES.md | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/HOWTO.md b/HOWTO.md index b67eeb1e..932e97d6 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -494,6 +494,7 @@ Key settings in `cameleer-server-app/src/main/resources/application.yml`. All cu | `cameleer.server.runtime.enabled` | `true` | `CAMELEER_SERVER_RUNTIME_ENABLED` | Enable Docker orchestration | | `cameleer.server.runtime.baseimage` | `cameleer-runtime-base:latest` | `CAMELEER_SERVER_RUNTIME_BASEIMAGE` | Base Docker image for app containers | | `cameleer.server.runtime.dockernetwork` | `cameleer` | `CAMELEER_SERVER_RUNTIME_DOCKERNETWORK` | Primary Docker network | +| `cameleer.server.runtime.dockerruntime` | *(empty = auto)* | `CAMELEER_SERVER_RUNTIME_DOCKERRUNTIME` | Container runtime override. Empty auto-detects gVisor (`runsc`) when registered with the daemon and falls back to the daemon default. Set to e.g. `kata` to force a specific runtime, or `runc` to force the default even if `runsc` is installed. | | `cameleer.server.runtime.jarstoragepath` | `/data/jars` | `CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH` | JAR file storage directory | | `cameleer.server.runtime.jardockervolume` | *(empty)* | `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME` | Docker volume for JAR sharing | | `cameleer.server.runtime.routingmode` | `path` | `CAMELEER_SERVER_RUNTIME_ROUTINGMODE` | `path` or `subdomain` Traefik routing | diff --git a/cameleer-server-app/src/main/resources/application.yml b/cameleer-server-app/src/main/resources/application.yml index 3ded9adf..a7e29444 100644 --- a/cameleer-server-app/src/main/resources/application.yml +++ b/cameleer-server-app/src/main/resources/application.yml @@ -47,6 +47,11 @@ cameleer: jarstoragepath: ${CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH:/data/jars} baseimage: ${CAMELEER_SERVER_RUNTIME_BASEIMAGE:gitea.siegeln.net/cameleer/cameleer-runtime-base:latest} dockernetwork: ${CAMELEER_SERVER_RUNTIME_DOCKERNETWORK:cameleer} + # Container runtime override. Empty (default) auto-detects: uses runsc + # (gVisor) if the daemon has it registered, otherwise the daemon default + # (runc). Set to a registered runtime name (e.g. "kata", "runc") to + # force a specific runtime. See issue #152 for the threat model. + dockerruntime: ${CAMELEER_SERVER_RUNTIME_DOCKERRUNTIME:} agenthealthport: 9464 healthchecktimeout: 60 container: diff --git a/docs/SERVER-CAPABILITIES.md b/docs/SERVER-CAPABILITIES.md index d4f0e25d..529a5220 100644 --- a/docs/SERVER-CAPABILITIES.md +++ b/docs/SERVER-CAPABILITIES.md @@ -34,6 +34,24 @@ Each server instance serves exactly one tenant. Multiple tenants share infrastru --- +## Multi-Tenant Runtime Sandboxing + +When the server orchestrates tenant containers (SaaS / managed mode), every container is launched with an unconditional hardening contract — Java 17 has no `SecurityManager`, so isolation must live below the JVM. Camel ships components that turn a header into shell (`camel-exec`, `camel-bean`, `camel-groovy`, `camel-mvel`, `camel-velocity`), so tenant JARs are treated as hostile by default. + +| Layer | What is enforced | +|---|---| +| Capabilities | `cap_drop` every Linux capability the SDK enumerates (effectively ALL — outbound TCP needs none). | +| Privilege escalation | `no-new-privileges` — setuid binaries cannot escalate. | +| MAC profile | `apparmor=docker-default`. The Docker daemon's default seccomp profile is applied implicitly. | +| Filesystem | `read_only` rootfs. `/tmp` is a 256m tmpfs (`rw,nosuid` — `noexec` is intentionally **not** set so JNI native libs from Netty/Snappy/LZ4/Zstd can `dlopen`). | +| Resource caps | `pids_limit=512` per container; CPU and memory limits per tenant config. | +| Container runtime | Auto-detects gVisor (`runsc`) via `docker info` and uses it when registered with the daemon. Override with `CAMELEER_SERVER_RUNTIME_DOCKERRUNTIME` (e.g. `kata`, or `runc` to force the default). | +| Network | Per-tenant Docker bridge `cameleer-tenant-{slug}` + per-env discovery network `cameleer-env-{tenantId}-{envSlug}`. Tenants cannot reach each other's containers. | + +**Implication for tenants writing on-disk state**: with `read_only` rootfs, anything that needs durable disk (Kafka Streams RocksDB stores, Hibernate L2 cache, log files outside stdout) must be on a writeable volume. Per-app `containerConfig.writeableVolumes` support is tracked separately — see issue #153. + +--- + ## Agent Protocol ### Lifecycle