mirror of
https://github.com/langgenius/dify.git
synced 2026-06-22 19:21:13 +08:00
fix(docker): harden default SSRF proxy egress (#36332)
This commit is contained in:
parent
26b0137c83
commit
7bfcf9185c
@ -1,6 +1,7 @@
|
||||
# ------------------------------------------------------------------
|
||||
# Essential defaults for Docker Compose deployments.
|
||||
# Only include variables required for services to start.
|
||||
# Do not add optional variables to this file.
|
||||
#
|
||||
# For a default deployment, copy this file to .env and run:
|
||||
# docker compose up -d
|
||||
@ -200,8 +201,6 @@ SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
|
||||
SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128
|
||||
SSRF_HTTP_PORT=3128
|
||||
SSRF_COREDUMP_DIR=/var/spool/squid
|
||||
SSRF_REVERSE_PROXY_PORT=8194
|
||||
SSRF_SANDBOX_HOST=sandbox
|
||||
SSRF_DEFAULT_TIME_OUT=5
|
||||
SSRF_DEFAULT_CONNECT_TIME_OUT=5
|
||||
SSRF_DEFAULT_READ_TIME_OUT=5
|
||||
|
||||
@ -622,9 +622,8 @@ services:
|
||||
# pls clearly modify the squid env vars to fit your network environment.
|
||||
HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
|
||||
COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
|
||||
REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194}
|
||||
SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox}
|
||||
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
|
||||
SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}
|
||||
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-}
|
||||
networks:
|
||||
- ssrf_proxy_network
|
||||
- default
|
||||
|
||||
@ -212,12 +212,13 @@ services:
|
||||
# pls clearly modify the squid env vars to fit your network environment.
|
||||
HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
|
||||
COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
|
||||
REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194}
|
||||
SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox}
|
||||
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
|
||||
SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}
|
||||
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-}
|
||||
SSRF_SANDBOX_PROXY_PORT: ${SSRF_SANDBOX_PROXY_PORT:-8194}
|
||||
SSRF_SANDBOX_PROXY_HOST: ${SSRF_SANDBOX_PROXY_HOST:-sandbox}
|
||||
ports:
|
||||
- "${EXPOSE_SSRF_PROXY_PORT:-3128}:${SSRF_HTTP_PORT:-3128}"
|
||||
- "${EXPOSE_SANDBOX_PORT:-8194}:${SANDBOX_PORT:-8194}"
|
||||
- "${EXPOSE_SANDBOX_PORT:-8194}:${SSRF_SANDBOX_PROXY_PORT:-8194}"
|
||||
networks:
|
||||
- ssrf_proxy_network
|
||||
- default
|
||||
|
||||
@ -628,9 +628,8 @@ services:
|
||||
# pls clearly modify the squid env vars to fit your network environment.
|
||||
HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
|
||||
COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
|
||||
REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194}
|
||||
SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox}
|
||||
SANDBOX_PORT: ${SANDBOX_PORT:-8194}
|
||||
SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}
|
||||
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-}
|
||||
networks:
|
||||
- ssrf_proxy_network
|
||||
- default
|
||||
|
||||
@ -188,8 +188,6 @@ WEBHOOK_REQUEST_BODY_MAX_SIZE=10485760
|
||||
RESPECT_XFORWARD_HEADERS_ENABLED=false
|
||||
SSRF_HTTP_PORT=3128
|
||||
SSRF_COREDUMP_DIR=/var/spool/squid
|
||||
SSRF_REVERSE_PROXY_PORT=8194
|
||||
SSRF_SANDBOX_HOST=sandbox
|
||||
SSRF_DEFAULT_TIME_OUT=5
|
||||
SSRF_DEFAULT_CONNECT_TIME_OUT=5
|
||||
SSRF_DEFAULT_READ_TIME_OUT=5
|
||||
|
||||
@ -6,8 +6,8 @@ SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128
|
||||
SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128
|
||||
SSRF_HTTP_PORT=3128
|
||||
SSRF_COREDUMP_DIR=/var/spool/squid
|
||||
SSRF_REVERSE_PROXY_PORT=8194
|
||||
SSRF_SANDBOX_HOST=sandbox
|
||||
SSRF_PROXY_ALLOW_PRIVATE_IPS=
|
||||
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS=
|
||||
SSRF_DEFAULT_TIME_OUT=5
|
||||
SSRF_DEFAULT_CONNECT_TIME_OUT=5
|
||||
SSRF_DEFAULT_READ_TIME_OUT=5
|
||||
|
||||
@ -111,8 +111,10 @@ SANDBOX_PORT=8194
|
||||
# ------------------------------
|
||||
SSRF_HTTP_PORT=3128
|
||||
SSRF_COREDUMP_DIR=/var/spool/squid
|
||||
SSRF_REVERSE_PROXY_PORT=8194
|
||||
SSRF_SANDBOX_HOST=sandbox
|
||||
SSRF_PROXY_ALLOW_PRIVATE_IPS=
|
||||
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS=
|
||||
SSRF_SANDBOX_PROXY_PORT=8194
|
||||
SSRF_SANDBOX_PROXY_HOST=sandbox
|
||||
|
||||
# ------------------------------
|
||||
# Environment Variables for weaviate Service
|
||||
@ -240,4 +242,4 @@ LOGSTORE_DUAL_READ_ENABLED=true
|
||||
# Control flag for whether to write the `graph` field to LogStore.
|
||||
# If LOGSTORE_ENABLE_PUT_GRAPH_FIELD is "true", write the full `graph` field;
|
||||
# otherwise write an empty {} instead. Defaults to writing the `graph` field.
|
||||
LOGSTORE_ENABLE_PUT_GRAPH_FIELD=true
|
||||
LOGSTORE_ENABLE_PUT_GRAPH_FIELD=true
|
||||
|
||||
@ -26,6 +26,54 @@ tail -F /var/log/squid/error.log 2>/dev/null &
|
||||
tail -F /var/log/squid/store.log 2>/dev/null &
|
||||
tail -F /var/log/squid/cache.log 2>/dev/null &
|
||||
|
||||
ALLOW_PRIVATE_CONF=/etc/squid/dify_allow_private.conf
|
||||
SANDBOX_PROXY_CONF=/etc/squid/dify_sandbox_proxy.conf
|
||||
|
||||
write_optional_private_allowlist() {
|
||||
local env_name="$1"
|
||||
local acl_name="$2"
|
||||
local acl_type="$3"
|
||||
local raw_values="${!env_name:-}"
|
||||
|
||||
raw_values="${raw_values//,/ }"
|
||||
|
||||
if [ -z "${raw_values//[[:space:]]/}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
printf 'acl %s %s' "$acl_name" "$acl_type" >> "$ALLOW_PRIVATE_CONF"
|
||||
for value in $raw_values; do
|
||||
printf ' %s' "$value" >> "$ALLOW_PRIVATE_CONF"
|
||||
done
|
||||
printf '\nhttp_access allow client_localnet %s\n' "$acl_name" >> "$ALLOW_PRIVATE_CONF"
|
||||
}
|
||||
|
||||
{
|
||||
echo "# Generated by docker-entrypoint.sh."
|
||||
echo "# Allows selected private targets before the default private-network deny rule."
|
||||
} > "$ALLOW_PRIVATE_CONF"
|
||||
write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_IPS" "dify_allowed_private_networks" "dst"
|
||||
write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_DOMAINS" "dify_allowed_private_domains" "dstdomain"
|
||||
|
||||
{
|
||||
echo "# Generated by docker-entrypoint.sh."
|
||||
echo "# Enables the middleware-only sandbox host bridge when configured."
|
||||
} > "$SANDBOX_PROXY_CONF"
|
||||
|
||||
if [ -n "${SSRF_SANDBOX_PROXY_PORT:-}" ]; then
|
||||
sandbox_proxy_host="${SSRF_SANDBOX_PROXY_HOST:-sandbox}"
|
||||
sandbox_proxy_target_port="${SANDBOX_PORT:-8194}"
|
||||
|
||||
{
|
||||
printf 'http_port %s accel vhost\n' "$SSRF_SANDBOX_PROXY_PORT"
|
||||
printf 'cache_peer %s parent %s 0 no-query originserver name=dify_sandbox\n' \
|
||||
"$sandbox_proxy_host" \
|
||||
"$sandbox_proxy_target_port"
|
||||
printf 'acl dify_sandbox_proxy_port localport %s\n' "$SSRF_SANDBOX_PROXY_PORT"
|
||||
printf 'http_access allow dify_sandbox_proxy_port\n'
|
||||
} >> "$SANDBOX_PROXY_CONF"
|
||||
fi
|
||||
|
||||
# Replace environment variables in the template and output to the squid.conf
|
||||
echo "[ENTRYPOINT] replacing environment variables in the template"
|
||||
awk '{
|
||||
|
||||
@ -1,11 +1,26 @@
|
||||
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
|
||||
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
|
||||
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
|
||||
acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
|
||||
acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN)
|
||||
acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
|
||||
acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
acl client_localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
|
||||
acl client_localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
|
||||
acl client_localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
|
||||
acl client_localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
|
||||
acl client_localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN)
|
||||
acl client_localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
|
||||
acl client_localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl client_localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
acl to_private_networks dst 0.0.0.0/8
|
||||
acl to_private_networks dst 10.0.0.0/8
|
||||
acl to_private_networks dst 100.64.0.0/10
|
||||
acl to_private_networks dst 127.0.0.0/8
|
||||
acl to_private_networks dst 169.254.0.0/16
|
||||
acl to_private_networks dst 172.16.0.0/12
|
||||
acl to_private_networks dst 192.168.0.0/16
|
||||
acl to_private_networks dst 224.0.0.0/4
|
||||
acl to_private_networks dst 240.0.0.0/4
|
||||
acl to_private_networks dst ::/128
|
||||
acl to_private_networks dst ::1/128
|
||||
acl to_private_networks dst ::ffff:0:0/96 # IPv4-mapped
|
||||
acl to_private_networks dst ::/96 # deprecated IPv4-compatible
|
||||
acl to_private_networks dst fc00::/7
|
||||
acl to_private_networks dst fe80::/10
|
||||
acl SSL_ports port 443
|
||||
# acl SSL_ports port 1025-65535 # Enable the configuration to resolve this issue: https://github.com/langgenius/dify/issues/12792
|
||||
acl Safe_ports port 80 # http
|
||||
@ -20,18 +35,23 @@ acl Safe_ports port 591 # filemaker
|
||||
acl Safe_ports port 777 # multiling http
|
||||
acl CONNECT method CONNECT
|
||||
acl allowed_domains dstdomain .marketplace.dify.ai
|
||||
http_access allow allowed_domains
|
||||
|
||||
http_port ${HTTP_PORT}
|
||||
|
||||
http_access deny !Safe_ports
|
||||
http_access deny CONNECT !SSL_ports
|
||||
http_access allow localhost manager
|
||||
http_access deny manager
|
||||
include /etc/squid/dify_sandbox_proxy.conf
|
||||
include /etc/squid/dify_allow_private.conf
|
||||
http_access deny to_private_networks
|
||||
http_access allow allowed_domains
|
||||
http_access allow client_localnet
|
||||
http_access allow localhost
|
||||
include /etc/squid/conf.d/*.conf
|
||||
http_access deny all
|
||||
tcp_outgoing_address 0.0.0.0
|
||||
|
||||
################################## Proxy Server ################################
|
||||
http_port ${HTTP_PORT}
|
||||
coredump_dir ${COREDUMP_DIR}
|
||||
refresh_pattern ^ftp: 1440 20% 10080
|
||||
refresh_pattern ^gopher: 1440 0% 1440
|
||||
@ -47,11 +67,7 @@ refresh_pattern . 0 20% 4320
|
||||
# upstream proxy, set to your own upstream proxy IP to avoid SSRF attacks
|
||||
# cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default
|
||||
|
||||
################################## Reverse Proxy To Sandbox ################################
|
||||
http_port ${REVERSE_PROXY_PORT} accel vhost
|
||||
cache_peer ${SANDBOX_HOST} parent ${SANDBOX_PORT} 0 no-query originserver
|
||||
acl src_all src all
|
||||
http_access allow src_all
|
||||
################################## Request Buffer ################################
|
||||
|
||||
# Unless the option's size is increased, an error will occur when uploading more than two files.
|
||||
client_request_buffer_max_size 100 MB
|
||||
@ -103,4 +119,3 @@ access_log daemon:/var/log/squid/access.log dify_log
|
||||
|
||||
# Access log to track concurrent requests and timeouts
|
||||
logfile_rotate 10
|
||||
|
||||
|
||||
143
docker/ssrf_proxy/test_ssrf_proxy_config.sh
Executable file
143
docker/ssrf_proxy/test_ssrf_proxy_config.sh
Executable file
@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
IMAGE="${SSRF_PROXY_TEST_IMAGE:-ubuntu/squid:latest}"
|
||||
CLIENT_IMAGE="${SSRF_PROXY_TEST_CLIENT_IMAGE:-busybox:latest}"
|
||||
CONTAINER_NAME="${SSRF_PROXY_TEST_CONTAINER:-dify-ssrf-proxy-test-$$}"
|
||||
SANDBOX_CONTAINER_NAME="${CONTAINER_NAME}-sandbox"
|
||||
NETWORK_NAME="${SSRF_PROXY_TEST_NETWORK:-dify-ssrf-proxy-test-$$}"
|
||||
RUN_PUBLIC_CHECK="${SSRF_PROXY_TEST_PUBLIC_CHECK:-true}"
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
docker rm -f "$SANDBOX_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
http_code_for() {
|
||||
local proxy_url="$1"
|
||||
local target_url="$2"
|
||||
local output
|
||||
|
||||
output="$(
|
||||
docker run \
|
||||
--rm \
|
||||
--network "$NETWORK_NAME" \
|
||||
--env "http_proxy=$proxy_url" \
|
||||
--env "https_proxy=$proxy_url" \
|
||||
"$CLIENT_IMAGE" \
|
||||
wget -S -O /dev/null -T 10 "$target_url" 2>&1 || true
|
||||
)"
|
||||
|
||||
printf '%s\n' "$output" | awk '$1 ~ /^HTTP\// { code = $2 } END { print code }'
|
||||
}
|
||||
|
||||
direct_http_code_for() {
|
||||
local target_url="$1"
|
||||
local output
|
||||
|
||||
output="$(
|
||||
docker run \
|
||||
--rm \
|
||||
--network "$NETWORK_NAME" \
|
||||
"$CLIENT_IMAGE" \
|
||||
wget -S -O /dev/null -T 10 "$target_url" 2>&1 || true
|
||||
)"
|
||||
|
||||
printf '%s\n' "$output" | awk '$1 ~ /^HTTP\// { code = $2 } END { print code }'
|
||||
}
|
||||
|
||||
assert_private_target_blocked() {
|
||||
local proxy_url="$1"
|
||||
local target_url="$2"
|
||||
local status_code
|
||||
|
||||
status_code="$(http_code_for "$proxy_url" "$target_url")"
|
||||
if [[ "$status_code" != "403" ]]; then
|
||||
echo "Expected $target_url to be blocked with HTTP 403, got ${status_code:-no response}."
|
||||
docker logs "$CONTAINER_NAME" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_public_target_allowed() {
|
||||
local proxy_url="$1"
|
||||
local target_url="$2"
|
||||
local status_code
|
||||
|
||||
status_code="$(http_code_for "$proxy_url" "$target_url")"
|
||||
if [[ ! "$status_code" =~ ^[234][0-9][0-9]$ || "$status_code" == "403" ]]; then
|
||||
echo "Expected $target_url to remain reachable, got ${status_code:-no response}."
|
||||
docker logs "$CONTAINER_NAME" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_sandbox_bridge_allowed() {
|
||||
local target_url="$1"
|
||||
local status_code
|
||||
|
||||
status_code="$(direct_http_code_for "$target_url")"
|
||||
if [[ ! "$status_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
echo "Expected sandbox host bridge $target_url to remain reachable, got ${status_code:-no response}."
|
||||
docker logs "$CONTAINER_NAME" >&2 || true
|
||||
docker logs "$SANDBOX_CONTAINER_NAME" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
cleanup
|
||||
docker network create "$NETWORK_NAME" >/dev/null
|
||||
|
||||
docker run \
|
||||
--detach \
|
||||
--name "$SANDBOX_CONTAINER_NAME" \
|
||||
--network "$NETWORK_NAME" \
|
||||
--network-alias sandbox \
|
||||
"$CLIENT_IMAGE" \
|
||||
sh -c "mkdir -p /www && echo ok > /www/health && httpd -f -p 8194 -h /www" \
|
||||
>/dev/null
|
||||
|
||||
docker run \
|
||||
--detach \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--entrypoint sh \
|
||||
--network "$NETWORK_NAME" \
|
||||
--volume "$ROOT_DIR/docker/ssrf_proxy/squid.conf.template:/etc/squid/squid.conf.template:ro" \
|
||||
--volume "$ROOT_DIR/docker/ssrf_proxy/docker-entrypoint.sh:/docker-entrypoint-mount.sh:ro" \
|
||||
--env HTTP_PORT=3128 \
|
||||
--env COREDUMP_DIR=/var/spool/squid \
|
||||
--env SSRF_SANDBOX_PROXY_PORT=8194 \
|
||||
--env SSRF_SANDBOX_PROXY_HOST=sandbox \
|
||||
--env "SSRF_PROXY_ALLOW_PRIVATE_IPS=${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}" \
|
||||
--env "SSRF_PROXY_ALLOW_PRIVATE_DOMAINS=${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-}" \
|
||||
"$IMAGE" \
|
||||
-c "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" \
|
||||
>/dev/null
|
||||
|
||||
proxy_url="http://$CONTAINER_NAME:3128"
|
||||
for _ in {1..30}; do
|
||||
probe_status="$(http_code_for "$proxy_url" "http://127.0.0.1:80/")"
|
||||
if [[ -n "$probe_status" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ -z "${probe_status:-}" ]]; then
|
||||
echo "Squid proxy did not respond to probes."
|
||||
docker logs "$CONTAINER_NAME" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
assert_private_target_blocked "$proxy_url" "http://127.0.0.1:80/"
|
||||
assert_private_target_blocked "$proxy_url" "http://0.1.2.3:80/"
|
||||
assert_private_target_blocked "$proxy_url" "http://169.254.169.254/latest/meta-data/"
|
||||
|
||||
if [[ "$RUN_PUBLIC_CHECK" == "true" ]]; then
|
||||
assert_public_target_allowed "$proxy_url" "http://example.com/"
|
||||
fi
|
||||
|
||||
assert_sandbox_bridge_allowed "http://$CONTAINER_NAME:8194/health"
|
||||
Loading…
Reference in New Issue
Block a user