HH-620: secure default host bindings
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+3
-2
@@ -2,7 +2,8 @@ IMAGE_NAME=mohomo-docker:local
|
||||
CONTAINER_NAME=mohomo-docker
|
||||
SUBSCRIPTION_URL=https://subscription.example.invalid/mihomo
|
||||
SSCLASH_PASSWORD=
|
||||
WEB_BIND=0.0.0.0
|
||||
# 9091 is always host-loopback; expose it through a host HTTPS reverse proxy.
|
||||
WEB_PORT=9091
|
||||
PROXY_BIND=0.0.0.0
|
||||
# Public opt-in: use 0.0.0.0 only behind a trusted-network firewall/ACL.
|
||||
PROXY_BIND=127.0.0.1
|
||||
PROXY_PORT=7890
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mohomo-docker
|
||||
|
||||
Minimal Mihomo service with the ACL4SSR `Online Full MultiMode` routing model. The host exposes the SSClash Web UI on port `9091` and the mixed proxy on port `7890`; Mihomo's controller remains private to the container.
|
||||
Minimal Mihomo service with the ACL4SSR `Online Full MultiMode` routing model. The host publishes the SSClash Web UI on loopback port `9091` and the mixed proxy on loopback port `7890` by default; Mihomo's controller remains private to the container.
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -12,14 +12,24 @@ docker compose up -d --build
|
||||
docker compose logs -f ssclash
|
||||
```
|
||||
|
||||
The subscription endpoint must return a Clash/Mihomo proxy-provider YAML document (`proxies:`). Use an HTTPS endpoint when its URL contains a credential. A fresh volume refuses to start without an `SSCLASH_PASSWORD` of at least 12 characters; bootstrap uses SSClash's own `setpass` command before the Web listener starts. Open `http://<server>:9091` and log in with that password. A valid existing authentication file is preserved, so later starts do not require or replace the password. Proxy clients connect to either endpoint:
|
||||
The subscription endpoint must return a Clash/Mihomo proxy-provider YAML document (`proxies:`). Use an HTTPS endpoint when its URL contains a credential. A fresh volume refuses to start without an `SSCLASH_PASSWORD` of at least 12 characters; bootstrap uses SSClash's own `setpass` command before the Web listener starts. On the Docker host, open `http://127.0.0.1:9091` and log in with that password. A valid existing authentication file is preserved, so later starts do not require or replace the password. Local proxy clients connect to either endpoint:
|
||||
|
||||
```text
|
||||
HTTP proxy: http://<server>:7890
|
||||
SOCKS5 proxy: socks5://<server>:7890
|
||||
HTTP proxy: http://127.0.0.1:7890
|
||||
SOCKS5 proxy: socks5://127.0.0.1:7890
|
||||
```
|
||||
|
||||
`WEB_BIND`, `WEB_PORT`, `PROXY_BIND`, and `PROXY_PORT` are optional deployment overrides; both services bind all host interfaces by default. Authentication prevents anonymous first-run setup, but the Web UI still serves plain HTTP: place it behind HTTPS and additional access control before exposing it to the Internet. Configure Mihomo proxy authentication before publishing port `7890` outside a trusted network.
|
||||
The Compose boundary fixes plaintext `9091` to host loopback. To provide the required external Web access, configure a host HTTPS reverse proxy to `127.0.0.1:${WEB_PORT:-9091}`; for example, a host-native Caddy configuration is:
|
||||
|
||||
```caddyfile
|
||||
ssclash.example.com {
|
||||
reverse_proxy 127.0.0.1:9091
|
||||
}
|
||||
```
|
||||
|
||||
Replace the domain and ensure its DNS reaches the host; Caddy then obtains and serves the TLS certificate. Do not publish 9091 directly as public HTTP.
|
||||
|
||||
`WEB_PORT`, `PROXY_BIND`, and `PROXY_PORT` are optional deployment overrides. Port 7890 also defaults to `127.0.0.1`; setting `PROXY_BIND=0.0.0.0` is the explicit public opt-in. The packaged Mihomo proxy has no client authentication, so use that opt-in only when a host firewall or network ACL restricts clients to a trusted range. Prefer binding `PROXY_BIND` to a specific trusted host address.
|
||||
|
||||
## Update and secret handling
|
||||
|
||||
@@ -56,7 +66,7 @@ The GitHub Actions workflow builds `linux/amd64`, runs tests first, publishes on
|
||||
./tests/container-smoke.sh
|
||||
```
|
||||
|
||||
The unit suite checks strict fail-closed authentication-file validation, provider-link recovery, atomic rollback, URL redaction, server-only listeners, local ACL4SSR providers, and at least 65% bootstrap coverage. The container smoke test verifies that a fresh volume without an administrator password never starts the Web UI, logs in through published port `9091`, checks the exact `7890`/`9091` port set, confirms that plaintext credentials are neither persisted nor logged, and repeats health and login checks after recreating the container with the same volume.
|
||||
The unit suite checks strict fail-closed authentication-file validation, provider-link recovery, atomic rollback, URL redaction, server-only listeners, local ACL4SSR providers, and at least 65% bootstrap coverage. The container smoke test verifies loopback-only Compose defaults and proxy-only public opt-in, proves 7890/9091 are unreachable through a non-loopback host address, checks fresh-volume authentication and credential isolation, and repeats health and login checks after recreating the container with the same volume.
|
||||
|
||||
## License boundary
|
||||
|
||||
|
||||
+4
-3
@@ -10,9 +10,10 @@ services:
|
||||
SUBSCRIPTION_URL: ${SUBSCRIPTION_URL:?set SUBSCRIPTION_URL in .env}
|
||||
SSCLASH_PASSWORD: ${SSCLASH_PASSWORD:-}
|
||||
ports:
|
||||
- "${WEB_BIND:-0.0.0.0}:${WEB_PORT:-9091}:9091/tcp"
|
||||
- "${PROXY_BIND:-0.0.0.0}:${PROXY_PORT:-7890}:7890/tcp"
|
||||
- "${PROXY_BIND:-0.0.0.0}:${PROXY_PORT:-7890}:7890/udp"
|
||||
# Keep the plaintext Web UI behind a host-local HTTPS reverse proxy.
|
||||
- "127.0.0.1:${WEB_PORT:-9091}:9091/tcp"
|
||||
- "${PROXY_BIND:-127.0.0.1}:${PROXY_PORT:-7890}:7890/tcp"
|
||||
- "${PROXY_BIND:-127.0.0.1}:${PROXY_PORT:-7890}:7890/udp"
|
||||
volumes:
|
||||
- ssclash-data:/opt/clash
|
||||
cap_drop:
|
||||
|
||||
@@ -76,6 +76,22 @@ assert_web_login() {
|
||||
"http://127.0.0.1:${web_port}/config" | grep -F 'csrf-token' >/dev/null
|
||||
}
|
||||
|
||||
default_compose=$(SUBSCRIPTION_URL=https://subscription.example.invalid/mihomo docker compose config)
|
||||
loopback_bindings=$(printf '%s\n' "$default_compose" | awk '$1 == "host_ip:" && $2 == "127.0.0.1" { count++ } END { print count + 0 }')
|
||||
if [ "$loopback_bindings" -ne 3 ]; then
|
||||
echo "Compose must bind 7890/tcp, 7890/udp, and 9091/tcp to host loopback by default" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
public_proxy_compose=$(SUBSCRIPTION_URL=https://subscription.example.invalid/mihomo \
|
||||
PROXY_BIND=0.0.0.0 WEB_BIND=0.0.0.0 docker compose config)
|
||||
public_bindings=$(printf '%s\n' "$public_proxy_compose" | awk '$1 == "host_ip:" && $2 == "0.0.0.0" { count++ } END { print count + 0 }')
|
||||
loopback_bindings=$(printf '%s\n' "$public_proxy_compose" | awk '$1 == "host_ip:" && $2 == "127.0.0.1" { count++ } END { print count + 0 }')
|
||||
if [ "$public_bindings" -ne 2 ] || [ "$loopback_bindings" -ne 1 ]; then
|
||||
echo "public opt-in must affect only 7890; 9091 must remain on host loopback" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker build --tag "$image" .
|
||||
docker run --rm --entrypoint /usr/local/lib/ssclash/clash "$image" \
|
||||
-t -d /usr/local/share/ssclash -f /usr/local/share/ssclash/config.yaml >/dev/null 2>&1 && {
|
||||
@@ -166,6 +182,21 @@ assert_published_ports
|
||||
web_port=$(docker port "$container" 9091/tcp | awk -F: 'NR == 1 { print $NF }')
|
||||
assert_web_login "$web_port"
|
||||
|
||||
host_gateway=$(docker network inspect "$network" --format '{{(index .IPAM.Config 0).Gateway}}')
|
||||
container_ip=$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container")
|
||||
proxy_port=$(docker port "$container" 7890/tcp | awk -F: 'NR == 1 { print $NF }')
|
||||
if curl --fail --silent --show-error --max-time 2 --noproxy "" \
|
||||
--proxy "http://${host_gateway}:${proxy_port}" \
|
||||
"http://${container_ip}:9090/version" >/dev/null 2>&1; then
|
||||
echo "default 7890 publish was reachable through a non-loopback host address" >&2
|
||||
exit 1
|
||||
fi
|
||||
if curl --fail --silent --show-error --max-time 2 \
|
||||
"http://${host_gateway}:${web_port}/login" >/dev/null 2>&1; then
|
||||
echo "default 9091 publish was reachable through a non-loopback host address" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker exec "$container" grep -Fx 'OPERATING_MODE=server' /opt/clash/.ssclash/settings >/dev/null
|
||||
docker exec "$container" grep -Fx 'PROXY_MODE=none' /opt/clash/.ssclash/settings >/dev/null
|
||||
docker exec "$container" test -s /dev/shm/mohomo/subscription.yaml
|
||||
|
||||
Reference in New Issue
Block a user