From 54491de05c4db5931af21a1487d7896464163d25 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 24 Oct 2026 04:00:02 +0900 Subject: [PATCH] refac --- entrypoint-slim.sh | 67 +++++++++++++++++++++++----------------------- entrypoint.sh | 46 ++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 50 deletions(-) diff --git a/entrypoint-slim.sh b/entrypoint-slim.sh index 8bee808..5edba30 100755 --- a/entrypoint-slim.sh +++ b/entrypoint-slim.sh @@ -113,35 +113,25 @@ fix_home() { # 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 + echo "ERROR: iptables is required for egress filtering" >&2 + exit 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 + # Capture upstream DNS before replacing resolv.conf, including in deny-all mode. + UPSTREAM_DNS=$(grep -m1 '^nameserver' /etc/resolv.conf | awk '{print $2}') + UPSTREAM_DNS="${UPSTREAM_DNS:-8.8.8.8}" 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 @@ -151,6 +141,7 @@ setup_egress_firewall() { # - Auto-add resolved IPs to the 'allowed' ipset mkdir -p /etc/dnsmasq.d { + echo "user=dnsmasq" echo "no-resolv" echo "no-hosts" echo "listen-address=127.0.0.1" @@ -176,15 +167,32 @@ setup_egress_firewall() { # 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 + # Match the original destination: Docker rewrites its embedded DNS port. + # Check DNS before allowing loopback or established connections. + for protocol in udp tcp; do + iptables -A OUTPUT -p "$protocol" -m conntrack --ctdir ORIGINAL \ + --ctorigdst "$UPSTREAM_DNS" --ctorigdstport 53 \ + -m owner --uid-owner dnsmasq -j ACCEPT + iptables -A OUTPUT -p "$protocol" -m conntrack --ctdir ORIGINAL \ + --ctorigdst 127.0.0.1 --ctorigdstport 53 -j ACCEPT + done fi + # Also block Docker's loopback DNS in deny-all mode. + for protocol in udp tcp; do + iptables -A OUTPUT -p "$protocol" -m conntrack --ctdir ORIGINAL \ + --ctorigdstport 53 -j DROP + done + # Prevent direct access to Docker DNS's translated high ports as well. + iptables -A OUTPUT -o lo -d "$UPSTREAM_DNS" -j DROP + iptables -A OUTPUT -o lo -j ACCEPT + iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + if [ -n "$OPEN_TERMINAL_ALLOWED_DOMAINS" ]; then + iptables -A OUTPUT -m set --match-set allowed dst -j ACCEPT + fi + iptables -A OUTPUT -j DROP + echo "Egress firewall active — dropping CAP_NET_ADMIN permanently" - return 0 # signal caller: firewall is active, use capsh } @@ -199,19 +207,10 @@ fix_home 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 +# Call setup outside a conditional so failures stop startup under set -e. +if [ "${OPEN_TERMINAL_ALLOWED_DOMAINS+set}" = "set" ]; then + setup_egress_firewall + exec capsh --drop=cap_net_admin --user=user -- -c 'exec open-terminal "$@"' -- "$@" else drop_to_user open-terminal "$@" fi diff --git a/entrypoint.sh b/entrypoint.sh index 8afbba5..d3a4c4f 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -94,29 +94,24 @@ fi # ----------------------------------------------------------------------- if [ "${OPEN_TERMINAL_ALLOWED_DOMAINS+set}" = "set" ]; then if ! command -v iptables &>/dev/null; then - echo "WARNING: iptables not found — skipping egress firewall" - exec open-terminal "$@" + echo "ERROR: iptables is required for egress filtering" >&2 + exit 1 fi # Flush any prior OUTPUT rules sudo iptables -F OUTPUT 2>/dev/null || true - # Always allow loopback + established connections - sudo iptables -A OUTPUT -o lo -j ACCEPT - sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + # Capture upstream DNS before replacing resolv.conf, including in deny-all mode. + UPSTREAM_DNS=$(grep -m1 '^nameserver' /etc/resolv.conf | awk '{print $2}') + UPSTREAM_DNS="${UPSTREAM_DNS:-8.8.8.8}" if [ -z "$OPEN_TERMINAL_ALLOWED_DOMAINS" ]; then # ── Deny-all mode ────────────────────────────────────────────── echo "Egress: blocking ALL outbound traffic" - sudo iptables -A OUTPUT -j DROP else # ── Restricted mode (DNS whitelist + ipset) ──────────────────── echo "Egress: DNS whitelist — $OPEN_TERMINAL_ALLOWED_DOMAINS" - # Capture 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 sudo ipset create allowed hash:ip -exist @@ -126,6 +121,7 @@ if [ "${OPEN_TERMINAL_ALLOWED_DOMAINS+set}" = "set" ]; then # - Auto-add resolved IPs to the 'allowed' ipset sudo mkdir -p /etc/dnsmasq.d { + echo "user=dnsmasq" echo "no-resolv" echo "no-hosts" echo "listen-address=127.0.0.1" @@ -151,15 +147,33 @@ if [ "${OPEN_TERMINAL_ALLOWED_DOMAINS+set}" = "set" ]; then # Point the container at our local resolver echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf > /dev/null - # iptables: allow ONLY resolved IPs (via ipset) + block everything else - sudo iptables -A OUTPUT -p udp --dport 53 -j DROP # block external DNS - sudo iptables -A OUTPUT -p tcp --dport 53 -j DROP # block external DNS - sudo iptables -A OUTPUT -m set --match-set allowed dst -j ACCEPT # allow resolved IPs - sudo iptables -A OUTPUT -j DROP # drop everything else + # Match the original destination: Docker rewrites its embedded DNS port. + # Check DNS before allowing loopback or established connections. + for protocol in udp tcp; do + sudo iptables -A OUTPUT -p "$protocol" -m conntrack --ctdir ORIGINAL \ + --ctorigdst "$UPSTREAM_DNS" --ctorigdstport 53 \ + -m owner --uid-owner dnsmasq -j ACCEPT + sudo iptables -A OUTPUT -p "$protocol" -m conntrack --ctdir ORIGINAL \ + --ctorigdst 127.0.0.1 --ctorigdstport 53 -j ACCEPT + done fi + # Also block Docker's loopback DNS in deny-all mode. + for protocol in udp tcp; do + sudo iptables -A OUTPUT -p "$protocol" -m conntrack --ctdir ORIGINAL \ + --ctorigdstport 53 -j DROP + done + # Prevent direct access to Docker DNS's translated high ports as well. + sudo iptables -A OUTPUT -o lo -d "$UPSTREAM_DNS" -j DROP + sudo iptables -A OUTPUT -o lo -j ACCEPT + sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + if [ -n "$OPEN_TERMINAL_ALLOWED_DOMAINS" ]; then + sudo iptables -A OUTPUT -m set --match-set allowed dst -j ACCEPT + fi + sudo iptables -A OUTPUT -j DROP + echo "Egress firewall active — dropping CAP_NET_ADMIN permanently" - exec capsh --drop=cap_net_admin -- -c "exec open-terminal $*" + exec sudo -E PATH="$PATH" capsh --drop=cap_net_admin --user=user -- -c 'exec open-terminal "$@"' -- "$@" fi exec open-terminal "$@"