75 lines
3.8 KiB
Bash
75 lines
3.8 KiB
Bash
#!/bin/sh
|
|
# Render-service entrypoint.
|
|
#
|
|
# The service renders UNTRUSTED, uploaded HTML in Chromium. The export ZIP is
|
|
# fully self-contained (all assets + vendored GSAP are bundled at build time and
|
|
# served to Chromium over loopback), so the render needs ZERO outbound network.
|
|
# We enforce that here: block all egress except loopback and replies on already
|
|
# established (app-initiated) connections. This is the boundary that stops the
|
|
# untrusted page from initiating connections back to the app (e.g. the compose
|
|
# `openmaic` service) or anywhere else, even though both share a Docker network.
|
|
#
|
|
# Requires the container to start as root with CAP_NET_ADMIN (compose:
|
|
# `cap_add: [NET_ADMIN]`). We install the rules as root, then drop to the
|
|
# unprivileged `render` user for the Node process.
|
|
#
|
|
# FAIL CLOSED: when the lockdown is requested (RENDER_EGRESS_LOCKDOWN=true, the
|
|
# default) but cannot be installed, we EXIT non-zero rather than start an
|
|
# unisolated service that /health would still report as healthy — the app would
|
|
# otherwise advertise MP4 rendering while Chromium could reach the app. An
|
|
# operator who knowingly accepts an unisolated standalone setup must opt out
|
|
# explicitly with RENDER_EGRESS_LOCKDOWN=false.
|
|
set -eu
|
|
|
|
# `setpriv` changes uid/gid but deliberately preserves the root process's
|
|
# environment. Reset HOME before dropping privileges so producer font caches do
|
|
# not resolve to /root/.cache and fail with EACCES under the `render` user.
|
|
export HOME="${RENDER_HOME:-/app}"
|
|
export XDG_CACHE_HOME="$HOME/.cache"
|
|
mkdir -p "$XDG_CACHE_HOME"
|
|
if [ "$(id -u)" = "0" ]; then
|
|
chown -R render:render "$XDG_CACHE_HOME"
|
|
fi
|
|
|
|
lockdown() {
|
|
# ESTABLISHED,RELATED lets the Hono API respond to the app's inbound requests;
|
|
# loopback lets the producer's file server + Chromium talk locally. Everything
|
|
# else outbound (new connections, DNS to resolve `openmaic`, etc.) is dropped.
|
|
# IPv4 rules must all succeed; IPv6 is best-effort (the stack/table may be
|
|
# absent), but when present we still default-drop so v6 can't be an escape.
|
|
iptables -A OUTPUT -o lo -j ACCEPT || return 1
|
|
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT || return 1
|
|
iptables -P OUTPUT DROP || return 1
|
|
if command -v ip6tables >/dev/null 2>&1; then
|
|
ip6tables -A OUTPUT -o lo -j ACCEPT 2>/dev/null || true
|
|
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
|
|
ip6tables -P OUTPUT DROP 2>/dev/null || true
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
if [ "${RENDER_EGRESS_LOCKDOWN:-true}" = "true" ]; then
|
|
if [ "$(id -u)" != "0" ]; then
|
|
echo "[render-service] FATAL: egress lockdown requested but not running as root (need root + CAP_NET_ADMIN). Set RENDER_EGRESS_LOCKDOWN=false to run unisolated." >&2
|
|
exit 1
|
|
fi
|
|
if ! command -v iptables >/dev/null 2>&1; then
|
|
echo "[render-service] FATAL: egress lockdown requested but iptables is not installed. Set RENDER_EGRESS_LOCKDOWN=false to run unisolated." >&2
|
|
exit 1
|
|
fi
|
|
if ! lockdown; then
|
|
echo "[render-service] FATAL: egress lockdown requested but iptables rules failed to apply (missing CAP_NET_ADMIN or backend mismatch). Refusing to start unisolated. Set RENDER_EGRESS_LOCKDOWN=false to override." >&2
|
|
exit 1
|
|
fi
|
|
echo "[render-service] egress lockdown active (outbound blocked except loopback)"
|
|
else
|
|
echo "[render-service] WARNING: egress lockdown DISABLED (RENDER_EGRESS_LOCKDOWN=false). Chromium can reach the Docker network — only safe on a network you have isolated yourself." >&2
|
|
fi
|
|
|
|
# Drop privileges to the unprivileged render user for the Node process. When
|
|
# already running as that user (no lockdown / non-root start), exec directly.
|
|
if [ "$(id -u)" = "0" ]; then
|
|
exec setpriv --reuid=render --regid=render --init-groups node_modules/.bin/tsx src/main.ts
|
|
fi
|
|
exec node_modules/.bin/tsx src/main.ts
|