From 3c8e3ce36b8679ef3c6fd8600d58bb354fa07328 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Mon, 18 May 2026 14:00:17 -0400 Subject: [PATCH 1/3] feat(e2e): enable mTLS for Podman compute driver e2e harness The Podman driver already supported mTLS certificate injection into sandbox containers, but the e2e test harness explicitly blocked HTTPS and ran plaintext-only gateways. Generate ephemeral PKI (CA, server, client certs) with host.containers.internal in the server SANs, wire guest_tls_* paths into the TOML config, replace --disable-tls with --tls-cert/--tls-key/--tls-client-ca, and switch to mTLS gateway registration. The health check remains on the plaintext health port. Closes #1428 --- e2e/rust/e2e-podman.sh | 2 +- e2e/with-podman-gateway.sh | 65 ++++++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/e2e/rust/e2e-podman.sh b/e2e/rust/e2e-podman.sh index c82891338..b352a8e18 100755 --- a/e2e/rust/e2e-podman.sh +++ b/e2e/rust/e2e-podman.sh @@ -4,7 +4,7 @@ # Run the Rust e2e suite against a standalone gateway running the bundled Podman # compute driver. Set OPENSHELL_GATEWAY_ENDPOINT=http://host:port to reuse an -# existing plaintext gateway instead of starting an ephemeral one. +# existing gateway instead of starting an ephemeral one. set -euo pipefail diff --git a/e2e/with-podman-gateway.sh b/e2e/with-podman-gateway.sh index 727737d25..dcca5a23b 100755 --- a/e2e/with-podman-gateway.sh +++ b/e2e/with-podman-gateway.sh @@ -11,8 +11,8 @@ # - OPENSHELL_GATEWAY_ENDPOINT=http://host:port: # Use the existing plaintext gateway endpoint and run the command. # -# Podman e2e currently uses plaintext gateway traffic. The Podman driver does -# not yet inject gateway mTLS client materials into sandbox containers. +# HTTPS endpoint-only mode is intentionally unsupported here. Use a named +# gateway config when mTLS materials are needed. set -euo pipefail @@ -277,12 +277,12 @@ if [ -n "${OPENSHELL_GATEWAY_ENDPOINT:-}" ]; then case "${OPENSHELL_GATEWAY_ENDPOINT}" in http://*) ;; https://*) - echo "ERROR: OPENSHELL_GATEWAY_ENDPOINT endpoint mode is HTTP-only for Podman e2e." >&2 - echo " Podman e2e does not yet support sandbox mTLS client material injection." >&2 + echo "ERROR: OPENSHELL_GATEWAY_ENDPOINT endpoint mode is HTTP-only for e2e." >&2 + echo " Register a named gateway with mTLS config instead of using a raw HTTPS endpoint." >&2 exit 2 ;; *) - echo "ERROR: OPENSHELL_GATEWAY_ENDPOINT must start with http:// for Podman e2e endpoint mode." >&2 + echo "ERROR: OPENSHELL_GATEWAY_ENDPOINT must start with http:// for e2e endpoint mode." >&2 exit 2 ;; esac @@ -313,6 +313,10 @@ if ! podman_cmd info >/dev/null 2>&1; then echo " Start it with 'podman machine start' on macOS, or the user service on Linux." >&2 exit 2 fi +if ! command -v openssl >/dev/null 2>&1; then + echo "ERROR: openssl is required to generate ephemeral PKI" >&2 + exit 2 +fi ensure_podman_api_socket e2e_build_gateway_binaries "${ROOT}" TARGET_DIR GATEWAY_BIN CLI_BIN @@ -328,6 +332,43 @@ if ! podman_cmd image exists "${SANDBOX_IMAGE}" 2>/dev/null; then podman_cmd pull "${SANDBOX_IMAGE}" fi +PKI_DIR="${WORKDIR}/pki" +mkdir -p "${PKI_DIR}" +cd "${PKI_DIR}" + +cat > openssl.cnf <<'EOF' +[req] +distinguished_name = dn +prompt = no +[dn] +CN = openshell-server +[san_server] +subjectAltName = @alt_server +[alt_server] +DNS.1 = localhost +DNS.2 = host.openshell.internal +DNS.3 = host.containers.internal +IP.1 = 127.0.0.1 +IP.2 = ::1 +[san_client] +subjectAltName = DNS:openshell-client +EOF + +openssl req -x509 -newkey rsa:2048 -nodes -days 30 \ + -keyout ca.key -out ca.crt -subj "/CN=openshell-e2e-ca" >/dev/null 2>&1 + +openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \ + -config openssl.cnf >/dev/null 2>&1 +openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -out server.crt -days 30 -extfile openssl.cnf -extensions san_server >/dev/null 2>&1 + +openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr \ + -subj "/CN=openshell-client" >/dev/null 2>&1 +openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -out client.crt -days 30 -extfile openssl.cnf -extensions san_client >/dev/null 2>&1 + +cd "${ROOT}" + HOST_PORT=$(e2e_pick_port) HEALTH_PORT=$(e2e_pick_port) STATE_DIR="${WORKDIR}/state" @@ -366,6 +407,9 @@ GATEWAY_CONFIG="${STATE_DIR}/gateway.toml" printf 'default_image = %s\n' "$(toml_string "${SANDBOX_IMAGE}")" printf 'image_pull_policy = "missing"\n' printf 'supervisor_image = %s\n' "$(toml_string "${SUPERVISOR_IMAGE}")" + printf 'guest_tls_ca = %s\n' "$(toml_string "${PKI_DIR}/ca.crt")" + printf 'guest_tls_cert = %s\n' "$(toml_string "${PKI_DIR}/client.crt")" + printf 'guest_tls_key = %s\n' "$(toml_string "${PKI_DIR}/client.key")" # The in-process Podman driver reads `socket_path` from TOML only — the # OPENSHELL_PODMAN_SOCKET env var is honoured by the standalone driver # binary, not the in-process driver used here. Pin the socket to the one @@ -382,7 +426,9 @@ GATEWAY_ARGS=( --port "${HOST_PORT}" --health-port "${HEALTH_PORT}" --drivers podman - --disable-tls + --tls-cert "${PKI_DIR}/server.crt" + --tls-key "${PKI_DIR}/server.key" + --tls-client-ca "${PKI_DIR}/ca.crt" --db-url "sqlite:${STATE_DIR}/gateway.db?mode=rwc" --log-level info ) @@ -401,12 +447,13 @@ GATEWAY_PID=$! printf '%s\n' "${GATEWAY_PID}" >"${GATEWAY_PID_FILE}" GATEWAY_NAME="openshell-e2e-podman-${HOST_PORT}" -CLI_GATEWAY_ENDPOINT="http://127.0.0.1:${HOST_PORT}" -e2e_register_plaintext_gateway \ +CLI_GATEWAY_ENDPOINT="https://127.0.0.1:${HOST_PORT}" +e2e_register_mtls_gateway \ "${XDG_CONFIG_HOME}" \ "${GATEWAY_NAME}" \ "${CLI_GATEWAY_ENDPOINT}" \ - "${HOST_PORT}" + "${HOST_PORT}" \ + "${PKI_DIR}" export OPENSHELL_GATEWAY="${GATEWAY_NAME}" export OPENSHELL_PROVISION_TIMEOUT="${OPENSHELL_PROVISION_TIMEOUT:-300}" From 5fed024dc0f73b79e086989381bd8e6a7fe6b55c Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Mon, 18 May 2026 14:14:06 -0400 Subject: [PATCH 2/3] refactor(e2e): extract shared PKI generation into gateway-common.sh The Docker and Podman e2e scripts had near-identical 35-line PKI generation blocks differing only in the host-gateway SAN. Extract into e2e_generate_pki(pki_dir, host_alias) in gateway-common.sh, which also eliminates the cd/cd pattern by using absolute paths throughout. --- e2e/support/gateway-common.sh | 45 +++++++++++++++++++++++++++++++++++ e2e/with-docker-gateway.sh | 36 +--------------------------- e2e/with-podman-gateway.sh | 36 +--------------------------- 3 files changed, 47 insertions(+), 70 deletions(-) diff --git a/e2e/support/gateway-common.sh b/e2e/support/gateway-common.sh index d8acbd191..17d7d12ea 100644 --- a/e2e/support/gateway-common.sh +++ b/e2e/support/gateway-common.sh @@ -34,6 +34,51 @@ e2e_pick_port() { python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()' } +e2e_generate_pki() { + local pki_dir=$1 + local host_alias=$2 # e.g. host.docker.internal or host.containers.internal + + mkdir -p "${pki_dir}" + + cat > "${pki_dir}/openssl.cnf" </dev/null 2>&1 + + openssl req -newkey rsa:2048 -nodes \ + -keyout "${pki_dir}/server.key" -out "${pki_dir}/server.csr" \ + -config "${pki_dir}/openssl.cnf" >/dev/null 2>&1 + openssl x509 -req -in "${pki_dir}/server.csr" \ + -CA "${pki_dir}/ca.crt" -CAkey "${pki_dir}/ca.key" -CAcreateserial \ + -out "${pki_dir}/server.crt" -days 30 \ + -extfile "${pki_dir}/openssl.cnf" -extensions san_server >/dev/null 2>&1 + + openssl req -newkey rsa:2048 -nodes \ + -keyout "${pki_dir}/client.key" -out "${pki_dir}/client.csr" \ + -subj "/CN=openshell-client" >/dev/null 2>&1 + openssl x509 -req -in "${pki_dir}/client.csr" \ + -CA "${pki_dir}/ca.crt" -CAkey "${pki_dir}/ca.key" -CAcreateserial \ + -out "${pki_dir}/client.crt" -days 30 \ + -extfile "${pki_dir}/openssl.cnf" -extensions san_client >/dev/null 2>&1 +} + e2e_register_plaintext_gateway() { local config_home=$1 local name=$2 diff --git a/e2e/with-docker-gateway.sh b/e2e/with-docker-gateway.sh index 83e4185f2..cf7a46436 100755 --- a/e2e/with-docker-gateway.sh +++ b/e2e/with-docker-gateway.sh @@ -390,41 +390,7 @@ if ! docker image inspect "${SANDBOX_IMAGE}" >/dev/null 2>&1; then fi PKI_DIR="${WORKDIR}/pki" -mkdir -p "${PKI_DIR}" -cd "${PKI_DIR}" - -cat > openssl.cnf <<'EOF' -[req] -distinguished_name = dn -prompt = no -[dn] -CN = openshell-server -[san_server] -subjectAltName = @alt_server -[alt_server] -DNS.1 = localhost -DNS.2 = host.openshell.internal -DNS.3 = host.docker.internal -IP.1 = 127.0.0.1 -IP.2 = ::1 -[san_client] -subjectAltName = DNS:openshell-client -EOF - -openssl req -x509 -newkey rsa:2048 -nodes -days 30 \ - -keyout ca.key -out ca.crt -subj "/CN=openshell-e2e-ca" >/dev/null 2>&1 - -openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \ - -config openssl.cnf >/dev/null 2>&1 -openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out server.crt -days 30 -extfile openssl.cnf -extensions san_server >/dev/null 2>&1 - -openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr \ - -subj "/CN=openshell-client" >/dev/null 2>&1 -openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out client.crt -days 30 -extfile openssl.cnf -extensions san_client >/dev/null 2>&1 - -cd "${ROOT}" +e2e_generate_pki "${PKI_DIR}" "host.docker.internal" HOST_PORT=$(e2e_pick_port) STATE_DIR="${WORKDIR}/state" diff --git a/e2e/with-podman-gateway.sh b/e2e/with-podman-gateway.sh index dcca5a23b..c4c73a996 100755 --- a/e2e/with-podman-gateway.sh +++ b/e2e/with-podman-gateway.sh @@ -333,41 +333,7 @@ if ! podman_cmd image exists "${SANDBOX_IMAGE}" 2>/dev/null; then fi PKI_DIR="${WORKDIR}/pki" -mkdir -p "${PKI_DIR}" -cd "${PKI_DIR}" - -cat > openssl.cnf <<'EOF' -[req] -distinguished_name = dn -prompt = no -[dn] -CN = openshell-server -[san_server] -subjectAltName = @alt_server -[alt_server] -DNS.1 = localhost -DNS.2 = host.openshell.internal -DNS.3 = host.containers.internal -IP.1 = 127.0.0.1 -IP.2 = ::1 -[san_client] -subjectAltName = DNS:openshell-client -EOF - -openssl req -x509 -newkey rsa:2048 -nodes -days 30 \ - -keyout ca.key -out ca.crt -subj "/CN=openshell-e2e-ca" >/dev/null 2>&1 - -openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \ - -config openssl.cnf >/dev/null 2>&1 -openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out server.crt -days 30 -extfile openssl.cnf -extensions san_server >/dev/null 2>&1 - -openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr \ - -subj "/CN=openshell-client" >/dev/null 2>&1 -openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out client.crt -days 30 -extfile openssl.cnf -extensions san_client >/dev/null 2>&1 - -cd "${ROOT}" +e2e_generate_pki "${PKI_DIR}" "host.containers.internal" HOST_PORT=$(e2e_pick_port) HEALTH_PORT=$(e2e_pick_port) From 99170b56402e39df0369e38f844c85866338a710 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Mon, 18 May 2026 14:59:16 -0400 Subject: [PATCH 3/3] refactor(e2e): use generate-certs instead of raw openssl for PKI Replace the hand-rolled openssl PKI generation in e2e_generate_pki() with the gateway's built-in generate-certs --output-dir command. This uses the same PKI generation as production deployments (Helm and RPM). The helper now takes the gateway binary path and passes extra SANs via --server-san (host.openshell.internal always, plus the driver's host alias for Podman). The default SAN list already includes host.docker.internal and localhost. Update all cert path references to match the generate-certs output layout (server/tls.{crt,key}, client/tls.{crt,key}) and remove the openssl preflight checks from both gateway scripts. --- e2e/support/gateway-common.sh | 59 +++++++++-------------------------- e2e/with-docker-gateway.sh | 14 +++------ e2e/with-podman-gateway.sh | 14 +++------ 3 files changed, 25 insertions(+), 62 deletions(-) diff --git a/e2e/support/gateway-common.sh b/e2e/support/gateway-common.sh index 17d7d12ea..2f8a2c141 100644 --- a/e2e/support/gateway-common.sh +++ b/e2e/support/gateway-common.sh @@ -35,48 +35,19 @@ e2e_pick_port() { } e2e_generate_pki() { - local pki_dir=$1 - local host_alias=$2 # e.g. host.docker.internal or host.containers.internal - - mkdir -p "${pki_dir}" - - cat > "${pki_dir}/openssl.cnf" </dev/null 2>&1 - - openssl req -newkey rsa:2048 -nodes \ - -keyout "${pki_dir}/server.key" -out "${pki_dir}/server.csr" \ - -config "${pki_dir}/openssl.cnf" >/dev/null 2>&1 - openssl x509 -req -in "${pki_dir}/server.csr" \ - -CA "${pki_dir}/ca.crt" -CAkey "${pki_dir}/ca.key" -CAcreateserial \ - -out "${pki_dir}/server.crt" -days 30 \ - -extfile "${pki_dir}/openssl.cnf" -extensions san_server >/dev/null 2>&1 - - openssl req -newkey rsa:2048 -nodes \ - -keyout "${pki_dir}/client.key" -out "${pki_dir}/client.csr" \ - -subj "/CN=openshell-client" >/dev/null 2>&1 - openssl x509 -req -in "${pki_dir}/client.csr" \ - -CA "${pki_dir}/ca.crt" -CAkey "${pki_dir}/ca.key" -CAcreateserial \ - -out "${pki_dir}/client.crt" -days 30 \ - -extfile "${pki_dir}/openssl.cnf" -extensions san_client >/dev/null 2>&1 + "${gateway_bin}" generate-certs --output-dir "${pki_dir}" "${san_args[@]}" } e2e_register_plaintext_gateway() { @@ -108,9 +79,9 @@ e2e_register_mtls_gateway() { local gateway_config_dir="${config_home}/openshell/gateways/${name}" mkdir -p "${gateway_config_dir}/mtls" - cp "${pki_dir}/ca.crt" "${gateway_config_dir}/mtls/ca.crt" - cp "${pki_dir}/client.crt" "${gateway_config_dir}/mtls/tls.crt" - cp "${pki_dir}/client.key" "${gateway_config_dir}/mtls/tls.key" + cp "${pki_dir}/ca.crt" "${gateway_config_dir}/mtls/ca.crt" + cp "${pki_dir}/client/tls.crt" "${gateway_config_dir}/mtls/tls.crt" + cp "${pki_dir}/client/tls.key" "${gateway_config_dir}/mtls/tls.key" cat >"${gateway_config_dir}/metadata.json" </dev/null 2>&1; then echo "ERROR: docker daemon is not reachable (docker info failed)" >&2 exit 2 fi -if ! command -v openssl >/dev/null 2>&1; then - echo "ERROR: openssl is required to generate ephemeral PKI" >&2 - exit 2 -fi if [ "${GPU_MODE}" = "1" ]; then DOCKER_CDI_SPEC_DIRS="$(docker info --format '{{json .CDISpecDirs}}' 2>/dev/null || true)" if [ -z "${DOCKER_CDI_SPEC_DIRS}" ] \ @@ -390,7 +386,7 @@ if ! docker image inspect "${SANDBOX_IMAGE}" >/dev/null 2>&1; then fi PKI_DIR="${WORKDIR}/pki" -e2e_generate_pki "${PKI_DIR}" "host.docker.internal" +e2e_generate_pki "${GATEWAY_BIN}" "${PKI_DIR}" HOST_PORT=$(e2e_pick_port) STATE_DIR="${WORKDIR}/state" @@ -439,8 +435,8 @@ GATEWAY_CONFIG="${STATE_DIR}/gateway.toml" printf 'default_image = %s\n' "$(toml_string "${SANDBOX_IMAGE}")" printf 'image_pull_policy = "IfNotPresent"\n' printf 'guest_tls_ca = %s\n' "$(toml_string "${PKI_DIR}/ca.crt")" - printf 'guest_tls_cert = %s\n' "$(toml_string "${PKI_DIR}/client.crt")" - printf 'guest_tls_key = %s\n' "$(toml_string "${PKI_DIR}/client.key")" + printf 'guest_tls_cert = %s\n' "$(toml_string "${PKI_DIR}/client/tls.crt")" + printf 'guest_tls_key = %s\n' "$(toml_string "${PKI_DIR}/client/tls.key")" # DOCKER_SUPERVISOR_ARGS holds either ("--docker-supervisor-bin" "") # or ("--docker-supervisor-image" ""); both map to TOML keys on # the docker driver config. @@ -464,8 +460,8 @@ GATEWAY_ARGS=( --bind-address 0.0.0.0 --port "${HOST_PORT}" --drivers docker - --tls-cert "${PKI_DIR}/server.crt" - --tls-key "${PKI_DIR}/server.key" + --tls-cert "${PKI_DIR}/server/tls.crt" + --tls-key "${PKI_DIR}/server/tls.key" --tls-client-ca "${PKI_DIR}/ca.crt" --db-url "sqlite:${STATE_DIR}/gateway.db?mode=rwc" ) diff --git a/e2e/with-podman-gateway.sh b/e2e/with-podman-gateway.sh index c4c73a996..d3b93e51a 100755 --- a/e2e/with-podman-gateway.sh +++ b/e2e/with-podman-gateway.sh @@ -313,10 +313,6 @@ if ! podman_cmd info >/dev/null 2>&1; then echo " Start it with 'podman machine start' on macOS, or the user service on Linux." >&2 exit 2 fi -if ! command -v openssl >/dev/null 2>&1; then - echo "ERROR: openssl is required to generate ephemeral PKI" >&2 - exit 2 -fi ensure_podman_api_socket e2e_build_gateway_binaries "${ROOT}" TARGET_DIR GATEWAY_BIN CLI_BIN @@ -333,7 +329,7 @@ if ! podman_cmd image exists "${SANDBOX_IMAGE}" 2>/dev/null; then fi PKI_DIR="${WORKDIR}/pki" -e2e_generate_pki "${PKI_DIR}" "host.containers.internal" +e2e_generate_pki "${GATEWAY_BIN}" "${PKI_DIR}" "host.containers.internal" HOST_PORT=$(e2e_pick_port) HEALTH_PORT=$(e2e_pick_port) @@ -374,8 +370,8 @@ GATEWAY_CONFIG="${STATE_DIR}/gateway.toml" printf 'image_pull_policy = "missing"\n' printf 'supervisor_image = %s\n' "$(toml_string "${SUPERVISOR_IMAGE}")" printf 'guest_tls_ca = %s\n' "$(toml_string "${PKI_DIR}/ca.crt")" - printf 'guest_tls_cert = %s\n' "$(toml_string "${PKI_DIR}/client.crt")" - printf 'guest_tls_key = %s\n' "$(toml_string "${PKI_DIR}/client.key")" + printf 'guest_tls_cert = %s\n' "$(toml_string "${PKI_DIR}/client/tls.crt")" + printf 'guest_tls_key = %s\n' "$(toml_string "${PKI_DIR}/client/tls.key")" # The in-process Podman driver reads `socket_path` from TOML only — the # OPENSHELL_PODMAN_SOCKET env var is honoured by the standalone driver # binary, not the in-process driver used here. Pin the socket to the one @@ -392,8 +388,8 @@ GATEWAY_ARGS=( --port "${HOST_PORT}" --health-port "${HEALTH_PORT}" --drivers podman - --tls-cert "${PKI_DIR}/server.crt" - --tls-key "${PKI_DIR}/server.key" + --tls-cert "${PKI_DIR}/server/tls.crt" + --tls-key "${PKI_DIR}/server/tls.key" --tls-client-ca "${PKI_DIR}/ca.crt" --db-url "sqlite:${STATE_DIR}/gateway.db?mode=rwc" --log-level info