docs(runtime): document hardening contract and runtime override (#152)

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-25 21:06:10 +02:00
parent 8e9ad47077
commit f6b76b2d5e
3 changed files with 24 additions and 0 deletions

View File

@@ -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 |

View File

@@ -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:

View File

@@ -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