Skip to content
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

ARG VERSION=X.Y.Z-stable

FROM tryretool/agent-sandbox-service:${VERSION} AS agent-sandbox

FROM tryretool/code-executor-service:${VERSION} AS code-executor

FROM tryretool/js-executor-service:${VERSION} AS js-executor

FROM tryretool/backend:${VERSION}

CMD ./docker_scripts/start_api.sh
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,18 @@ Configure

1. Check the generated `.env` files to make sure the license key and randomized keys were set as expected during the installation.

2. Save off the `ENCRYPTION_KEY` value, since this is needed to encrypt/decrypt values saved into the Postgres database the Retool instance runs on.
2. Review the blob storage settings in `docker.env`. The generated defaults use bundled MinIO through `RR_BLOB_STORAGE_PROVIDER=s3` and `RR_DEFAULT_S3_*` variables. For production, replace these with your external object store credentials. For AWS S3, remove `RR_DEFAULT_S3_ENDPOINT` and `AWS_ENDPOINT_URL`; for S3-compatible providers such as MinIO, set both endpoint variables to the provider endpoint.

3. Replace `X.Y.Z-stable` in `Dockerfile` with the desired Retool version listed in our [Dockerhub repo](https://hub.docker.com/r/tryretool/backend/tags), we recommend the latest patch of the most recent [stable version](https://hub.docker.com/r/tryretool/backend/tags?name=stable).
3. Save off the `ENCRYPTION_KEY` value, since this is needed to encrypt/decrypt values saved into the Postgres database the Retool instance runs on.

4. To set up HTTPS, you'll need your domain pointing to your server's IP address. If that's in place, make sure `DOMAINS` is correct in `docker.env`, and then set `STAGE=production` in `compose.yaml` for the `https-portal` container to attempt to get and use a free `Let's Encrypt` cert for your domain on startup.
4. Replace `X.Y.Z-stable` in `Dockerfile` with the desired Retool version listed in our [Dockerhub repo](https://hub.docker.com/r/tryretool/backend/tags), we recommend the latest patch of the most recent [stable version](https://hub.docker.com/r/tryretool/backend/tags?name=stable).

5. To set up HTTPS, you'll need your domain pointing to your server's IP address. If that's in place, make sure `DOMAINS` is correct in `docker.env`, and then set `STAGE=production` in `compose.yaml` for the `https-portal` container to attempt to get and use a free `Let's Encrypt` cert for your domain on startup.

> [!WARNING]
> You must set `COOKIE_INSECURE=true` in `docker.env` to allow logging into Retool without HTTPS configured (not recommended)

5. By default, the deployment will include a Temporal container for Workflows. If you have an Enterprise license and would like to instead use Retool's managed Temporal cluster, comment out the `include` block in `compose.yaml` and the `WORKFLOW_TEMPORAL_...` environment variables in `docker.env`. Check out [our docs](https://docs.retool.com/self-hosted/concepts/temporal) for more information on Temporal deployment options.
6. By default, the deployment will include a Temporal container for Workflows. If you have an Enterprise license and would like to instead use Retool's managed Temporal cluster, comment out the `include` block in `compose.yaml` and the `WORKFLOW_TEMPORAL_...` environment variables in `docker.env`. Check out [our docs](https://docs.retool.com/self-hosted/concepts/temporal) for more information on Temporal deployment options.

<br>

Expand Down
55 changes: 55 additions & 0 deletions appArmor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,58 @@ sudo apt install apparmor-profiles
sudo apparmor_parser /etc/apparmor.d/usr.bin.nsjail
```

## Agent Sandbox: replacing `docker-default`

The agent-sandbox-controller spawns sandbox containers over the Docker socket. Inside each sandbox, two mount-using tools run:

- [pasta](https://passt.top/) sets up isolated networking and remounts `/` to make the root mount private.
- [runsc (gVisor)](https://gvisor.dev/) in `--rootless` mode mounts `proc`, `tmpfs`, `/dev`, and calls `pivot_root` to assemble its second-level sandbox — even when networking is disabled.

The Docker daemon auto-applies its built-in `docker-default` AppArmor profile to every container it starts. The bundled `gvisor-seccomp.json` profile already permits `mount` / `umount2` / `pivot_root` (those are explicitly allow-listed for gVisor + pasta), but AppArmor sits in front of seccomp and rejects the syscall first. The visible symptom is pasta exiting with `Failed to remount /: Permission denied`; runsc's mount calls would be the next failure if pasta were skipped.

`docker-default` is loaded into the kernel by the Docker daemon at startup; it usually isn't present as a file in `/etc/apparmor.d/`. We ship a replacement profile (`docker-default` in this directory) with the same name. Loading it with `apparmor_parser -r` replaces the in-kernel version, and new containers — including those spawned by the agent-sandbox-controller — pick up our variant. The rest of upstream `docker-default`'s hardening (the `/proc`, `/sys`, sysrq, ptrace denylists) is kept intact.

### What's different from upstream `docker-default`

Three deltas:

1. **Replaced `deny mount,` with `mount,`.** Upstream blocks all mount operations at the AppArmor layer. Agent-sandbox needs `mount` for pasta (remount `/` private) and runsc (mount `proc` / `tmpfs` / `/dev`). A bare `mount,` rule is a broad allow.
2. **Added `pivot_root,`.** `pivot_root` is a distinct AppArmor mediation class from `mount` and is also default-deny once any other mount-class rule exists in the profile. pasta and runsc both call `pivot_root` to swap rootfs into their sandboxes, so we need an explicit allow. Symptom without it: `apparmor="DENIED" operation="pivotroot" ...` for `comm="passt.avx2"` and `comm="exe"` (runsc) on `/tmp/` and `/proc/fs/`.
3. **Explicit `signal` rules.** Upstream historically didn't include any `signal` rules. AppArmor 4.x (Ubuntu 24.04+) treats a profile that has *any* rules but no `signal` rules as default-deny for cross-profile signals, which surfaces as `apparmor="DENIED" operation="signal" ... comm="runc"` audit lines during container teardown. We add explicit rules for peer=docker-default and receive-from-unconfined to match the implicit behavior on older kernels.

All `/proc`, `/sys`, sysrq, kcore, powercap, security and ptrace deny rules are kept verbatim from upstream so the rest of the hardening surface is unchanged.

### Why an explicit `mount,` rule is needed (and not just dropping the `deny`)

`#include <abstractions/base>` brings in narrow mount rules on Ubuntu 24.04+ (e.g. specific tmpfs / proc allowances). That puts the profile into "default-deny for unmatched mounts" mode — once any mount rule exists in the evaluated profile, every other mount must match an allow rule. Removing `deny mount,` alone leaves only the abstraction's narrow allows, so pasta's `mount / / -o rw,rslave` falls through to a default-deny and the audit log shows `info="failed mntpnt match"`. The broad `mount,` rule explicitly permits unmatched mount syscalls.

### Installation

Run the bundled script:

```
sudo ./scripts/setup-docker-apparmor.sh
```

It is idempotent and skips cleanly on hosts without AppArmor (e.g. macOS Docker Desktop). What it does:

1. Copies the bundled `docker-default` profile to `/etc/apparmor.d/docker-default`.
2. Loads it into the kernel with `apparmor_parser -r` so new containers pick it up.
3. Installs a systemd drop-in at `/etc/systemd/system/docker.service.d/retool-apparmor.conf` containing `ExecStartPre=-/usr/sbin/apparmor_parser -r /etc/apparmor.d/docker-default`. This makes the docker daemon re-apply our profile on every start, so the override survives `systemctl restart docker` and host reboots without manual intervention.

`./install.sh` invokes this script automatically; only run it directly if you're applying the AppArmor change to an existing install or to a host where `install.sh` was run on an older version of this repo.

You do **not** need to restart Docker after a first-time install — already-running containers keep their existing profile, and any new container will use the replaced version.

### Verifying it's in effect

After loading, trigger a fresh sandbox spawn and tail the kernel audit log:

```
sudo dmesg -wT | grep -iE 'apparmor|audit'
```

You should see no `apparmor="DENIED"` lines for `operation="mount"` or `operation="signal"`. If you do, capture the line — it points at the next rule that needs widening.

This step only applies on Linux hosts where AppArmor is active. macOS hosts running Docker Desktop don't enforce AppArmor (the LinuxKit VM kernel ships without it), so no override is needed there; `apparmor_parser` won't be available on those machines.

59 changes: 59 additions & 0 deletions appArmor/docker-default
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include <tunables/global>


profile docker-default flags=(attach_disconnected,mediate_deleted) {

#include <abstractions/base>


network,
capability,
file,
umount,
# NOTE: upstream docker-default has `deny mount,` here. It is replaced with an
# explicit broad `mount,` allow so that — inside containers the
# agent-sandbox-controller spawns — pasta can remount `/` for its filesystem
# isolation AND runsc (gVisor) can mount proc / tmpfs / /dev when assembling
# its second-level sandbox. The bundled gvisor-seccomp.json already permits
# mount / umount2 / pivot_root, but AppArmor sits in front of seccomp.
#
# An explicit `mount,` rule is required (rather than just dropping the deny):
# `#include <abstractions/base>` brings in some narrow mount rules on
# Ubuntu 24.04+, which puts the profile into "default-deny for unmatched
# mounts" mode. Without a broad allow, pasta's `mount / / -o rw,rslave`
# fails with `failed mntpnt match`.
#
# The other denylists below are kept verbatim from upstream so the rest of
# docker-default's hardening (proc, sys, sysrq, kcore, ptrace) is preserved.
mount,

# pivot_root is a distinct AppArmor mediation class from mount. pasta and
# runsc both pivot_root when assembling their respective sandboxes, so we
# need an explicit allow.
pivot_root,

deny @{PROC}/* w, # deny write for all files directly in /proc
deny @{PROC}/sys/[^k]*/** wklx, # deny write to /proc/sys except /proc/sys/k*
deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} wklx, # deny /proc/sys/kernel/* except *shm*
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/kcore rwklx,

deny /sys/[^f]*/** wklx,
deny /sys/f[^s]*/** wklx,
deny /sys/fs/[^c]*/** wklx,
deny /sys/fs/c[^g]*/** wklx,
deny /sys/fs/cg[^r]*/** wklx,
deny /sys/firmware/** rwklx,
deny /sys/devices/virtual/powercap/** rwklx,
deny /sys/kernel/security/** rwklx,

# Suppress ptrace denials when running `ps` / `docker ps` inside a container.
ptrace (trace,read,tracedby,readby) peer=docker-default,

# Signal mediation. AppArmor 4.x (Ubuntu 24.04+) defaults to deny when a
# profile has any rules but no signal rules; this allows signals between
# docker-default-confined processes (container peers) and from the
# unconfined Docker daemon (e.g. SIGKILL during teardown).
signal (send,receive) peer=docker-default,
signal (receive) peer=unconfined,
}
Loading