openpencil/deploy/collab-relay-edge/validate.sh
2026-07-29 21:42:33 +08:00

286 lines
10 KiB
Bash
Executable file

#!/bin/sh
set -eu
LC_ALL=C
export LC_ALL
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
global_config="$script_dir/global-nginx.conf"
cn_config="$script_dir/cn-federation-nginx.conf"
cn_wss_config="$script_dir/../collab-relay/nginx.conf"
relay_dockerfile="$script_dir/../collab-relay/Dockerfile"
global_compose="$script_dir/compose.global.yaml"
cn_compose="$script_dir/compose.cn.yaml"
crl_rotation="$script_dir/rotate-cn-crl.sh"
nft_reference="$script_dir/global-new-connection-rate.nft"
nft_installer="$script_dir/install-global-new-connection-rate.sh"
nft_verifier="$script_dir/verify-global-new-connection-rate.sh"
rate_rules_verifier="$script_dir/verify-rate-rules.py"
global_deployer="$script_dir/deploy-global.sh"
global_service="$script_dir/openpencil-collab-relay-global.service.example"
require_literal() {
pattern=$1
file=$2
grep -F -- "$pattern" "$file" >/dev/null
}
require_single_upstream_server() {
upstream=$1
expected=$2
file=$3
[ "$(grep -Ec \
"^[[:space:]]*upstream[[:space:]][[:space:]]*$upstream[[:space:]]*\\{" \
"$file" || true)" -eq 1 ]
block=$(awk -v expected_name="$upstream" '
$1 == "upstream" && $2 == expected_name && $3 == "{" {
inside = 1
next
}
inside && /^[[:space:]]*}/ {
exit
}
inside {
print
}
' "$file")
[ "$(printf '%s\n' "$block" |
grep -Ec '^[[:space:]]*server[[:space:]]+' || true)" -eq 1 ]
printf '%s\n' "$block" | grep -F "server $expected;" >/dev/null
if printf '%s\n' "$block" |
grep -Eq '^[[:space:]]*include[[:space:]]+'
then
echo "upstream include directives are forbidden" >&2
exit 1
fi
}
validation_mode=${OPENPENCIL_RELAY_EDGE_VALIDATION_MODE:-scaffold}
case "$validation_mode" in
scaffold)
expected_cn_upstream=192.0.2.10:9443
expected_inner_upstream=192.0.2.20:8444
expected_outer_name=cn-federation.example.cn
;;
production)
: "${OPENPENCIL_RELAY_EDGE_EXPECTED_CN_FEDERATION_UPSTREAM:?set fixed CN federation IPv4:port}"
: "${OPENPENCIL_RELAY_EDGE_EXPECTED_CN_INNER_WSS_UPSTREAM:?set fixed CN inner WSS IPv4:port}"
: "${OPENPENCIL_RELAY_EDGE_EXPECTED_CN_FEDERATION_NAME:?set outer TLS DNS name}"
expected_cn_upstream=$OPENPENCIL_RELAY_EDGE_EXPECTED_CN_FEDERATION_UPSTREAM
expected_inner_upstream=$OPENPENCIL_RELAY_EDGE_EXPECTED_CN_INNER_WSS_UPSTREAM
expected_outer_name=$OPENPENCIL_RELAY_EDGE_EXPECTED_CN_FEDERATION_NAME
for endpoint in "$expected_cn_upstream" "$expected_inner_upstream"; do
if ! printf '%s\n' "$endpoint" |
grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}:[1-9][0-9]{0,4}$'
then
echo "production upstreams must be fixed IPv4:port literals" >&2
exit 1
fi
done
if ! printf '%s\n' "$expected_outer_name" |
grep -Eq '^[a-z0-9]([a-z0-9.-]*[a-z0-9])$' ||
printf '%s\n' "$expected_outer_name" |
grep -Eq '(^|\.)example\.(com|cn|net|org)$'
then
echo "production outer TLS name must be lowercase and non-example" >&2
exit 1
fi
;;
*)
echo "OPENPENCIL_RELAY_EDGE_VALIDATION_MODE must be scaffold or production" >&2
exit 2
;;
esac
for pattern in \
'listen 8443;' \
'limit_conn global_clients 32;' \
'proxy_pass cn_federation_listener;' \
'proxy_ssl on;' \
'proxy_ssl_verify on;' \
"proxy_ssl_name $expected_outer_name;" \
'proxy_ssl_certificate /run/secrets/global-edge-client-cert.pem;' \
'proxy_ssl_certificate_key /run/secrets/global-edge-client-key.pem;' \
'proxy_ssl_trusted_certificate /run/secrets/cn-federation-ca.pem;' \
'proxy_ssl_session_reuse off;' \
'proxy_next_upstream off;' \
'access_log off;'
do
require_literal "$pattern" "$global_config"
done
for pattern in \
'listen 9443 ssl;' \
'limit_conn trusted_global_edges 512;' \
'ssl_verify_client on;' \
'ssl_client_certificate /run/secrets/global-edge-client-ca.pem;' \
'ssl_crl /run/secrets/global-edge-client-crl.pem;' \
'ssl_handshake_timeout 5s;' \
'ssl_session_cache off;' \
'proxy_pass cn_inner_wss;' \
'proxy_next_upstream off;' \
'access_log off;'
do
require_literal "$pattern" "$cn_config"
done
require_single_upstream_server \
cn_federation_listener "$expected_cn_upstream" "$global_config"
require_single_upstream_server \
cn_inner_wss "$expected_inner_upstream" "$cn_config"
for config in "$global_config" "$cn_config"; do
[ "$(grep -Ec '^[[:space:]]*proxy_pass[[:space:]]+' "$config" || true)" -eq 1 ]
if grep -Eq '^[[:space:]]*include[[:space:]]+' "$config"; then
echo "Nginx include directives are forbidden in fixed edge configs" >&2
exit 1
fi
done
if grep -Eq \
'proxy_pass[[:space:]]+\$|proxy_next_upstream[[:space:]]+(on|error|timeout|http_)' \
"$global_config" "$cn_config"
then
echo "dynamic or fallback upstream is forbidden" >&2
exit 1
fi
if grep -Eq \
'ssl_preread[[:space:]]+on|listen[[:space:]]+8443[[:space:]]+ssl|proxy_protocol|set_real_ip_from' \
"$global_config" "$cn_config"
then
echo "Global must pass through inner TLS without inspecting source metadata" >&2
exit 1
fi
require_literal 'location = /v1/tunnel {' "$cn_wss_config"
require_literal 'proxy_set_header Authorization $http_authorization;' "$cn_wss_config"
require_literal 'proxy_pass_header OpenPencil-Relay-Challenge;' "$cn_wss_config"
[ "$(grep -Fc 'client_header_buffer_size 64k;' "$cn_wss_config")" -eq 2 ]
[ "$(grep -Fc 'large_client_header_buffers 2 64k;' "$cn_wss_config")" -eq 2 ]
require_literal \
'FROM rust:1.94-bookworm@sha256:6ae102bdbf528294bc79ad6e1fae682f6f7c2a6e6621506ba959f9685b308a55 AS build' \
"$relay_dockerfile"
require_literal \
'FROM gcr.io/distroless/cc-debian12:nonroot@sha256:fccdbb0a547c14e23fcf4ce8ad62ca5d43b4faae8d22cd292f490fef9946c96e' \
"$relay_dockerfile"
for compose_file in "$global_compose" "$cn_compose"; do
for pattern in \
'user: "101:101"' \
'@sha256:${OPENPENCIL_RELAY_EDGE_IMAGE_SHA256:?set its reviewed 64-hex digest}' \
'read_only: true' \
'cap_drop:' \
'- ALL' \
'no-new-privileges:true' \
'pids_limit:' \
'mem_limit:' \
'cpus:'
do
require_literal "$pattern" "$compose_file"
done
if grep -Eq \
'privileged:[[:space:]]*true|network_mode:[[:space:]]*host|user:[[:space:]]*(root|"?0)|cap_add:' \
"$compose_file"
then
echo "edge containers must remain non-root and unprivileged" >&2
exit 1
fi
done
require_literal 'published: 443' "$global_compose"
require_literal \
'host_ip: ${OPENPENCIL_RELAY_EDGE_PUBLIC_BIND_IP:?set a dedicated overseas public IPv4}' \
"$global_compose"
require_literal 'restart: "no"' "$global_compose"
require_literal 'restart: unless-stopped' "$cn_compose"
for pattern in \
'ip daddr 203.0.113.10 tcp dport 443' \
'ct state new meter relay_edge_new_v4' \
'ip saddr timeout 2m limit rate over 60/minute burst 20 packets' \
'counter drop'
do
require_literal "$pattern" "$nft_reference"
done
for pattern in \
'nft --check --file "$staged"' \
'nft --file "$staged"' \
'"$verify_script" "$public_ipv4"'
do
require_literal "$pattern" "$nft_installer"
done
for pattern in \
'nft --json --numeric list table inet "$table_name"' \
'"$rules_verifier" --address "$public_ipv4" --rules-json'
do
require_literal "$pattern" "$nft_verifier"
done
for pattern in \
'not address.is_global' \
'len(expressions) != 7' \
'"rate": 60' \
'"burst": 20' \
'"per": "minute"' \
'expect_equal(expressions[6], {"drop": None}'
do
require_literal "$pattern" "$rate_rules_verifier"
done
for pattern in \
'"$script_dir/install-global-new-connection-rate.sh"' \
'OPENPENCIL_RELAY_EDGE_VALIDATION_MODE=production' \
'--abort-on-container-exit --exit-code-from global-edge'
do
require_literal "$pattern" "$global_deployer"
done
for pattern in \
'Requires=docker.service nftables.service' \
'After=network-online.target nftables.service docker.service' \
'ExecStart=/opt/openpencil/deploy/collab-relay-edge/deploy-global.sh' \
'Restart=always'
do
require_literal "$pattern" "$global_service"
done
for pattern in \
'openssl crl' \
'openssl x509' \
'candidate CRLNumber must be strictly greater' \
'delta CRLs are forbidden' \
'candidate CRL drops an existing revoked certificate serial' \
'candidate CRL does not revoke the supplied Global edge certificate' \
'CRL activation requires root' \
'CRL/CA files must be root:101 mode 0440' \
'OPENPENCIL_RELAY_EDGE_VALIDATION_MODE=production' \
'Recheck the final staged inode after ownership and mode changes.' \
'mv -f "$staged" "$active"' \
'up -d --no-deps --force-recreate cn-federation' \
'exec -T cn-federation nginx -t'
do
require_literal "$pattern" "$crl_rotation"
done
if [ -n "${OPENPENCIL_RELAY_EDGE_IMAGE_SHA256:-}" ] &&
! printf '%s\n' "$OPENPENCIL_RELAY_EDGE_IMAGE_SHA256" |
grep -Eq '^[0-9a-f]{64}$'
then
echo "OPENPENCIL_RELAY_EDGE_IMAGE_SHA256 must be 64 lowercase hex characters" >&2
exit 1
fi
if command -v docker >/dev/null 2>&1 &&
docker compose version >/dev/null 2>&1
then
test_repository=nginx-unprivileged.invalid/test
test_digest=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OPENPENCIL_RELAY_EDGE_IMAGE_REPOSITORY=$test_repository \
OPENPENCIL_RELAY_EDGE_IMAGE_SHA256=$test_digest \
OPENPENCIL_RELAY_EDGE_PUBLIC_BIND_IP=203.0.113.10 \
OPENPENCIL_RELAY_EDGE_CLIENT_CERT=/dev/null \
OPENPENCIL_RELAY_EDGE_CLIENT_KEY=/dev/null \
OPENPENCIL_RELAY_CN_FEDERATION_CA=/dev/null \
docker compose -f "$global_compose" config -q
OPENPENCIL_RELAY_EDGE_IMAGE_REPOSITORY=$test_repository \
OPENPENCIL_RELAY_EDGE_IMAGE_SHA256=$test_digest \
OPENPENCIL_RELAY_CN_FEDERATION_BIND_IP=10.0.0.1 \
OPENPENCIL_RELAY_CN_FEDERATION_CERT=/dev/null \
OPENPENCIL_RELAY_CN_FEDERATION_KEY=/dev/null \
OPENPENCIL_RELAY_EDGE_CLIENT_CA=/dev/null \
OPENPENCIL_RELAY_EDGE_CLIENT_CRL=/dev/null \
docker compose -f "$cn_compose" config -q
fi
echo "nested-TLS collab relay edge validation passed ($validation_mode)"