#!/usr/bin/env bash # # A3 real nginx+njs integration acceptance (FIX C-native-3 / PLAN §1.2 B1 step 4). # # Proves the SINGLE load-bearing tenant-isolation control on a REAL nginx with the REAL njs module # and the REAL getCertSub.js, using device-CA-signed client certs whose dNSName SAN is stamped # exactly as the B1/A4 issuers stamp it: # # POSITIVE : alice client cert → https://alice.terminal.yaojia.wang → 200 (own host) # NEGATIVE : bob client cert → https://alice.terminal.yaojia.wang → 403 (cross-tenant) # NEGATIVE : alice client cert → https://bob.terminal.yaojia.wang → 403 (cross-tenant) # POSITIVE : bob client cert → https://bob.terminal.yaojia.wang → 200 # NEGATIVE : NO client cert → https://alice.terminal.yaojia.wang → 400 (ssl_verify_client on) # # Requires: docker + openssl. If docker is unavailable, this same script is the VPS/CI acceptance # step (run it there). Exit 0 iff every assertion holds. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" NJS_FILE="$(cd "$SCRIPT_DIR/../../njs" && pwd)/getCertSub.js" CONF="$SCRIPT_DIR/nginx.test.conf" IMAGE="wt-nginx-njs:a3-test" CONTAINER="wt-a3-nginx-$$" HOST_PORT="${HOST_PORT:-18470}" WORK="$(mktemp -d)" cleanup() { docker rm -f "$CONTAINER" >/dev/null 2>&1 || true rm -rf "$WORK" } trap cleanup EXIT echo "==> workdir: $WORK" echo "==> njs source under test: $NJS_FILE" # --------------------------------------------------------------------------- # 1. Mint the device CA, a server cert, and two client leaves with dNSName SANs # --------------------------------------------------------------------------- gen_ca() { openssl ecparam -name prime256v1 -genkey -noout -out "$WORK/ca.key" openssl req -x509 -new -key "$WORK/ca.key" -sha256 -days 1 -subj "/CN=device-CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -out "$WORK/ca.crt" } gen_server() { openssl ecparam -name prime256v1 -genkey -noout -out "$WORK/server.key" openssl req -x509 -new -key "$WORK/server.key" -sha256 -days 1 \ -subj "/CN=*.terminal.yaojia.wang" \ -addext "subjectAltName=DNS:*.terminal.yaojia.wang" \ -out "$WORK/server.crt" } # gen_client → mints .key/.crt with SAN dNSName .terminal... gen_client() { local name="$1" sub="$2" openssl ecparam -name prime256v1 -genkey -noout -out "$WORK/$name.key" openssl req -new -key "$WORK/$name.key" -subj "/CN=$sub" -out "$WORK/$name.csr" openssl x509 -req -in "$WORK/$name.csr" -CA "$WORK/ca.crt" -CAkey "$WORK/ca.key" \ -CAcreateserial -days 1 -sha256 \ -extfile <(printf 'subjectAltName=DNS:%s.terminal.yaojia.wang,URI:spiffe://terminal.yaojia.wang/account/a1/host/%s\nextendedKeyUsage=clientAuth\nbasicConstraints=critical,CA:FALSE\n' "$sub" "$sub") \ -out "$WORK/$name.crt" } echo "==> minting CA + server + client (alice, bob) certs" gen_ca gen_server gen_client alice alice gen_client bob bob # --------------------------------------------------------------------------- # 2. Build the nginx+njs image and start the container # --------------------------------------------------------------------------- echo "==> docker build ($IMAGE)" docker build -q -t "$IMAGE" "$SCRIPT_DIR" >/dev/null echo "==> docker run ($CONTAINER) on :$HOST_PORT" docker rm -f "$CONTAINER" >/dev/null 2>&1 || true docker run -d --name "$CONTAINER" \ -p "$HOST_PORT:8470" \ -v "$NJS_FILE:/etc/nginx/njs/getCertSub.js:ro" \ -v "$CONF:/etc/nginx/nginx.conf:ro" \ -v "$WORK:/certs:ro" \ "$IMAGE" >/dev/null # nginx -t inside the running image (config sanity on the real binary) echo "==> nginx -t (real binary)" docker exec "$CONTAINER" nginx -t # wait for readiness for i in $(seq 1 30); do if curl -sk -o /dev/null "https://alice.terminal.yaojia.wang:$HOST_PORT/" \ --resolve "alice.terminal.yaojia.wang:$HOST_PORT:127.0.0.1" \ --cert "$WORK/alice.crt" --key "$WORK/alice.key" 2>/dev/null; then break fi sleep 0.3 done # --------------------------------------------------------------------------- # 3. Assert positive (200) and negative (403 / 400) cases # --------------------------------------------------------------------------- # hit → prints HTTP status code hit() { local host="$1" client="$2" local args=(-sk -o /dev/null -w '%{http_code}' --resolve "$host:$HOST_PORT:127.0.0.1" "https://$host:$HOST_PORT/") if [ "$client" != "-" ]; then args+=(--cert "$WORK/$client.crt" --key "$WORK/$client.key") fi curl "${args[@]}" || echo "000" } FAILED=0 assert() { local label="$1" expected="$2" actual="$3" if [ "$actual" = "$expected" ]; then echo " PASS $label → $actual" else echo " FAIL $label → expected $expected, got $actual" FAILED=1 fi } echo "==> assertions" assert "POSITIVE alice-cert @ alice host" 200 "$(hit alice.terminal.yaojia.wang alice)" assert "NEGATIVE bob-cert @ alice host" 403 "$(hit alice.terminal.yaojia.wang bob)" assert "NEGATIVE alice-cert @ bob host" 403 "$(hit bob.terminal.yaojia.wang alice)" assert "POSITIVE bob-cert @ bob host" 200 "$(hit bob.terminal.yaojia.wang bob)" assert "NEGATIVE no-cert @ alice host" 400 "$(hit alice.terminal.yaojia.wang -)" if [ "$FAILED" -ne 0 ]; then echo "==> A3 INTEGRATION FAILED" docker logs "$CONTAINER" 2>&1 | tail -20 || true exit 1 fi echo "==> A3 INTEGRATION PASSED (positive 200 + cross-tenant 403 on real nginx+njs)"