mirror of
https://github.com/open-webui/open-terminal.git
synced 2026-11-04 08:10:53 +00:00
feat: add slim and alpine Docker image variants
- Dockerfile.slim: multi-stage Debian slim (~200 MB) - Dockerfile.alpine: multi-stage Alpine (~100 MB) - entrypoint-slim.sh: hardened shared entrypoint (no sudo, no runtime installs) - CI: build matrix expanded to 3 variants × 2 platforms - README: image variants table with example commands
This commit is contained in:
1 parent
68b9380c94
commit
980292b134
5 files changed
+518
-8
No files matched your search
@@ -19,6 +19,16 @@ jobs:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
variant:
|
||||
- name: default
|
||||
file: Dockerfile
|
||||
suffix: ""
|
||||
- name: slim
|
||||
file: Dockerfile.slim
|
||||
suffix: "-slim"
|
||||
- name: alpine
|
||||
file: Dockerfile.alpine
|
||||
suffix: "-alpine"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -46,11 +56,12 @@ jobs:
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.variant.file }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,scope=${{ matrix.platform }},mode=max
|
||||
cache-from: type=gha,scope=${{ matrix.variant.name }}-${{ matrix.platform }}
|
||||
cache-to: type=gha,scope=${{ matrix.variant.name }}-${{ matrix.platform }},mode=max
|
||||
|
||||
- name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -62,7 +73,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||
name: digests-${{ matrix.variant.name }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -71,6 +82,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: build
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
- name: default
|
||||
suffix: ""
|
||||
- name: slim
|
||||
suffix: "-slim"
|
||||
- name: alpine
|
||||
suffix: "-alpine"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -101,7 +121,7 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
pattern: digests-${{ matrix.variant.name }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
@@ -117,10 +137,10 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=${{ steps.version.outputs.version }},enable=${{ steps.version.outputs.is_new_version }}
|
||||
type=raw,value=${{ steps.version.outputs.major_minor }},enable=${{ steps.version.outputs.is_new_version }}
|
||||
type=sha
|
||||
type=raw,value=latest${{ matrix.variant.suffix }},enable={{is_default_branch}}
|
||||
type=raw,value=${{ steps.version.outputs.version }}${{ matrix.variant.suffix }},enable=${{ steps.version.outputs.is_new_version }}
|
||||
type=raw,value=${{ steps.version.outputs.major_minor }}${{ matrix.variant.suffix }},enable=${{ steps.version.outputs.is_new_version }}
|
||||
type=sha,suffix=${{ matrix.variant.suffix }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# ============================================================================
|
||||
# Open Terminal — Alpine Image
|
||||
# ============================================================================
|
||||
#
|
||||
# The smallest possible Open Terminal image. Built on Alpine Linux with
|
||||
# musl libc, this image is ~100 MB — great for edge deployments, CI
|
||||
# runners, and environments where every megabyte counts.
|
||||
#
|
||||
# Build:
|
||||
# docker build -f Dockerfile.alpine -t open-terminal:alpine .
|
||||
#
|
||||
# Run:
|
||||
# docker run -d -p 8000:8000 -e OPEN_TERMINAL_API_KEY=secret open-terminal:alpine
|
||||
#
|
||||
# Customize:
|
||||
# - Want extra apk packages? Add them to the "Runtime packages" section below.
|
||||
# - Want extra pip packages? Add them to the "pip install" line in the builder.
|
||||
# - Need glibc or heavier tools? Use the slim (Debian) or full image instead.
|
||||
#
|
||||
# ⚠️ Alpine uses musl libc. Most pure-Python packages work fine, but
|
||||
# some C-extension wheels (numpy, pandas, etc.) may need to be compiled
|
||||
# from source, which is slower. If you hit issues, try the slim image.
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Stage 1: Builder
|
||||
# Compiles Python dependencies in an isolated prefix. Alpine needs
|
||||
# musl-dev and gcc for most C extensions.
|
||||
# --------------------------------------------------------------------------
|
||||
FROM python:3.12-alpine AS builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Build-time system dependencies.
|
||||
# ➡️ If a pip package needs extra C headers, add the -dev package here
|
||||
# (e.g. "postgresql-dev" for psycopg2, "libxml2-dev" for lxml).
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libffi-dev
|
||||
|
||||
COPY pyproject.toml uv.lock* ./
|
||||
COPY open_terminal/ open_terminal/
|
||||
|
||||
# Install the app + dependencies into /install so we can cherry-pick them.
|
||||
# ➡️ To add extra pip packages to the alpine image, append them here:
|
||||
# e.g. pip install --no-cache-dir --prefix=/install . httpx polars
|
||||
RUN pip install --no-cache-dir --prefix=/install .
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Stage 2: Runtime
|
||||
# Tiny Alpine runtime. No compilers, no build tools, no sudo.
|
||||
# --------------------------------------------------------------------------
|
||||
FROM python:3.12-alpine
|
||||
|
||||
# ── Runtime system packages ────────────────────────────────────────────────
|
||||
#
|
||||
# These are the only apk packages in the final image. Kept intentionally
|
||||
# tiny. If you need something extra, just add it below.
|
||||
#
|
||||
# Core: tini (PID 1), su-exec (drop privileges, Alpine's gosu)
|
||||
# Utilities: curl, git, jq, less, procps (ps/top)
|
||||
# Firewall: iptables, ipset, dnsmasq (egress whitelist)
|
||||
# Permissions: libcap (setcap for cap_setgid)
|
||||
# Shell: bash (the terminal API spawns bash sessions)
|
||||
#
|
||||
# ➡️ Want more tools? Add them here, one per line for easy diffs:
|
||||
# e.g. vim \
|
||||
# sqlite \
|
||||
#
|
||||
RUN apk add --no-cache \
|
||||
tini \
|
||||
su-exec \
|
||||
curl \
|
||||
git \
|
||||
jq \
|
||||
less \
|
||||
procps \
|
||||
bash \
|
||||
iptables \
|
||||
ipset \
|
||||
dnsmasq \
|
||||
libcap \
|
||||
libcap-utils \
|
||||
ca-certificates
|
||||
|
||||
# Copy the pre-built Python packages from the builder stage.
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Install the app itself (fast — all deps are already present) and grant
|
||||
# the Python binary cap_setgid so multi-user mode can call setgid().
|
||||
# ⚠️ setcap MUST run in the same layer as the binary to avoid overlay2 issues.
|
||||
RUN pip install --no-cache-dir --no-deps . \
|
||||
&& setcap cap_setgid+ep "$(readlink -f "$(which python3)")"
|
||||
|
||||
# ── Non-root user ─────────────────────────────────────────────────────────
|
||||
#
|
||||
# The app runs as "user" (UID 1000) with NO sudo access.
|
||||
# The entrypoint handles any root-level setup (iptables, chown) via su-exec.
|
||||
#
|
||||
RUN adduser -D -s /bin/bash -u 1000 user
|
||||
|
||||
COPY entrypoint-slim.sh /app/entrypoint-slim.sh
|
||||
RUN chmod +x /app/entrypoint-slim.sh
|
||||
|
||||
# ── NOTE: We do NOT set "USER user" here ──────────────────────────────────
|
||||
#
|
||||
# The container starts as root so the entrypoint can:
|
||||
# 1. Fix /home/user ownership on bind mounts
|
||||
# 2. Set up iptables egress rules (if configured)
|
||||
# 3. Drop privileges via su-exec → runs the app as "user"
|
||||
#
|
||||
# If you don't need the egress firewall, you can add "USER user" here
|
||||
# and simplify the entrypoint — but su-exec handles it cleanly either way.
|
||||
#
|
||||
ENV SHELL=/bin/bash
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# tini is PID 1 — reaps zombies and forwards signals cleanly.
|
||||
ENTRYPOINT ["/sbin/tini", "--", "/app/entrypoint-slim.sh"]
|
||||
CMD ["run"]
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# ============================================================================
|
||||
# Open Terminal — Slim Image (Debian)
|
||||
# ============================================================================
|
||||
#
|
||||
# A minimal, hardened image for running the Open Terminal API.
|
||||
# No Node.js, no Docker CLI, no data-science libraries, no sudo.
|
||||
# Just the terminal API, git, curl, and the egress firewall.
|
||||
#
|
||||
# Build:
|
||||
# docker build -f Dockerfile.slim -t open-terminal:slim .
|
||||
#
|
||||
# Run:
|
||||
# docker run -d -p 8000:8000 -e OPEN_TERMINAL_API_KEY=secret open-terminal:slim
|
||||
#
|
||||
# Customize:
|
||||
# - Want extra apt packages? Add them to the "Runtime packages" section below.
|
||||
# - Want extra pip packages? Add them to the "pip install" line in the builder.
|
||||
# - Need Node.js or heavier tools? Use the full image (Dockerfile) instead.
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Stage 1: Builder
|
||||
# Compiles Python dependencies in an isolated prefix so we can copy only
|
||||
# the final result into the slim runtime image — no compilers shipped.
|
||||
# --------------------------------------------------------------------------
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Build-time system dependencies.
|
||||
# If a pip package needs extra C libraries to compile, add the -dev
|
||||
# package here (e.g. "libpq-dev" for psycopg2).
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
gcc \
|
||||
libffi-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml uv.lock* ./
|
||||
COPY open_terminal/ open_terminal/
|
||||
|
||||
# Install the app + dependencies into /install so we can cherry-pick them.
|
||||
# ➡️ To add extra pip packages to the slim image, append them here:
|
||||
# e.g. pip install --no-cache-dir --prefix=/install . httpx polars
|
||||
RUN pip install --no-cache-dir --prefix=/install .
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Stage 2: Runtime
|
||||
# The actual image that ships. No compilers, no build tools, no sudo.
|
||||
# --------------------------------------------------------------------------
|
||||
FROM python:3.12-slim
|
||||
|
||||
# ── Runtime system packages ────────────────────────────────────────────────
|
||||
#
|
||||
# These are the only apt packages in the final image. The list is kept
|
||||
# intentionally tiny. If you need something extra, just add it below.
|
||||
#
|
||||
# Core: tini (PID 1), gosu (drop privileges)
|
||||
# Utilities: curl, git, jq, less, procps (ps/top)
|
||||
# Firewall: iptables, ipset, dnsmasq (egress whitelist)
|
||||
# Permissions: libcap2-bin (setcap for cap_setgid)
|
||||
#
|
||||
# ➡️ Want more tools? Add them here, one per line for easy diffs:
|
||||
# e.g. vim \
|
||||
# sqlite3 \
|
||||
#
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tini \
|
||||
gosu \
|
||||
curl \
|
||||
git \
|
||||
jq \
|
||||
less \
|
||||
procps \
|
||||
iptables \
|
||||
ipset \
|
||||
dnsmasq \
|
||||
libcap2-bin \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the pre-built Python packages from the builder stage.
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Install the app itself (fast — all deps are already present) and grant
|
||||
# the Python binary cap_setgid so multi-user mode can call setgid().
|
||||
# ⚠️ setcap MUST run in the same layer as the binary to avoid overlay2 issues.
|
||||
RUN pip install --no-cache-dir --no-deps . \
|
||||
&& setcap cap_setgid+ep "$(readlink -f "$(which python3)")"
|
||||
|
||||
# ── Non-root user ─────────────────────────────────────────────────────────
|
||||
#
|
||||
# The app runs as "user" (UID 1000) with NO sudo access.
|
||||
# The entrypoint handles any root-level setup (iptables, chown) via gosu.
|
||||
#
|
||||
RUN useradd -m -s /bin/bash -u 1000 user
|
||||
|
||||
COPY entrypoint-slim.sh /app/entrypoint-slim.sh
|
||||
RUN chmod +x /app/entrypoint-slim.sh
|
||||
|
||||
# ── NOTE: We do NOT set "USER user" here ──────────────────────────────────
|
||||
#
|
||||
# The container starts as root so the entrypoint can:
|
||||
# 1. Fix /home/user ownership on bind mounts
|
||||
# 2. Set up iptables egress rules (if configured)
|
||||
# 3. Drop privileges via gosu → runs the app as "user"
|
||||
#
|
||||
# If you don't need the egress firewall, you can add "USER user" here
|
||||
# and simplify the entrypoint — but gosu handles it cleanly either way.
|
||||
#
|
||||
ENV SHELL=/bin/bash
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# tini is PID 1 — reaps zombies and forwards signals cleanly.
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/app/entrypoint-slim.sh"]
|
||||
CMD ["run"]
|
||||
@@ -24,6 +24,29 @@ That's it — you're up and running at `http://localhost:8000`.
|
||||
> [!TIP]
|
||||
> If you don't set an API key, one is generated automatically. Grab it with `docker logs open-terminal`.
|
||||
|
||||
#### Image Variants
|
||||
|
||||
| Tag | Base | What's included | Size |
|
||||
|---|---|---|---|
|
||||
| `latest` | Debian | Full toolkit — Python, Node.js, git, Docker CLI, ffmpeg, data science libs, and more | ~2 GB |
|
||||
| `slim` | Debian slim | Terminal API + git, curl, jq. No Node.js, no Docker CLI, no sudo. | ~200 MB |
|
||||
| `alpine` | Alpine | Same as slim but on Alpine Linux with musl libc. Smallest possible image. | ~100 MB |
|
||||
|
||||
```bash
|
||||
# Slim — minimal Debian-based image
|
||||
docker run -d --name open-terminal -p 8000:8000 \
|
||||
-e OPEN_TERMINAL_API_KEY=your-secret-key \
|
||||
ghcr.io/open-webui/open-terminal:slim
|
||||
|
||||
# Alpine — smallest image
|
||||
docker run -d --name open-terminal -p 8000:8000 \
|
||||
-e OPEN_TERMINAL_API_KEY=your-secret-key \
|
||||
ghcr.io/open-webui/open-terminal:alpine
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The slim and Alpine images do **not** support `OPEN_TERMINAL_PACKAGES` or `OPEN_TERMINAL_PIP_PACKAGES` runtime installs. To add packages, build a custom image based on the slim/Alpine Dockerfile.
|
||||
|
||||
#### Updating
|
||||
|
||||
```bash
|
||||
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# ============================================================================
|
||||
# Open Terminal — Hardened Entrypoint (Slim & Alpine)
|
||||
# ============================================================================
|
||||
#
|
||||
# This entrypoint is shared by both the slim (Debian) and Alpine images.
|
||||
# It handles:
|
||||
# 1. Docker-secrets support (_FILE env vars)
|
||||
# 2. Home directory permissions & dotfile seeding
|
||||
# 3. Network egress filtering (iptables + dnsmasq whitelist)
|
||||
#
|
||||
# What it does NOT do (by design):
|
||||
# ✘ No runtime apt/pip package installation
|
||||
# ✘ No Docker socket detection
|
||||
# ✘ No sudo — privilege drop uses gosu (Debian) or su-exec (Alpine)
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
|
||||
# ── Helper: Docker-secrets _FILE resolution ────────────────────────────────
|
||||
#
|
||||
# Follows the convention from the official PostgreSQL image.
|
||||
# If OPEN_TERMINAL_API_KEY_FILE is set, its contents become the key.
|
||||
#
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
|
||||
local val="$def"
|
||||
eval local currentVal="\${$var:-}"
|
||||
eval local fileVal="\${$fileVar:-}"
|
||||
|
||||
if [ -n "$currentVal" ] && [ -n "$fileVal" ]; then
|
||||
printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$currentVal" ]; then
|
||||
val="$currentVal"
|
||||
elif [ -n "$fileVal" ]; then
|
||||
val="$(cat "$fileVal")"
|
||||
fi
|
||||
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
}
|
||||
|
||||
file_env 'OPEN_TERMINAL_API_KEY'
|
||||
|
||||
|
||||
# ── Helper: drop privileges to "user" ─────────────────────────────────────
|
||||
#
|
||||
# Detects gosu (Debian) vs su-exec (Alpine) automatically.
|
||||
# Both do the same thing: exec a command as a different user.
|
||||
#
|
||||
drop_to_user() {
|
||||
if command -v gosu >/dev/null 2>&1; then
|
||||
exec gosu user "$@"
|
||||
elif command -v su-exec >/dev/null 2>&1; then
|
||||
exec su-exec user "$@"
|
||||
else
|
||||
# Fallback: already running as the right user
|
||||
exec "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ── Home directory setup ──────────────────────────────────────────────────
|
||||
#
|
||||
# When /home/user is bind-mounted empty, Docker doesn't populate it with
|
||||
# the image contents. We seed essential dotfiles so bash works properly.
|
||||
#
|
||||
fix_home() {
|
||||
local home="/home/user"
|
||||
|
||||
# Fix ownership if the mount changed it (works on both GNU and BusyBox stat)
|
||||
local owner_uid
|
||||
owner_uid=$(stat -c '%u' "$home" 2>/dev/null) || owner_uid=$(stat -f '%u' "$home" 2>/dev/null) || owner_uid="1000"
|
||||
if [ "$owner_uid" != "1000" ]; then
|
||||
chown -R user:user "$home" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Seed bashrc / profile from skeleton if missing
|
||||
if [ ! -f "$home/.bashrc" ]; then
|
||||
if [ -f /etc/skel/.bashrc ]; then
|
||||
cp /etc/skel/.bashrc "$home/.bashrc" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
if [ ! -f "$home/.profile" ]; then
|
||||
if [ -f /etc/skel/.profile ]; then
|
||||
cp /etc/skel/.profile "$home/.profile" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$home/.local/bin"
|
||||
chown -R user:user "$home" 2>/dev/null || true
|
||||
}
|
||||
|
||||
|
||||
# ── Network egress filtering ──────────────────────────────────────────────
|
||||
#
|
||||
# OPEN_TERMINAL_ALLOWED_DOMAINS unset → full internet access
|
||||
# OPEN_TERMINAL_ALLOWED_DOMAINS="" → block ALL outbound traffic
|
||||
# OPEN_TERMINAL_ALLOWED_DOMAINS="a,b" → DNS whitelist via dnsmasq
|
||||
#
|
||||
# When restricted, a local dnsmasq resolves only whitelisted domains.
|
||||
# iptables blocks all other outbound traffic. CAP_NET_ADMIN is then
|
||||
# permanently dropped so the rules can't be undone from inside.
|
||||
#
|
||||
setup_egress_firewall() {
|
||||
# Not configured — allow everything
|
||||
if [ "${OPEN_TERMINAL_ALLOWED_DOMAINS+set}" != "set" ]; then
|
||||
return 1 # signal caller: no firewall, just exec normally
|
||||
fi
|
||||
|
||||
if ! command -v iptables >/dev/null 2>&1; then
|
||||
echo "WARNING: iptables not found — skipping egress firewall"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Flush any prior OUTPUT rules
|
||||
iptables -F OUTPUT 2>/dev/null || true
|
||||
|
||||
# Always allow loopback + established connections
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
if [ -z "$OPEN_TERMINAL_ALLOWED_DOMAINS" ]; then
|
||||
# ── Deny-all mode ──────────────────────────────────────────────
|
||||
echo "Egress: blocking ALL outbound traffic"
|
||||
iptables -A OUTPUT -j DROP
|
||||
else
|
||||
# ── Restricted mode (DNS whitelist + ipset) ────────────────────
|
||||
echo "Egress: DNS whitelist — $OPEN_TERMINAL_ALLOWED_DOMAINS"
|
||||
|
||||
# Grab the current upstream nameserver before we override resolv.conf
|
||||
UPSTREAM_DNS=$(grep -m1 '^nameserver' /etc/resolv.conf | awk '{print $2}')
|
||||
UPSTREAM_DNS="${UPSTREAM_DNS:-8.8.8.8}"
|
||||
|
||||
# Create ipset for dynamically resolved IPs
|
||||
ipset create allowed hash:ip -exist
|
||||
|
||||
# Generate dnsmasq config:
|
||||
# - NXDOMAIN for everything by default
|
||||
# - Forward allowed domains to upstream DNS
|
||||
# - Auto-add resolved IPs to the 'allowed' ipset
|
||||
mkdir -p /etc/dnsmasq.d
|
||||
{
|
||||
echo "no-resolv"
|
||||
echo "no-hosts"
|
||||
echo "listen-address=127.0.0.1"
|
||||
echo "port=53"
|
||||
echo "address=/#/" # NXDOMAIN for everything by default
|
||||
|
||||
# Process each comma-separated domain
|
||||
echo "$OPEN_TERMINAL_ALLOWED_DOMAINS" | tr ',' '\n' | while read -r domain; do
|
||||
domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[ -z "$domain" ] && continue
|
||||
# Strip wildcard prefix — dnsmasq matches all subdomains natively
|
||||
domain="${domain#\*.}"
|
||||
echo "server=/${domain}/${UPSTREAM_DNS}"
|
||||
echo "ipset=/${domain}/allowed"
|
||||
echo " ✓ ${domain} (+ subdomains)" >&2
|
||||
done
|
||||
} > /etc/dnsmasq.d/egress.conf
|
||||
|
||||
# Start dnsmasq as a background daemon
|
||||
dnsmasq --conf-file=/etc/dnsmasq.d/egress.conf
|
||||
echo "dnsmasq started (upstream: ${UPSTREAM_DNS})"
|
||||
|
||||
# Point the container at our local resolver
|
||||
echo "nameserver 127.0.0.1" > /etc/resolv.conf
|
||||
|
||||
# Allow ONLY resolved IPs (via ipset) + block everything else
|
||||
iptables -A OUTPUT -p udp --dport 53 -j DROP # block external DNS
|
||||
iptables -A OUTPUT -p tcp --dport 53 -j DROP # block external DNS
|
||||
iptables -A OUTPUT -m set --match-set allowed dst -j ACCEPT
|
||||
iptables -A OUTPUT -j DROP # drop everything else
|
||||
fi
|
||||
|
||||
echo "Egress firewall active — dropping CAP_NET_ADMIN permanently"
|
||||
return 0 # signal caller: firewall is active, use capsh
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
fix_home
|
||||
|
||||
# Export env vars for the app user (since we're running as root, these
|
||||
# won't be inherited automatically).
|
||||
export HOME="/home/user"
|
||||
export PATH="/home/user/.local/bin:${PATH}"
|
||||
|
||||
# Try to set up the egress firewall (requires running as root / CAP_NET_ADMIN).
|
||||
# If the firewall was configured, drop capabilities and switch to the app user.
|
||||
# If not, just switch to the app user normally.
|
||||
if setup_egress_firewall; then
|
||||
# Firewall is active — drop CAP_NET_ADMIN and exec as user
|
||||
if command -v capsh >/dev/null 2>&1; then
|
||||
exec capsh --drop=cap_net_admin -- -c "
|
||||
exec $(command -v gosu 2>/dev/null || command -v su-exec 2>/dev/null || echo exec) \
|
||||
user open-terminal $*
|
||||
"
|
||||
else
|
||||
drop_to_user open-terminal "$@"
|
||||
fi
|
||||
else
|
||||
drop_to_user open-terminal "$@"
|
||||
fi
|
||||
Reference in new issue
Block a user