Files
ModelForge-Public/docs/operations/VALIDATION_TLS_TEST_PORT.md

3.5 KiB

Candidate HTTPS on the canonical hostname

Port 443 is reserved for the existing production stack. A retained, isolated candidate uses https://modelforge.example.com:18444. The two ports never share a proxy route. Before configuring port 443, verify that the existing production Console uses the HTTPS API origin and matching CSP through a separately authorized production release. The candidate's tls service belongs to its own Compose project and only joins that project's ingress network. It sends /api/* and /v1/* to that project's API and everything else to that project's Web service.

Use a valid Nginx Proxy Manager certificate for the canonical hostname. Its key must not be mounted directly from NPM or copied into the source tree or image build context. For each retained candidate, stage only fullchain.pem and privkey.pem under a root-owned tls directory inside that candidate's private root but outside its source directory. After choosing an exact candidate root and certificate directory:

candidate_root=/path/to/private/modelforge-validation/EXACT_PROJECT
npm_live_dir=/path/to/npm/letsencrypt/live/CERTIFICATE_ID
tls_dir="${candidate_root}/tls"
install -d -m 0700 "${candidate_root}"
install -d -m 0700 "${tls_dir}"
install -m 0600 "${npm_live_dir}/fullchain.pem" "${tls_dir}/fullchain.pem"
install -m 0600 "${npm_live_dir}/privkey.pem" "${tls_dir}/privkey.pem"
openssl x509 -in "${tls_dir}/fullchain.pem" -noout -checkend 86400
openssl x509 -in "${tls_dir}/fullchain.pem" -noout -ext subjectAltName
test "$(openssl x509 -in "${tls_dir}/fullchain.pem" -pubkey -noout | sha256sum | cut -d' ' -f1)" = \
     "$(openssl pkey -in "${tls_dir}/privkey.pem" -pubout | sha256sum | cut -d' ' -f1)"

Confirm the SAN contains exactly the intended hostname and the public keys match. Set MODELFORGE_CANDIDATE_TLS_DIR to this absolute tls_dir, and build the Web image with VITE_API_BASE_URL=https://modelforge.example.com:18444. The API's MODELFORGE_CORS_ORIGINS must be the same origin. Use the base, production, docker-compose.rc-ingress.yml, and docker-compose.validation-tls.yml overlays in that order. The rc in the historical overlay filename is not a runtime name. The Compose project must use the neutral modelforge-validation-* namespace.

Before up, render the exact Compose JSON and pass it through scripts/rc_isolation_preflight.py with --public-tls-port 18444, candidate root, protected production ports and resource names. The preflight admits this one public port only for the isolated tls service; all other published ports must remain loopback only. Run the observed-resource preflight again after up. Scan the exact pinned Caddy image ID with the release Trivy severity policy and retain its report alongside the four application-image scans. Do not start the candidate while the host resource guard is active.

Verify the hostname and certificate without -k or --insecure:

server_ip=SERVER_LAN_IP
curl --fail --show-error --resolve "modelforge.example.com:18444:${server_ip}" \
  https://modelforge.example.com:18444/api/v1/health/live

Verify the Console, its browser API requests, CSP and authentication against the same origin. Record the exact image IDs, source SHA, certificate expiry, and before and after production health. The staged key is per candidate and must be removed with its candidate root only after retained evidence and cleanup checks pass. If NPM renews the certificate while a candidate remains live, copy the new pair and restart only that candidate's tls service after checking the SAN and key.