From dfff5b6d3905da6afa0f4c11c0cfce570269b7e4 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 16:25:25 -0700 Subject: [PATCH 01/11] fix(snap): require mTLS for the snap gateway Replace the installer opt-in with an authenticated snap gateway. The wrapper no longer forces plaintext, so the gateway serves TLS from the bundle it already generates in $SNAP_COMMON/tls. The install hook writes a config that enables mTLS user auth instead of unauthenticated access, and a new post-refresh hook migrates the exact legacy default on existing installs. install.sh waits for the gateway, detects whether it serves TLS, copies the client bundle into the target user's snap state directory, and registers the gateway over HTTPS. Older plaintext snap revisions still register over HTTP with a warning. The release canary asserts mTLS auth and HTTPS registration. Signed-off-by: Drew Newberry --- .github/workflows/release-canary.yml | 3 ++ install.sh | 45 ++++++++++++++-- snap/hooks/install | 38 +++++++++++-- snap/hooks/post-refresh | 6 +++ snapcraft.yaml | 25 +++++---- tasks/scripts/snap-gateway-wrapper.sh | 16 +++--- tasks/scripts/test-install-sh.sh | 62 ++++++++++++++++++++-- tasks/scripts/test-packaging-assets.sh | 7 ++- tasks/scripts/test-snap-gateway-wrapper.sh | 26 ++++----- tasks/scripts/test-snap-install-hook.sh | 43 +++++++++++++++ 10 files changed, 227 insertions(+), 44 deletions(-) create mode 100755 snap/hooks/post-refresh diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index 239f49eced..e81094098f 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -232,6 +232,9 @@ jobs: sudo snap connections openshell | grep -E '^docker +openshell:docker +:docker +' openshell --version sudo snap services openshell + sudo journalctl -b -u snap.openshell.gateway.service --no-pager | + grep -F "mTLS user authentication enabled" + openshell gateway list | grep -F "https://127.0.0.1:17670" openshell status - name: Create and exercise a sandbox diff --git a/install.sh b/install.sh index 60bfd6f64c..7ccfd86c2c 100755 --- a/install.sh +++ b/install.sh @@ -1220,6 +1220,9 @@ openshell_snap_channel() { esac } +# Fallback for snap revisions that predate the install hook, which serve +# plaintext HTTP. Current revisions create an mTLS config in their install +# hook, and their post-refresh hook migrates this legacy default. ensure_snap_gateway_config() { _config_file="${1:-/var/snap/openshell/common/gateway.toml}" @@ -1280,9 +1283,32 @@ wait_for_docker_daemon() { error "Docker daemon did not become reachable within ${_timeout}s" } +# Copy the snap gateway's client bundle into the target user's snap state +# directory, where `openshell gateway add --local` imports it. Root only reads +# the source files; the target user writes the copies into their own home. +copy_snap_client_bundle() { + _src="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}" + _dst="${TARGET_HOME}/snap/openshell/common/.local/state/openshell/tls" + + as_target_user mkdir -p "${_dst}/client" + as_target_user chmod 700 "$_dst" "${_dst}/client" + for _file in ca.crt client/tls.crt client/tls.key; do + as_root cat "${_src}/${_file}" | + as_target_user sh -c 'umask 077; cat >"$1"' sh "${_dst}/${_file}" + done +} + register_snap_gateway() { _register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}" - _endpoint="http://127.0.0.1:${LOCAL_GATEWAY_PORT}" + _scheme="${SNAP_GATEWAY_SCHEME:-https}" + _endpoint="${_scheme}://127.0.0.1:${LOCAL_GATEWAY_PORT}" + + if [ "$_scheme" = "https" ]; then + info "copying the gateway client certificate for ${TARGET_USER}..." + copy_snap_client_bundle + else + warn "this OpenShell snap revision serves plaintext HTTP without client authentication; any local user can operate the gateway" + fi if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell 2>&1)"; then [ -z "$_add_output" ] || print_gateway_add_output "$_add_output" @@ -1304,15 +1330,26 @@ register_snap_gateway() { esac } +# Wait for the snap gateway and record its scheme in SNAP_GATEWAY_SCHEME. +# Current revisions serve mTLS; earlier revisions serve plaintext HTTP. Probe +# HTTPS first because a TLS gateway also answers plaintext loopback requests +# for sandbox service routing. wait_for_snap_gateway_listener() { _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}" _elapsed=0 _last_output="" - _probe_url="http://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + _probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/" info "waiting for local gateway listener to become reachable..." while [ "$_elapsed" -lt "$_timeout" ]; do - if _last_output="$(curl -sS --max-time 2 -o /dev/null "$_probe_url" 2>&1)"; then + # The probe only checks reachability; the CLI verifies the gateway CA. + if _last_output="$(curl -sS -k --max-time 2 -o /dev/null "$_probe_url" 2>&1)"; then + SNAP_GATEWAY_SCHEME=https + info "local gateway listener is reachable" + return 0 + fi + if curl -sS --max-time 2 -o /dev/null "http://127.0.0.1:${LOCAL_GATEWAY_PORT}/" >/dev/null 2>&1; then + SNAP_GATEWAY_SCHEME=http info "local gateway listener is reachable" return 0 fi @@ -1354,9 +1391,9 @@ Install Docker Engine from a system package or Docker's package repository, then as_root snap restart openshell.gateway info "installed OpenShell snap from ${_channel}" + wait_for_snap_gateway_listener info "registering local gateway as ${TARGET_USER}..." register_snap_gateway - wait_for_snap_gateway_listener OPENSHELL_REGISTER_BIN="/snap/bin/openshell" wait_for_local_gateway_status } diff --git a/snap/hooks/install b/snap/hooks/install index c5fb3baf87..08bf917d95 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -2,10 +2,32 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# Bootstrap the snap gateway config. Operator-owned configs are never touched, +# except that the exact config written by earlier snap revisions, which allowed +# unauthenticated local access, is replaced with the mTLS default. The +# post-refresh hook runs this script too so existing installs are migrated. + set -eu config_file="${SNAP_COMMON}/gateway.toml" -if [ -e "$config_file" ] || [ -L "$config_file" ]; then + +legacy_config='[openshell] +version = 2 + +[openshell.gateway] + +[openshell.gateway.auth] +allow_unauthenticated_users = true' + +replace=false +if [ -L "$config_file" ]; then + exit 0 +elif [ -f "$config_file" ]; then + if [ "$(cat "$config_file")" != "$legacy_config" ]; then + exit 0 + fi + replace=true +elif [ -e "$config_file" ]; then exit 0 fi @@ -14,15 +36,21 @@ umask 077 temporary_file=$(mktemp "${config_file}.tmp.XXXXXX") trap 'rm -f "$temporary_file"' 0 HUP INT TERM -cat >"$temporary_file" <<'EOF' +cat >"$temporary_file" <<'CONFIG' [openshell] version = 2 [openshell.gateway] -[openshell.gateway.auth] -allow_unauthenticated_users = true -EOF +[openshell.gateway.mtls_auth] +enabled = true +CONFIG + +if [ "$replace" = true ]; then + mv -f "$temporary_file" "$config_file" + trap - 0 HUP INT TERM + exit 0 +fi # A hard link publishes the config atomically without replacing a path created # concurrently. SNAP_COMMON and the temporary file are on the same filesystem. diff --git a/snap/hooks/post-refresh b/snap/hooks/post-refresh new file mode 100755 index 0000000000..8d00e95f8a --- /dev/null +++ b/snap/hooks/post-refresh @@ -0,0 +1,6 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Migrate the legacy unauthenticated default config to mTLS on refresh. +exec "${SNAP}/meta/hooks/install" diff --git a/snapcraft.yaml b/snapcraft.yaml index 4c602620cf..382521bee3 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -29,11 +29,18 @@ description: | sudo systemctl reset-failed snap.openshell.gateway.service sudo snap restart openshell.gateway - 2. Verify the gateway and register it locally: + 2. Give your user the gateway client certificate and register the gateway. + The gateway requires mTLS; only users holding this certificate can use + it, so copy it only for trusted users: snap services openshell.gateway + d=~/snap/openshell/common/.local/state/openshell/tls + mkdir -p -m 700 "$d/client" + sudo cat /var/snap/openshell/common/tls/ca.crt > "$d/ca.crt" + sudo cat /var/snap/openshell/common/tls/client/tls.crt > "$d/client/tls.crt" + (umask 077; sudo cat /var/snap/openshell/common/tls/client/tls.key > "$d/client/tls.key") + openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status - openshell gateway add http://127.0.0.1:17670 --local --name openshell-gateway After a snap refresh, restart the gateway to pick up the new revision: @@ -92,13 +99,13 @@ apps: # during snap refreshes, which would kill active sandbox sessions. # Operators must manually restart the service after a refresh if needed. refresh-mode: endure - # Snapd runs this daemon as root, so ordinary CLI users cannot read the - # client certificate generated in root-owned snap state. The wrapper sets - # OPENSHELL_DISABLE_TLS=true for the loopback-only listener and uses - # $SNAP_COMMON/gateway.db. Before startup it bootstraps package-managed - # credentials and validates the selected operator-provided config without - # creating or rewriting it. A nonempty OPENSHELL_GATEWAY_CONFIG takes - # precedence over gateway.toml. + # Snapd runs this daemon as root. The wrapper serves TLS from the bundle + # generated in $SNAP_COMMON/tls, and the default config requires client + # certificates; the installer copies the client bundle to the target + # user. The wrapper uses $SNAP_COMMON/gateway.db. Before startup it + # bootstraps package-managed credentials and validates the selected + # operator-provided config without creating or rewriting it. A nonempty + # OPENSHELL_GATEWAY_CONFIG takes precedence over gateway.toml. environment: XDG_DATA_HOME: "$SNAP_COMMON" XDG_RUNTIME_DIR: "$SNAP_COMMON" diff --git a/tasks/scripts/snap-gateway-wrapper.sh b/tasks/scripts/snap-gateway-wrapper.sh index 4a98962559..a047cf72e3 100755 --- a/tasks/scripts/snap-gateway-wrapper.sh +++ b/tasks/scripts/snap-gateway-wrapper.sh @@ -3,16 +3,16 @@ # SPDX-License-Identifier: Apache-2.0 # Snap wrapper for openshell-gateway. Sets snap-specific defaults: -# - OPENSHELL_DB_URL -> sqlite:$SNAP_COMMON/gateway.db (overridable) -# - OPENSHELL_DISABLE_TLS -> true -# It bootstraps package-managed credentials and validates, but never creates or -# rewrites, an operator-provided config before starting the gateway. +# - OPENSHELL_DB_URL -> sqlite:$SNAP_COMMON/gateway.db (overridable) +# - OPENSHELL_LOCAL_TLS_DIR -> $SNAP_COMMON/tls (overridable) +# The gateway serves TLS from the generated bundle and requires client +# certificates. It bootstraps package-managed credentials and validates, but +# never creates or rewrites, an operator-provided config before starting. set -eu CANONICAL_CONFIG_FILE="${SNAP_COMMON}/gateway.toml" export OPENSHELL_DB_URL="${OPENSHELL_DB_URL:-sqlite:${SNAP_COMMON}/gateway.db?mode=rwc}" -export OPENSHELL_DISABLE_TLS="${OPENSHELL_DISABLE_TLS:-true}" export OPENSHELL_LOCAL_TLS_DIR="${OPENSHELL_LOCAL_TLS_DIR:-${SNAP_COMMON}/tls}" # Mirror clap's CLI-over-environment precedence so preflight always inspects @@ -64,9 +64,9 @@ if [ "$expect_config_path" = true ] || { [ "$config_seen" = true ] && [ -z "$cli exit 2 fi -# Docker sandboxes require gateway-minted, launch-scoped credentials for the -# supervisor. Generate the local JWT bundle alongside the otherwise-unused TLS -# material; generate-certs is idempotent and preserves an existing bundle. +# Generate the local TLS bundle and the JWT bundle used for launch-scoped +# supervisor credentials; generate-certs is idempotent and preserves an +# existing bundle. "${SNAP}/bin/openshell-gateway" generate-certs \ --output-dir "$OPENSHELL_LOCAL_TLS_DIR" \ --server-san host.openshell.internal diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 8cc6102148..630a49a927 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -295,8 +295,8 @@ assert_snap_install_flow \ root:snap install openshell --channel=latest/stable ensure:gateway-config root:snap restart openshell.gateway -register:gateway wait:gateway-listener +register:gateway wait:gateway-status" assert_snap_install_flow \ @@ -306,8 +306,8 @@ assert_snap_install_flow \ root:snap refresh openshell --channel=latest/stable ensure:gateway-config root:snap restart openshell.gateway -register:gateway wait:gateway-listener +register:gateway wait:gateway-status" assert_snap_install_rejected() { @@ -450,19 +450,73 @@ registration_calls_file="${tmpdir}/registration-calls" : >"$registration_calls_file" if ! ( as_target_user() { printf 'target:%s\n' "$*" >>"$registration_calls_file"; } + copy_snap_client_bundle() { printf 'copy:client-bundle\n' >>"$registration_calls_file"; } print_gateway_add_output() { :; } - register_snap_gateway + info() { :; } + TARGET_USER=test-user + SNAP_GATEWAY_SCHEME=https register_snap_gateway ) >"$out" 2>"$err"; then echo "FAIL: Snap gateway registration should succeed" >&2 cat "$err" >&2 || true exit 1 fi registration_calls="$(cat "$registration_calls_file")" +if [ "$registration_calls" != "copy:client-bundle +target:/snap/bin/openshell gateway add https://127.0.0.1:17670 --local --name openshell" ]; then + echo "FAIL: mTLS Snap gateway registration must copy the client bundle and use HTTPS" >&2 + printf '%s\n' "$registration_calls" >&2 + exit 1 +fi + +: >"$registration_calls_file" +if ! ( + as_target_user() { printf 'target:%s\n' "$*" >>"$registration_calls_file"; } + copy_snap_client_bundle() { printf 'copy:client-bundle\n' >>"$registration_calls_file"; } + print_gateway_add_output() { :; } + SNAP_GATEWAY_SCHEME=http register_snap_gateway +) >"$out" 2>"$err"; then + echo "FAIL: legacy plaintext Snap gateway registration should succeed" >&2 + cat "$err" >&2 || true + exit 1 +fi +registration_calls="$(cat "$registration_calls_file")" if [ "$registration_calls" != "target:/snap/bin/openshell gateway add http://127.0.0.1:17670 --local --name openshell" ]; then - echo "FAIL: Snap gateway registration must use the Snap CLI as the target user" >&2 + echo "FAIL: legacy Snap gateway registration must use HTTP without copying certificates" >&2 printf '%s\n' "$registration_calls" >&2 exit 1 fi +if ! grep -Fq "without client authentication" "$err"; then + echo "FAIL: legacy Snap gateway registration must warn about unauthenticated access" >&2 + exit 1 +fi + +snap_tls_src="${tmpdir}/snap-tls" +mkdir -p "${snap_tls_src}/client" +printf 'ca\n' >"${snap_tls_src}/ca.crt" +printf 'cert\n' >"${snap_tls_src}/client/tls.crt" +printf 'key\n' >"${snap_tls_src}/client/tls.key" +snap_user_home="${tmpdir}/snap-user-home" +( + as_root() { "$@"; } + as_target_user() { "$@"; } + TARGET_HOME="$snap_user_home" + OPENSHELL_SNAP_TLS_DIR="$snap_tls_src" copy_snap_client_bundle +) +snap_user_tls="${snap_user_home}/snap/openshell/common/.local/state/openshell/tls" +for file in ca.crt client/tls.crt client/tls.key; do + if ! cmp -s "${snap_tls_src}/${file}" "${snap_user_tls}/${file}"; then + echo "FAIL: Snap client bundle copy missing ${file}" >&2 + exit 1 + fi + if [[ -z $(find "${snap_user_tls}/${file}" -perm 600) ]]; then + echo "FAIL: Snap client bundle ${file} must be mode 0600" >&2 + exit 1 + fi +done +if [[ -z $(find "$snap_user_tls" -maxdepth 0 -perm 700) ]]; then + echo "FAIL: Snap client bundle directory must be mode 0700" >&2 + exit 1 +fi if [ "$(PLATFORM=darwin local_gateway_endpoint)" != "https://localhost:17670" ]; then echo "FAIL: macOS local gateway endpoint must use a TLS-compatible loopback hostname" >&2 diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index 345a0e3964..5127238cef 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -123,7 +123,12 @@ if [[ ! -x "$snap_install_hook" ]]; then echo "FAIL: Snap install hook must be executable" >&2 exit 1 fi -assert_contains "$snap_install_hook" 'allow_unauthenticated_users = true' +assert_contains "$snap_install_hook" '[openshell.gateway.mtls_auth]' +if [[ ! -x "$(dirname "$snap_install_hook")/post-refresh" ]]; then + echo "FAIL: Snap post-refresh hook must be executable" >&2 + exit 1 +fi +assert_not_contains "$ROOT/tasks/scripts/snap-gateway-wrapper.sh" 'OPENSHELL_DISABLE_TLS' bash "$ROOT/tasks/scripts/test-snap-install-hook.sh" "$snap_install_hook" assert_not_contains "$snap_install_docs" "snap connect openshell:home" assert_not_contains "$snap_install_docs" "snap connect openshell:network" diff --git a/tasks/scripts/test-snap-gateway-wrapper.sh b/tasks/scripts/test-snap-gateway-wrapper.sh index a14076b121..691a1e7628 100755 --- a/tasks/scripts/test-snap-gateway-wrapper.sh +++ b/tasks/scripts/test-snap-gateway-wrapper.sh @@ -82,9 +82,9 @@ cp "$override" "$work/override-before" : >"$log" run_wrapper "$override" assert_log "config preflight -- --trace -env:$override|sqlite:$common/gateway.db?mode=rwc|true +env:$override|sqlite:$common/gateway.db?mode=rwc| --trace -env:$override|sqlite:$common/gateway.db?mode=rwc|true" +env:$override|sqlite:$common/gateway.db?mode=rwc|" cmp -s "$work/override-before" "$override" cli_config="$work/cli.toml" @@ -98,9 +98,9 @@ env \ FAKE_GATEWAY_LOG="$log" \ "$wrapper" --trace --config "$cli_config" assert_log "config preflight -- --trace --config $cli_config -env:$override|sqlite:$common/gateway.db?mode=rwc|true +env:$override|sqlite:$common/gateway.db?mode=rwc| --trace --config $cli_config -env:$override|sqlite:$common/gateway.db?mode=rwc|true" +env:$override|sqlite:$common/gateway.db?mode=rwc|" cmp -s "$work/cli-before" "$cli_config" : >"$log" @@ -115,7 +115,7 @@ if env \ exit 1 fi assert_log "config preflight -- --config=$cli_config -env:$override|sqlite:$common/gateway.db?mode=rwc|true" +env:$override|sqlite:$common/gateway.db?mode=rwc|" cmp -s "$work/cli-before" "$cli_config" : >"$log" @@ -130,7 +130,7 @@ if env \ exit 1 fi assert_log "config preflight -- --grpc-rate-limit-requests 10 -env:$override|sqlite:$common/gateway.db?mode=rwc|true" +env:$override|sqlite:$common/gateway.db?mode=rwc|" for invalid_selector in terminator nested-config; do : >"$log" @@ -162,9 +162,9 @@ env \ FAKE_GATEWAY_LOG="$log" \ "$wrapper" --config=--dash-leading assert_log "config preflight -- --config=--dash-leading -env:$override|sqlite:$common/gateway.db?mode=rwc|true +env:$override|sqlite:$common/gateway.db?mode=rwc| --config=--dash-leading -env:$override|sqlite:$common/gateway.db?mode=rwc|true" +env:$override|sqlite:$common/gateway.db?mode=rwc|" canonical="$common/gateway.toml" printf 'valid schema-v2\n' >"$canonical" @@ -172,18 +172,18 @@ cp "$canonical" "$work/canonical-before" : >"$log" run_wrapper unset assert_log "config preflight -- --config $canonical --trace -env:|sqlite:$common/gateway.db?mode=rwc|true +env:|sqlite:$common/gateway.db?mode=rwc| --config $canonical --trace -env:|sqlite:$common/gateway.db?mode=rwc|true" +env:|sqlite:$common/gateway.db?mode=rwc|" cmp -s "$work/canonical-before" "$canonical" rm "$canonical" : >"$log" run_wrapper unset assert_log "config preflight -- --trace -env:|sqlite:$common/gateway.db?mode=rwc|true +env:|sqlite:$common/gateway.db?mode=rwc| --trace -env:|sqlite:$common/gateway.db?mode=rwc|true" +env:|sqlite:$common/gateway.db?mode=rwc|" assert_preflight_failure() { local name=$1 @@ -193,7 +193,7 @@ assert_preflight_failure() { exit 1 fi assert_log "config preflight -- --config $canonical --trace -env:|sqlite:$common/gateway.db?mode=rwc|true" +env:|sqlite:$common/gateway.db?mode=rwc|" } printf 'legacy version = 1\n' >"$canonical" diff --git a/tasks/scripts/test-snap-install-hook.sh b/tasks/scripts/test-snap-install-hook.sh index aacea97bbb..45178cc137 100755 --- a/tasks/scripts/test-snap-install-hook.sh +++ b/tasks/scripts/test-snap-install-hook.sh @@ -17,6 +17,17 @@ version = 2 [openshell.gateway] +[openshell.gateway.mtls_auth] +enabled = true +EOF + +legacy="${work}/legacy.toml" +cat >"$legacy" <<'EOF' +[openshell] +version = 2 + +[openshell.gateway] + [openshell.gateway.auth] allow_unauthenticated_users = true EOF @@ -34,6 +45,38 @@ cp "$common/gateway.toml" "${work}/operator-before" SNAP_COMMON="$common" "$hook" cmp -s "${work}/operator-before" "$common/gateway.toml" +common="${work}/legacy" +mkdir -p "$common" +cp "$legacy" "$common/gateway.toml" +chmod 644 "$common/gateway.toml" +SNAP_COMMON="$common" "$hook" +if ! cmp -s "$expected" "$common/gateway.toml"; then + echo "FAIL: install hook must migrate the legacy unauthenticated config" >&2 + exit 1 +fi +if [[ -z $(find "$common/gateway.toml" -perm 600) ]]; then + echo "FAIL: migrated config must be mode 0600" >&2 + exit 1 +fi + +common="${work}/legacy-edited" +mkdir -p "$common" +cp "$legacy" "$common/gateway.toml" +printf '\n# operator note\n' >>"$common/gateway.toml" +cp "$common/gateway.toml" "${work}/legacy-edited-before" +SNAP_COMMON="$common" "$hook" +cmp -s "${work}/legacy-edited-before" "$common/gateway.toml" + +common="${work}/post-refresh" +mkdir -p "$common" "${work}/snap/meta/hooks" +cp "$hook" "${work}/snap/meta/hooks/install" +cp "$legacy" "$common/gateway.toml" +SNAP="${work}/snap" SNAP_COMMON="$common" "${hook_dir}/post-refresh" +if ! cmp -s "$expected" "$common/gateway.toml"; then + echo "FAIL: post-refresh hook must migrate the legacy unauthenticated config" >&2 + exit 1 +fi + common="${work}/broken-link" mkdir -p "$common" ln -s "${work}/missing-target" "$common/gateway.toml" From 62dd5ed13cba31718be8d25cd0132947c747b6d8 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 16:48:16 -0700 Subject: [PATCH 02/11] fix(snap): pass config preflight and detect the mTLS gateway reliably An explicit [openshell.gateway.mtls_auth] table fails config preflight, which validates mTLS auth before the local TLS bundle supplies the client CA. Write a default that pins the Docker driver instead; with the wrapper's TLS bundle the gateway requires client certificates and enables mTLS user auth automatically, as the native packages do. The mTLS gateway rejects TLS handshakes without a client certificate, and it still answers plaintext loopback HTTP for sandbox service routing, so the installer could misdetect it as a legacy plaintext gateway. Probe HTTPS with the root-owned client bundle, and treat a gateway as legacy only when a plaintext gRPC Health call succeeds. Signed-off-by: Drew Newberry --- install.sh | 28 +++++++++++++++----- snap/hooks/install | 13 ++++++---- tasks/scripts/test-install-sh.sh | 34 +++++++++++++++++++++++++ tasks/scripts/test-packaging-assets.sh | 2 +- tasks/scripts/test-snap-install-hook.sh | 4 +-- 5 files changed, 66 insertions(+), 15 deletions(-) diff --git a/install.sh b/install.sh index 7ccfd86c2c..2267869a64 100755 --- a/install.sh +++ b/install.sh @@ -1331,24 +1331,30 @@ register_snap_gateway() { } # Wait for the snap gateway and record its scheme in SNAP_GATEWAY_SCHEME. -# Current revisions serve mTLS; earlier revisions serve plaintext HTTP. Probe -# HTTPS first because a TLS gateway also answers plaintext loopback requests -# for sandbox service routing. +# Current revisions require a client certificate during the TLS handshake, so +# probe HTTPS with the root-owned client bundle. Earlier revisions serve +# plaintext gRPC; a TLS gateway also answers plaintext loopback HTTP for +# sandbox service routing, so only a successful plaintext gRPC Health call +# identifies a legacy gateway. wait_for_snap_gateway_listener() { _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}" _elapsed=0 _last_output="" + _tls_dir="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}" _probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/" info "waiting for local gateway listener to become reachable..." while [ "$_elapsed" -lt "$_timeout" ]; do - # The probe only checks reachability; the CLI verifies the gateway CA. - if _last_output="$(curl -sS -k --max-time 2 -o /dev/null "$_probe_url" 2>&1)"; then + if _last_output="$(as_root curl -sS --max-time 2 \ + --cacert "${_tls_dir}/ca.crt" \ + --cert "${_tls_dir}/client/tls.crt" \ + --key "${_tls_dir}/client/tls.key" \ + -o /dev/null "$_probe_url" 2>&1)"; then SNAP_GATEWAY_SCHEME=https info "local gateway listener is reachable" return 0 fi - if curl -sS --max-time 2 -o /dev/null "http://127.0.0.1:${LOCAL_GATEWAY_PORT}/" >/dev/null 2>&1; then + if [ "$(snap_legacy_grpc_health_status)" = "200" ]; then SNAP_GATEWAY_SCHEME=http info "local gateway listener is reachable" return 0 @@ -1362,6 +1368,16 @@ wait_for_snap_gateway_listener() { error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s" } +# Print the HTTP status of an empty plaintext gRPC Health call. +snap_legacy_grpc_health_status() { + printf '\000\000\000\000\000' | + curl -s --max-time 2 --http2-prior-knowledge -o /dev/null -w '%{http_code}' \ + -X POST -H 'content-type: application/grpc' -H 'te: trailers' \ + --data-binary @- \ + "http://127.0.0.1:${LOCAL_GATEWAY_PORT}/openshell.v1.OpenShell/Health" 2>/dev/null || + true +} + install_linux_snap() { require_cmd snap set_linux_target_runtime_dir diff --git a/snap/hooks/install b/snap/hooks/install index 08bf917d95..26e811848b 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -4,8 +4,13 @@ # Bootstrap the snap gateway config. Operator-owned configs are never touched, # except that the exact config written by earlier snap revisions, which allowed -# unauthenticated local access, is replaced with the mTLS default. The -# post-refresh hook runs this script too so existing installs are migrated. +# unauthenticated local access, is replaced with the default. The post-refresh +# hook runs this script too so existing installs are migrated. +# +# The default pins the Docker driver and leaves user auth unset: with the +# wrapper's local TLS bundle, the gateway requires client certificates and +# enables mTLS user auth automatically. Setting mtls_auth explicitly would fail +# config preflight, which runs before the local TLS defaults are applied. set -eu @@ -41,9 +46,7 @@ cat >"$temporary_file" <<'CONFIG' version = 2 [openshell.gateway] - -[openshell.gateway.mtls_auth] -enabled = true +compute_driver = "docker" CONFIG if [ "$replace" = true ]; then diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 630a49a927..2eb7f934bc 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -490,6 +490,40 @@ if ! grep -Fq "without client authentication" "$err"; then exit 1 fi +assert_snap_listener_scheme() { + local name=$1 + local https_ok=$2 + local grpc_status=$3 + local expected=$4 + local actual + + actual="$( + as_root() { "$@"; } + curl() { + case " $* " in + *" --cert "*) [ "$https_ok" = "1" ] ;; + *) return 1 ;; + esac + } + snap_legacy_grpc_health_status() { printf '%s' "$grpc_status"; } + sleep() { :; } + info() { :; } + dump_local_gateway_diagnostics() { :; } + error() { printf 'timeout\n'; exit 0; } + OPENSHELL_INSTALL_GATEWAY_TIMEOUT=2 + wait_for_snap_gateway_listener 2>/dev/null + printf '%s\n' "${SNAP_GATEWAY_SCHEME:-none}" + )" + if [ "$actual" != "$expected" ]; then + echo "FAIL: ${name}: expected ${expected}, got ${actual}" >&2 + exit 1 + fi +} + +assert_snap_listener_scheme "mTLS gateway accepts the client bundle" 1 404 https +assert_snap_listener_scheme "legacy gateway answers plaintext gRPC" 0 200 http +assert_snap_listener_scheme "plaintext service routing is not a legacy gateway" 0 404 timeout + snap_tls_src="${tmpdir}/snap-tls" mkdir -p "${snap_tls_src}/client" printf 'ca\n' >"${snap_tls_src}/ca.crt" diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index 5127238cef..a06bf5be3c 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -123,7 +123,7 @@ if [[ ! -x "$snap_install_hook" ]]; then echo "FAIL: Snap install hook must be executable" >&2 exit 1 fi -assert_contains "$snap_install_hook" '[openshell.gateway.mtls_auth]' +assert_contains "$snap_install_hook" 'compute_driver = "docker"' if [[ ! -x "$(dirname "$snap_install_hook")/post-refresh" ]]; then echo "FAIL: Snap post-refresh hook must be executable" >&2 exit 1 diff --git a/tasks/scripts/test-snap-install-hook.sh b/tasks/scripts/test-snap-install-hook.sh index 45178cc137..6b20b644a7 100755 --- a/tasks/scripts/test-snap-install-hook.sh +++ b/tasks/scripts/test-snap-install-hook.sh @@ -16,9 +16,7 @@ cat >"$expected" <<'EOF' version = 2 [openshell.gateway] - -[openshell.gateway.mtls_auth] -enabled = true +compute_driver = "docker" EOF legacy="${work}/legacy.toml" From ed9ca18cf1aab74a357e1b971b094675644b747c Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 16:59:26 -0700 Subject: [PATCH 03/11] chore(snap): simplify install hook comment Signed-off-by: Drew Newberry --- snap/hooks/install | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/snap/hooks/install b/snap/hooks/install index 26e811848b..bcc3c6a1ed 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -2,15 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Bootstrap the snap gateway config. Operator-owned configs are never touched, -# except that the exact config written by earlier snap revisions, which allowed -# unauthenticated local access, is replaced with the default. The post-refresh -# hook runs this script too so existing installs are migrated. -# -# The default pins the Docker driver and leaves user auth unset: with the -# wrapper's local TLS bundle, the gateway requires client certificates and -# enables mTLS user auth automatically. Setting mtls_auth explicitly would fail -# config preflight, which runs before the local TLS defaults are applied. +# Ensure the gateway is secured by default with mTLS. set -eu From 49ce59e1b217fa515a1b5e7adaf851edb4d10743 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 17:23:12 -0700 Subject: [PATCH 04/11] fix(snap): migrate insecure gateway configs on refresh Signed-off-by: Drew Newberry --- architecture/build.md | 17 ++++--- docs/about/installation.mdx | 26 +++++++---- install.sh | 1 + python/openshell/release_formula_test.py | 6 ++- skills/debug-openshell-cluster/SKILL.md | 1 + snap/hooks/install | 26 +++++++---- snapcraft.yaml | 23 ++++++---- tasks/scripts/test-install-sh.sh | 3 ++ tasks/scripts/test-packaging-assets.sh | 1 + tasks/scripts/test-snap-install-hook.sh | 58 ++++++++++++++++++++++-- 10 files changed, 124 insertions(+), 38 deletions(-) diff --git a/architecture/build.md b/architecture/build.md index 43161b8c0c..511a33dd50 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -336,12 +336,17 @@ artifact and verifies the release run instead of listing every repository artifact. Snapd runs the gateway as a root-owned system service. Its generated client -certificates reside in root-owned snap state and are unavailable to ordinary CLI -users, so the Snap uses plaintext loopback transport and enables unauthenticated -local users by default. Debian and RPM packages instead run systemd user services -and use user-owned mTLS material. Bootstrap creates the default configuration -only when it is missing. Sandbox-to-gateway sessions remain authenticated with -gateway-minted JWTs. +certificates reside in root-owned snap state. The installer copies the client +bundle into the target user's private Snap state and registers the TLS endpoint; +direct Snap installs require the same enrollment. The install and post-refresh +hooks replace configs that explicitly enable plaintext or unauthenticated access +with the secure Docker default, preserving one private backup. Snap refreshes +restart the gateway so the migrated config takes effect immediately. + +Debian and RPM packages instead run systemd user services with user-owned mTLS +material. Sandbox-to-gateway sessions remain authenticated with gateway-minted +JWTs. + The Debian qualification profile keeps candidate-image overrides outside the operator-owned gateway configuration: it writes a harness-owned file under `/var/lib/openshell-qualification` and selects it through the packaged systemd diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 350164c2a7..d6f060d3c1 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -112,18 +112,28 @@ sudo snap install openshell The snap does not migrate existing Debian, RPM, or Homebrew installs. Remove any existing installation first, then rerun the script with `OPENSHELL_ACK_BREAKING_UPGRADE=1`. -The gateway runs as a system service at `http://127.0.0.1:17670` and reads `/var/snap/openshell/common/gateway.toml`. - - -The snap gateway allows unauthenticated access from the local host. Any local user or process can operate it. Do not expose it beyond the local host. - - -Snap refreshes do not restart the gateway, so active sandboxes keep running. Restart it to pick up a new version: +The gateway runs as a system service at `https://127.0.0.1:17670` and reads `/var/snap/openshell/common/gateway.toml`. It requires a client certificate. The install script copies that certificate to the installing user's Snap state and registers the gateway automatically. If you installed with `sudo snap install openshell`, give each trusted user the certificate and register the gateway from that user's account: ```shell -sudo systemctl restart snap.openshell.gateway +d=~/snap/openshell/common/.local/state/openshell/tls +mkdir -p -m 700 "$d/client" +sudo cat /var/snap/openshell/common/tls/ca.crt > "$d/ca.crt" +sudo cat /var/snap/openshell/common/tls/client/tls.crt > "$d/client/tls.crt" +(umask 077; sudo cat /var/snap/openshell/common/tls/client/tls.key > "$d/client/tls.key") +chmod 700 "$d" "$d/client" +chmod 600 "$d/ca.crt" "$d/client/tls.crt" "$d/client/tls.key" +openshell gateway add https://127.0.0.1:17670 --local --name openshell +openshell status ``` +Keep the client key private. Anyone who can read it can authenticate to the local gateway. + +On refresh, the Snap replaces any regular gateway config that explicitly enables unauthenticated access or disables TLS with the secure Docker default. Other settings in that file are not carried over. It saves the previous config at `/var/snap/openshell/common/gateway.toml.pre-mtls` with mode `0600`, or at a uniquely suffixed path if that backup already exists. The refresh log prints the actual path. Review the backup before restoring other settings; restoring its insecure settings reopens access. + +Snap refreshes restart the gateway to apply the migrated config immediately. This interrupts active sandbox sessions. + +If you previously registered the plaintext endpoint, run the certificate and HTTPS registration steps above after the refresh. Remove the old registration first with `openshell gateway remove openshell` if the name already exists. You can also rerun the install script to refresh the registration automatically. + To install a locally built snap, connect its interfaces manually: ```shell diff --git a/install.sh b/install.sh index 2267869a64..cdbba19111 100755 --- a/install.sh +++ b/install.sh @@ -1295,6 +1295,7 @@ copy_snap_client_bundle() { for _file in ca.crt client/tls.crt client/tls.key; do as_root cat "${_src}/${_file}" | as_target_user sh -c 'umask 077; cat >"$1"' sh "${_dst}/${_file}" + as_target_user chmod 600 "${_dst}/${_file}" done } diff --git a/python/openshell/release_formula_test.py b/python/openshell/release_formula_test.py index 61fff150ac..4db21b0951 100644 --- a/python/openshell/release_formula_test.py +++ b/python/openshell/release_formula_test.py @@ -155,7 +155,11 @@ def test_snap_wrapper_uses_optional_gateway_config_without_generating_toml() -> 'export OPENSHELL_DB_URL="${OPENSHELL_DB_URL:-sqlite:${SNAP_COMMON}/gateway.db?mode=rwc}"' in wrapper ) - assert 'export OPENSHELL_DISABLE_TLS="${OPENSHELL_DISABLE_TLS:-true}"' in wrapper + assert "OPENSHELL_DISABLE_TLS" not in wrapper + assert ( + 'export OPENSHELL_LOCAL_TLS_DIR="${OPENSHELL_LOCAL_TLS_DIR:-${SNAP_COMMON}/tls}"' + in wrapper + ) assert ( 'exec "${SNAP}/bin/openshell-gateway" --config "$CANONICAL_CONFIG_FILE" "$@"' in wrapper diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 8e865f13ec..c73ef2547b 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -72,6 +72,7 @@ Common findings: - `No active gateway`: register one with `openshell gateway add `. - Connection refused: gateway process is not running, service exposure is wrong, or a port-forward/proxy is not active. - TLS/certificate errors: the endpoint scheme or trust chain is wrong, a local mTLS bundle does not match the gateway CA, or TLS termination does not match the gateway listener. +- A Snap refresh restarts the gateway with its migrated mTLS config. The secure Snap gateway uses `https://127.0.0.1:17670` and requires a client bundle in the user's Snap state. Check `/var/snap/openshell/common/gateway.toml.pre-mtls` for settings replaced during migration, then follow the published Snap installation steps to re-register an old HTTP client. - `Unauthenticated` from an edge or OIDC gateway: refresh stored credentials with `openshell gateway login [name]`, then retry. Use `gateway logout` only when intentionally clearing local credentials. - A direct development endpoint with a private or self-signed certificate can be isolated with `--gateway-endpoint --gateway-insecure`; do not persist or recommend insecure verification for shared gateways. diff --git a/snap/hooks/install b/snap/hooks/install index bcc3c6a1ed..f78c88bf45 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -2,25 +2,20 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Ensure the gateway is secured by default with mTLS. +# Create the mTLS default and replace insecure configs on refresh. set -eu config_file="${SNAP_COMMON}/gateway.toml" -legacy_config='[openshell] -version = 2 - -[openshell.gateway] - -[openshell.gateway.auth] -allow_unauthenticated_users = true' - replace=false if [ -L "$config_file" ]; then exit 0 elif [ -f "$config_file" ]; then - if [ "$(cat "$config_file")" != "$legacy_config" ]; then + # Refresh every insecure default or edited config. Keep secure operator + # configs intact, but do not preserve an explicit plaintext or anonymous + # access setting merely because the file has other edits. + if ! grep -Eq '^[[:space:]]*(allow_unauthenticated_users|disable_tls)[[:space:]]*=[[:space:]]*true([[:space:]#]|$)' "$config_file"; then exit 0 fi replace=true @@ -42,6 +37,17 @@ compute_driver = "docker" CONFIG if [ "$replace" = true ]; then + # Keep one private copy for operators who need to restore other settings. + backup_file="${config_file}.pre-mtls" + backup_tmp=$(mktemp "${backup_file}.XXXXXX") + cp "$config_file" "$backup_tmp" + chmod 600 "$backup_tmp" + if [ ! -e "$backup_file" ] && [ ! -L "$backup_file" ] && ln "$backup_tmp" "$backup_file"; then + rm -f "$backup_tmp" + else + backup_file="$backup_tmp" + fi + echo "openshell: replacing insecure gateway config; previous config saved at $backup_file" >&2 mv -f "$temporary_file" "$config_file" trap - 0 HUP INT TERM exit 0 diff --git a/snapcraft.yaml b/snapcraft.yaml index 382521bee3..9a75a1db36 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -39,15 +39,19 @@ description: | sudo cat /var/snap/openshell/common/tls/ca.crt > "$d/ca.crt" sudo cat /var/snap/openshell/common/tls/client/tls.crt > "$d/client/tls.crt" (umask 077; sudo cat /var/snap/openshell/common/tls/client/tls.key > "$d/client/tls.key") + chmod 700 "$d" "$d/client" + chmod 600 "$d/ca.crt" "$d/client/tls.crt" "$d/client/tls.key" openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status - After a snap refresh, restart the gateway to pick up the new revision: - - sudo snap restart openshell.gateway - - Restarting the gateway will interrupt active sandbox sessions. The gateway - is not restarted automatically to avoid disconnecting running sandboxes. + On refresh, configs that explicitly enable plaintext or unauthenticated + access are replaced with the secure default. The previous file is saved at + /var/snap/openshell/common/gateway.toml.pre-mtls (mode 0600), or a uniquely + suffixed path if that backup already exists. The gateway + restarts automatically during refresh to apply the new revision and + config. This interrupts active sandbox sessions. Users registered against + the old HTTP endpoint must remove that gateway registration and repeat + step 2, or rerun install.sh. base: core24 grade: stable @@ -95,10 +99,9 @@ apps: gateway: command: bin/openshell-gateway-wrapper daemon: simple - # refresh-mode: endure prevents snapd from restarting the gateway daemon - # during snap refreshes, which would kill active sandbox sessions. - # Operators must manually restart the service after a refresh if needed. - refresh-mode: endure + # Refresh must activate the migrated mTLS config immediately. This + # interrupts active sandbox sessions. + refresh-mode: restart # Snapd runs this daemon as root. The wrapper serves TLS from the bundle # generated in $SNAP_COMMON/tls, and the default config requires client # certificates; the installer copies the client bundle to the target diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 2eb7f934bc..89816a1473 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -530,6 +530,9 @@ printf 'ca\n' >"${snap_tls_src}/ca.crt" printf 'cert\n' >"${snap_tls_src}/client/tls.crt" printf 'key\n' >"${snap_tls_src}/client/tls.key" snap_user_home="${tmpdir}/snap-user-home" +mkdir -p "${snap_user_home}/snap/openshell/common/.local/state/openshell/tls/client" +printf 'old key\n' >"${snap_user_home}/snap/openshell/common/.local/state/openshell/tls/client/tls.key" +chmod 644 "${snap_user_home}/snap/openshell/common/.local/state/openshell/tls/client/tls.key" ( as_root() { "$@"; } as_target_user() { "$@"; } diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index a06bf5be3c..51ba8d0258 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -124,6 +124,7 @@ if [[ ! -x "$snap_install_hook" ]]; then exit 1 fi assert_contains "$snap_install_hook" 'compute_driver = "docker"' +assert_contains "$snapcraft" 'refresh-mode: restart' if [[ ! -x "$(dirname "$snap_install_hook")/post-refresh" ]]; then echo "FAIL: Snap post-refresh hook must be executable" >&2 exit 1 diff --git a/tasks/scripts/test-snap-install-hook.sh b/tasks/scripts/test-snap-install-hook.sh index 6b20b644a7..7d61adc073 100755 --- a/tasks/scripts/test-snap-install-hook.sh +++ b/tasks/scripts/test-snap-install-hook.sh @@ -63,17 +63,69 @@ cp "$legacy" "$common/gateway.toml" printf '\n# operator note\n' >>"$common/gateway.toml" cp "$common/gateway.toml" "${work}/legacy-edited-before" SNAP_COMMON="$common" "$hook" -cmp -s "${work}/legacy-edited-before" "$common/gateway.toml" +cmp -s "$expected" "$common/gateway.toml" +cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" +if [[ -z $(find "$common/gateway.toml.pre-mtls" -perm 600) ]]; then + echo "FAIL: migrated config backup must be mode 0600" >&2 + exit 1 +fi +SNAP_COMMON="$common" "$hook" +cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" +cp "$legacy" "$common/gateway.toml" +SNAP_COMMON="$common" "$hook" +cmp -s "$expected" "$common/gateway.toml" +cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" +if [[ $(find "$common" -maxdepth 1 -name 'gateway.toml.pre-mtls.*' -type f | wc -l) -ne 1 ]]; then + echo "FAIL: repeated migration must preserve the existing backup" >&2 + exit 1 +fi + +common="${work}/custom-insecure" +mkdir -p "$common" +cat >"$common/gateway.toml" <<'EOF' +[openshell] +version = 2 + +[openshell.gateway] +compute_driver = "docker" +disable_tls = true # old local override + +[openshell.gateway.auth] +allow_unauthenticated_users = true # old local override +EOF +cp "$common/gateway.toml" "${work}/custom-insecure-before" +SNAP_COMMON="$common" "$hook" +cmp -s "$expected" "$common/gateway.toml" +cmp -s "${work}/custom-insecure-before" "$common/gateway.toml.pre-mtls" + +common="${work}/custom-secure" +mkdir -p "$common" +cat >"$common/gateway.toml" <<'EOF' +[openshell] +version = 2 + +[openshell.gateway] +compute_driver = "docker" +# allow_unauthenticated_users = true +EOF +cp "$common/gateway.toml" "${work}/custom-secure-before" +SNAP_COMMON="$common" "$hook" +cmp -s "${work}/custom-secure-before" "$common/gateway.toml" +if [[ -e "$common/gateway.toml.pre-mtls" ]]; then + echo "FAIL: secure operator config should not be backed up or replaced" >&2 + exit 1 +fi common="${work}/post-refresh" mkdir -p "$common" "${work}/snap/meta/hooks" cp "$hook" "${work}/snap/meta/hooks/install" -cp "$legacy" "$common/gateway.toml" +cp "${work}/legacy-edited-before" "$common/gateway.toml" SNAP="${work}/snap" SNAP_COMMON="$common" "${hook_dir}/post-refresh" if ! cmp -s "$expected" "$common/gateway.toml"; then - echo "FAIL: post-refresh hook must migrate the legacy unauthenticated config" >&2 + echo "FAIL: post-refresh hook must migrate an edited insecure config" >&2 exit 1 fi +cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" common="${work}/broken-link" mkdir -p "$common" From 2c1a0379c22d0d1701e12b2f15b7e9eb7b4da2fc Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 17:31:07 -0700 Subject: [PATCH 05/11] refactor(snap): simplify mTLS detection and config migration Detect the mTLS snap from the installed revision's post-refresh hook instead of probing plaintext gRPC, and drop the scheme global. Remove the installer's pre-hook config fallback, which is dead now that every channel ships the install hook and which wrote the insecure default. Give the install hook a single write path with a simple backup name, and shorten the manual client certificate steps. Signed-off-by: Drew Newberry --- docs/about/installation.mdx | 10 ++-- install.sh | 95 ++++++++------------------------ snap/hooks/install | 47 ++++------------ snap/hooks/post-refresh | 2 +- snapcraft.yaml | 10 ++-- tasks/scripts/test-install-sh.sh | 71 ++++++------------------ 6 files changed, 60 insertions(+), 175 deletions(-) diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index d6f060d3c1..05160d2099 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -116,12 +116,10 @@ The gateway runs as a system service at `https://127.0.0.1:17670` and reads `/va ```shell d=~/snap/openshell/common/.local/state/openshell/tls -mkdir -p -m 700 "$d/client" -sudo cat /var/snap/openshell/common/tls/ca.crt > "$d/ca.crt" -sudo cat /var/snap/openshell/common/tls/client/tls.crt > "$d/client/tls.crt" -(umask 077; sudo cat /var/snap/openshell/common/tls/client/tls.key > "$d/client/tls.key") -chmod 700 "$d" "$d/client" -chmod 600 "$d/ca.crt" "$d/client/tls.crt" "$d/client/tls.key" +mkdir -p -m 700 "$d" "$d/client" +sudo install -o "$USER" -m 600 /var/snap/openshell/common/tls/ca.crt "$d/" +sudo install -o "$USER" -m 600 -t "$d/client" \ + /var/snap/openshell/common/tls/client/tls.crt /var/snap/openshell/common/tls/client/tls.key openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status ``` diff --git a/install.sh b/install.sh index cdbba19111..709cf750b2 100755 --- a/install.sh +++ b/install.sh @@ -1220,46 +1220,6 @@ openshell_snap_channel() { esac } -# Fallback for snap revisions that predate the install hook, which serve -# plaintext HTTP. Current revisions create an mTLS config in their install -# hook, and their post-refresh hook migrates this legacy default. -ensure_snap_gateway_config() { - _config_file="${1:-/var/snap/openshell/common/gateway.toml}" - - as_root sh -c ' - set -eu - config_file=$1 - if [ -e "$config_file" ] || [ -L "$config_file" ]; then - exit 0 - fi - - config_dir=${config_file%/*} - mkdir -p "$config_dir" - umask 077 - temporary_file=$(mktemp "${config_file}.tmp.XXXXXX") - trap '\''rm -f "$temporary_file"'\'' 0 HUP INT TERM - - cat >"$temporary_file" <<'\''EOF'\'' -[openshell] -version = 2 - -[openshell.gateway] - -[openshell.gateway.auth] -allow_unauthenticated_users = true -EOF - - if ! ln "$temporary_file" "$config_file"; then - if [ -e "$config_file" ] || [ -L "$config_file" ]; then - exit 0 - fi - exit 1 - fi - rm -f "$temporary_file" - trap - 0 HUP INT TERM - ' sh "$_config_file" -} - wait_for_docker_daemon() { _timeout="${OPENSHELL_INSTALL_DOCKER_TIMEOUT:-30}" _elapsed=0 @@ -1299,15 +1259,21 @@ copy_snap_client_bundle() { done } +# Snap revisions that require mTLS ship the post-refresh hook that migrates +# older plaintext configs. +snap_gateway_uses_mtls() { + [ -e "${OPENSHELL_SNAP_DIR:-/snap/openshell/current}/meta/hooks/post-refresh" ] +} + register_snap_gateway() { _register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}" - _scheme="${SNAP_GATEWAY_SCHEME:-https}" - _endpoint="${_scheme}://127.0.0.1:${LOCAL_GATEWAY_PORT}" - if [ "$_scheme" = "https" ]; then + if snap_gateway_uses_mtls; then + _endpoint="https://127.0.0.1:${LOCAL_GATEWAY_PORT}" info "copying the gateway client certificate for ${TARGET_USER}..." copy_snap_client_bundle else + _endpoint="http://127.0.0.1:${LOCAL_GATEWAY_PORT}" warn "this OpenShell snap revision serves plaintext HTTP without client authentication; any local user can operate the gateway" fi @@ -1331,32 +1297,28 @@ register_snap_gateway() { esac } -# Wait for the snap gateway and record its scheme in SNAP_GATEWAY_SCHEME. -# Current revisions require a client certificate during the TLS handshake, so -# probe HTTPS with the root-owned client bundle. Earlier revisions serve -# plaintext gRPC; a TLS gateway also answers plaintext loopback HTTP for -# sandbox service routing, so only a successful plaintext gRPC Health call -# identifies a legacy gateway. +# The mTLS gateway rejects TLS handshakes without a client certificate, so +# probe it with the root-owned client bundle. wait_for_snap_gateway_listener() { _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}" _elapsed=0 _last_output="" _tls_dir="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}" - _probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + + if snap_gateway_uses_mtls; then + _probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + _probe_as=as_root + set -- --cacert "${_tls_dir}/ca.crt" \ + --cert "${_tls_dir}/client/tls.crt" --key "${_tls_dir}/client/tls.key" + else + _probe_url="http://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + _probe_as="" + set -- + fi info "waiting for local gateway listener to become reachable..." while [ "$_elapsed" -lt "$_timeout" ]; do - if _last_output="$(as_root curl -sS --max-time 2 \ - --cacert "${_tls_dir}/ca.crt" \ - --cert "${_tls_dir}/client/tls.crt" \ - --key "${_tls_dir}/client/tls.key" \ - -o /dev/null "$_probe_url" 2>&1)"; then - SNAP_GATEWAY_SCHEME=https - info "local gateway listener is reachable" - return 0 - fi - if [ "$(snap_legacy_grpc_health_status)" = "200" ]; then - SNAP_GATEWAY_SCHEME=http + if _last_output="$($_probe_as curl -sS --max-time 2 "$@" -o /dev/null "$_probe_url" 2>&1)"; then info "local gateway listener is reachable" return 0 fi @@ -1369,16 +1331,6 @@ wait_for_snap_gateway_listener() { error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s" } -# Print the HTTP status of an empty plaintext gRPC Health call. -snap_legacy_grpc_health_status() { - printf '\000\000\000\000\000' | - curl -s --max-time 2 --http2-prior-knowledge -o /dev/null -w '%{http_code}' \ - -X POST -H 'content-type: application/grpc' -H 'te: trailers' \ - --data-binary @- \ - "http://127.0.0.1:${LOCAL_GATEWAY_PORT}/openshell.v1.OpenShell/Health" 2>/dev/null || - true -} - install_linux_snap() { require_cmd snap set_linux_target_runtime_dir @@ -1404,7 +1356,6 @@ Install Docker Engine from a system package or Docker's package repository, then as_root snap install openshell --channel="$_channel" fi - ensure_snap_gateway_config as_root snap restart openshell.gateway info "installed OpenShell snap from ${_channel}" diff --git a/snap/hooks/install b/snap/hooks/install index f78c88bf45..ea3dc734a5 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -7,18 +7,20 @@ set -eu config_file="${SNAP_COMMON}/gateway.toml" +insecure='^[[:space:]]*(allow_unauthenticated_users|disable_tls)[[:space:]]*=[[:space:]]*true([[:space:]#]|$)' -replace=false +# Keep secure operator configs, symlinks, and directories. Replace a config +# that explicitly allows plaintext or anonymous access, even if it has other +# edits, after saving a private backup. if [ -L "$config_file" ]; then exit 0 elif [ -f "$config_file" ]; then - # Refresh every insecure default or edited config. Keep secure operator - # configs intact, but do not preserve an explicit plaintext or anonymous - # access setting merely because the file has other edits. - if ! grep -Eq '^[[:space:]]*(allow_unauthenticated_users|disable_tls)[[:space:]]*=[[:space:]]*true([[:space:]#]|$)' "$config_file"; then - exit 0 - fi - replace=true + grep -Eq "$insecure" "$config_file" || exit 0 + backup_file="${config_file}.pre-mtls" + [ ! -e "$backup_file" ] || backup_file="${backup_file}.$(date +%s)" + umask 077 + cp "$config_file" "$backup_file" + echo "openshell: replacing insecure gateway config; previous config saved at $backup_file" >&2 elif [ -e "$config_file" ]; then exit 0 fi @@ -27,7 +29,6 @@ mkdir -p "$SNAP_COMMON" umask 077 temporary_file=$(mktemp "${config_file}.tmp.XXXXXX") trap 'rm -f "$temporary_file"' 0 HUP INT TERM - cat >"$temporary_file" <<'CONFIG' [openshell] version = 2 @@ -35,31 +36,5 @@ version = 2 [openshell.gateway] compute_driver = "docker" CONFIG - -if [ "$replace" = true ]; then - # Keep one private copy for operators who need to restore other settings. - backup_file="${config_file}.pre-mtls" - backup_tmp=$(mktemp "${backup_file}.XXXXXX") - cp "$config_file" "$backup_tmp" - chmod 600 "$backup_tmp" - if [ ! -e "$backup_file" ] && [ ! -L "$backup_file" ] && ln "$backup_tmp" "$backup_file"; then - rm -f "$backup_tmp" - else - backup_file="$backup_tmp" - fi - echo "openshell: replacing insecure gateway config; previous config saved at $backup_file" >&2 - mv -f "$temporary_file" "$config_file" - trap - 0 HUP INT TERM - exit 0 -fi - -# A hard link publishes the config atomically without replacing a path created -# concurrently. SNAP_COMMON and the temporary file are on the same filesystem. -if ! ln "$temporary_file" "$config_file"; then - if [ -e "$config_file" ] || [ -L "$config_file" ]; then - exit 0 - fi - exit 1 -fi -rm -f "$temporary_file" +mv -f "$temporary_file" "$config_file" trap - 0 HUP INT TERM diff --git a/snap/hooks/post-refresh b/snap/hooks/post-refresh index 8d00e95f8a..1e46280d6d 100755 --- a/snap/hooks/post-refresh +++ b/snap/hooks/post-refresh @@ -2,5 +2,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Migrate the legacy unauthenticated default config to mTLS on refresh. +# Replace insecure gateway configs on refresh. exec "${SNAP}/meta/hooks/install" diff --git a/snapcraft.yaml b/snapcraft.yaml index 9a75a1db36..e2d6dc3a4e 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -35,12 +35,10 @@ description: | snap services openshell.gateway d=~/snap/openshell/common/.local/state/openshell/tls - mkdir -p -m 700 "$d/client" - sudo cat /var/snap/openshell/common/tls/ca.crt > "$d/ca.crt" - sudo cat /var/snap/openshell/common/tls/client/tls.crt > "$d/client/tls.crt" - (umask 077; sudo cat /var/snap/openshell/common/tls/client/tls.key > "$d/client/tls.key") - chmod 700 "$d" "$d/client" - chmod 600 "$d/ca.crt" "$d/client/tls.crt" "$d/client/tls.key" + mkdir -p -m 700 "$d" "$d/client" + sudo install -o "$USER" -m 600 /var/snap/openshell/common/tls/ca.crt "$d/" + sudo install -o "$USER" -m 600 -t "$d/client" \ + /var/snap/openshell/common/tls/client/tls.crt /var/snap/openshell/common/tls/client/tls.key openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 89816a1473..48597aa213 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -269,7 +269,6 @@ assert_snap_install_flow() { as_root() { printf 'root:%s\n' "$*"; } set_linux_target_runtime_dir() { :; } wait_for_docker_daemon() { printf '%s\n' "wait:docker"; } - ensure_snap_gateway_config() { printf '%s\n' "ensure:gateway-config"; } register_snap_gateway() { printf '%s\n' "register:gateway"; } wait_for_snap_gateway_listener() { printf '%s\n' "wait:gateway-listener"; } wait_for_local_gateway_status() { printf '%s\n' "wait:gateway-status"; } @@ -293,7 +292,6 @@ assert_snap_install_flow \ 1 0 "" \ "wait:docker root:snap install openshell --channel=latest/stable -ensure:gateway-config root:snap restart openshell.gateway wait:gateway-listener register:gateway @@ -304,7 +302,6 @@ assert_snap_install_flow \ 1 1 "" \ "wait:docker root:snap refresh openshell --channel=latest/stable -ensure:gateway-config root:snap restart openshell.gateway wait:gateway-listener register:gateway @@ -363,34 +360,6 @@ assert_snap_install_rejected \ 1 1 \ "the Docker snap is not currently compatible with OpenShell" -snap_config_dir="${tmpdir}/snap-config" -snap_config="${snap_config_dir}/gateway.toml" -if ! (as_root() { "$@"; }; ensure_snap_gateway_config "$snap_config"); then - echo "FAIL: Snap gateway config bootstrap should create a missing config" >&2 - exit 1 -fi -if ! grep -Fq 'allow_unauthenticated_users = true' "$snap_config"; then - echo "FAIL: Snap gateway config must permit the plaintext local CLI" >&2 - exit 1 -fi -if [[ -z $(find "$snap_config" -perm 600) ]]; then - echo "FAIL: Snap gateway config must be mode 0600" >&2 - exit 1 -fi - -printf '\noperator setting = true\n' >>"$snap_config" -cp "$snap_config" "${tmpdir}/snap-config-before" -(as_root() { "$@"; }; ensure_snap_gateway_config "$snap_config") -cmp -s "${tmpdir}/snap-config-before" "$snap_config" - -broken_config="${tmpdir}/broken-gateway.toml" -ln -s "${tmpdir}/missing-gateway.toml" "$broken_config" -(as_root() { "$@"; }; ensure_snap_gateway_config "$broken_config") -if [[ $(readlink "$broken_config") != "${tmpdir}/missing-gateway.toml" ]]; then - echo "FAIL: Snap gateway config bootstrap replaced a broken operator symlink" >&2 - exit 1 -fi - attempts_file="${tmpdir}/docker-attempts" root_probes_file="${tmpdir}/docker-root-probes" printf '0\n' >"$attempts_file" @@ -454,7 +423,8 @@ if ! ( print_gateway_add_output() { :; } info() { :; } TARGET_USER=test-user - SNAP_GATEWAY_SCHEME=https register_snap_gateway + snap_gateway_uses_mtls() { return 0; } + register_snap_gateway ) >"$out" 2>"$err"; then echo "FAIL: Snap gateway registration should succeed" >&2 cat "$err" >&2 || true @@ -473,7 +443,8 @@ if ! ( as_target_user() { printf 'target:%s\n' "$*" >>"$registration_calls_file"; } copy_snap_client_bundle() { printf 'copy:client-bundle\n' >>"$registration_calls_file"; } print_gateway_add_output() { :; } - SNAP_GATEWAY_SCHEME=http register_snap_gateway + snap_gateway_uses_mtls() { return 1; } + register_snap_gateway ) >"$out" 2>"$err"; then echo "FAIL: legacy plaintext Snap gateway registration should succeed" >&2 cat "$err" >&2 || true @@ -490,29 +461,20 @@ if ! grep -Fq "without client authentication" "$err"; then exit 1 fi -assert_snap_listener_scheme() { +assert_snap_listener_probe() { local name=$1 - local https_ok=$2 - local grpc_status=$3 - local expected=$4 + local uses_mtls=$2 + local expected=$3 local actual actual="$( - as_root() { "$@"; } - curl() { - case " $* " in - *" --cert "*) [ "$https_ok" = "1" ] ;; - *) return 1 ;; - esac - } - snap_legacy_grpc_health_status() { printf '%s' "$grpc_status"; } - sleep() { :; } + as_root() { printf 'root:'; "$@"; } + curl() { printf '%s\n' "$*"; } + snap_gateway_uses_mtls() { [ "$uses_mtls" = "1" ]; } info() { :; } - dump_local_gateway_diagnostics() { :; } - error() { printf 'timeout\n'; exit 0; } - OPENSHELL_INSTALL_GATEWAY_TIMEOUT=2 - wait_for_snap_gateway_listener 2>/dev/null - printf '%s\n' "${SNAP_GATEWAY_SCHEME:-none}" + OPENSHELL_SNAP_TLS_DIR=/tls + wait_for_snap_gateway_listener >/dev/null + printf '%s\n' "$_last_output" )" if [ "$actual" != "$expected" ]; then echo "FAIL: ${name}: expected ${expected}, got ${actual}" >&2 @@ -520,9 +482,10 @@ assert_snap_listener_scheme() { fi } -assert_snap_listener_scheme "mTLS gateway accepts the client bundle" 1 404 https -assert_snap_listener_scheme "legacy gateway answers plaintext gRPC" 0 200 http -assert_snap_listener_scheme "plaintext service routing is not a legacy gateway" 0 404 timeout +assert_snap_listener_probe "mTLS snap probes HTTPS with the client bundle as root" 1 \ + "root:-sS --max-time 2 --cacert /tls/ca.crt --cert /tls/client/tls.crt --key /tls/client/tls.key -o /dev/null https://127.0.0.1:17670/" +assert_snap_listener_probe "legacy snap probes plaintext HTTP" 0 \ + "-sS --max-time 2 -o /dev/null http://127.0.0.1:17670/" snap_tls_src="${tmpdir}/snap-tls" mkdir -p "${snap_tls_src}/client" From 836315299de1742aaa94cbf58209628e7f2e40b2 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 17:37:41 -0700 Subject: [PATCH 06/11] fix(snap): stop keeping a copy of replaced insecure configs Signed-off-by: Drew Newberry --- architecture/build.md | 2 +- docs/about/installation.mdx | 2 +- skills/debug-openshell-cluster/SKILL.md | 2 +- snap/hooks/install | 8 ++------ snapcraft.yaml | 12 +++++------- tasks/scripts/test-snap-install-hook.sh | 26 +++++-------------------- 6 files changed, 15 insertions(+), 37 deletions(-) diff --git a/architecture/build.md b/architecture/build.md index 511a33dd50..e1a18ed548 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -340,7 +340,7 @@ certificates reside in root-owned snap state. The installer copies the client bundle into the target user's private Snap state and registers the TLS endpoint; direct Snap installs require the same enrollment. The install and post-refresh hooks replace configs that explicitly enable plaintext or unauthenticated access -with the secure Docker default, preserving one private backup. Snap refreshes +with the secure Docker default. Snap refreshes restart the gateway so the migrated config takes effect immediately. Debian and RPM packages instead run systemd user services with user-owned mTLS diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 05160d2099..ef9933790b 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -126,7 +126,7 @@ openshell status Keep the client key private. Anyone who can read it can authenticate to the local gateway. -On refresh, the Snap replaces any regular gateway config that explicitly enables unauthenticated access or disables TLS with the secure Docker default. Other settings in that file are not carried over. It saves the previous config at `/var/snap/openshell/common/gateway.toml.pre-mtls` with mode `0600`, or at a uniquely suffixed path if that backup already exists. The refresh log prints the actual path. Review the backup before restoring other settings; restoring its insecure settings reopens access. +On refresh, the Snap replaces any regular gateway config that explicitly enables unauthenticated access or disables TLS with the secure Docker default. Other settings in that file are not carried over. Snap refreshes restart the gateway to apply the migrated config immediately. This interrupts active sandbox sessions. diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index c73ef2547b..6ccff2f2f8 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -72,7 +72,7 @@ Common findings: - `No active gateway`: register one with `openshell gateway add `. - Connection refused: gateway process is not running, service exposure is wrong, or a port-forward/proxy is not active. - TLS/certificate errors: the endpoint scheme or trust chain is wrong, a local mTLS bundle does not match the gateway CA, or TLS termination does not match the gateway listener. -- A Snap refresh restarts the gateway with its migrated mTLS config. The secure Snap gateway uses `https://127.0.0.1:17670` and requires a client bundle in the user's Snap state. Check `/var/snap/openshell/common/gateway.toml.pre-mtls` for settings replaced during migration, then follow the published Snap installation steps to re-register an old HTTP client. +- A Snap refresh restarts the gateway with its migrated mTLS config. The secure Snap gateway uses `https://127.0.0.1:17670` and requires a client bundle in the user's Snap state. Refresh replaces insecure configs without keeping a copy; follow the published Snap installation steps to re-register an old HTTP client. - `Unauthenticated` from an edge or OIDC gateway: refresh stored credentials with `openshell gateway login [name]`, then retry. Use `gateway logout` only when intentionally clearing local credentials. - A direct development endpoint with a private or self-signed certificate can be isolated with `--gateway-endpoint --gateway-insecure`; do not persist or recommend insecure verification for shared gateways. diff --git a/snap/hooks/install b/snap/hooks/install index ea3dc734a5..4ee70c32a5 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -11,16 +11,12 @@ insecure='^[[:space:]]*(allow_unauthenticated_users|disable_tls)[[:space:]]*=[[: # Keep secure operator configs, symlinks, and directories. Replace a config # that explicitly allows plaintext or anonymous access, even if it has other -# edits, after saving a private backup. +# edits. if [ -L "$config_file" ]; then exit 0 elif [ -f "$config_file" ]; then grep -Eq "$insecure" "$config_file" || exit 0 - backup_file="${config_file}.pre-mtls" - [ ! -e "$backup_file" ] || backup_file="${backup_file}.$(date +%s)" - umask 077 - cp "$config_file" "$backup_file" - echo "openshell: replacing insecure gateway config; previous config saved at $backup_file" >&2 + echo "openshell: replacing insecure gateway config with the mTLS default" >&2 elif [ -e "$config_file" ]; then exit 0 fi diff --git a/snapcraft.yaml b/snapcraft.yaml index e2d6dc3a4e..0f0456151f 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -43,13 +43,11 @@ description: | openshell status On refresh, configs that explicitly enable plaintext or unauthenticated - access are replaced with the secure default. The previous file is saved at - /var/snap/openshell/common/gateway.toml.pre-mtls (mode 0600), or a uniquely - suffixed path if that backup already exists. The gateway - restarts automatically during refresh to apply the new revision and - config. This interrupts active sandbox sessions. Users registered against - the old HTTP endpoint must remove that gateway registration and repeat - step 2, or rerun install.sh. + access are replaced with the secure default. The gateway restarts + automatically during refresh to apply the new revision and config. This + interrupts active sandbox sessions. Users registered against the old HTTP + endpoint must remove that gateway registration and repeat step 2, or + rerun install.sh. base: core24 grade: stable diff --git a/tasks/scripts/test-snap-install-hook.sh b/tasks/scripts/test-snap-install-hook.sh index 7d61adc073..16c4d0565d 100755 --- a/tasks/scripts/test-snap-install-hook.sh +++ b/tasks/scripts/test-snap-install-hook.sh @@ -64,21 +64,6 @@ printf '\n# operator note\n' >>"$common/gateway.toml" cp "$common/gateway.toml" "${work}/legacy-edited-before" SNAP_COMMON="$common" "$hook" cmp -s "$expected" "$common/gateway.toml" -cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" -if [[ -z $(find "$common/gateway.toml.pre-mtls" -perm 600) ]]; then - echo "FAIL: migrated config backup must be mode 0600" >&2 - exit 1 -fi -SNAP_COMMON="$common" "$hook" -cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" -cp "$legacy" "$common/gateway.toml" -SNAP_COMMON="$common" "$hook" -cmp -s "$expected" "$common/gateway.toml" -cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" -if [[ $(find "$common" -maxdepth 1 -name 'gateway.toml.pre-mtls.*' -type f | wc -l) -ne 1 ]]; then - echo "FAIL: repeated migration must preserve the existing backup" >&2 - exit 1 -fi common="${work}/custom-insecure" mkdir -p "$common" @@ -96,7 +81,6 @@ EOF cp "$common/gateway.toml" "${work}/custom-insecure-before" SNAP_COMMON="$common" "$hook" cmp -s "$expected" "$common/gateway.toml" -cmp -s "${work}/custom-insecure-before" "$common/gateway.toml.pre-mtls" common="${work}/custom-secure" mkdir -p "$common" @@ -111,10 +95,6 @@ EOF cp "$common/gateway.toml" "${work}/custom-secure-before" SNAP_COMMON="$common" "$hook" cmp -s "${work}/custom-secure-before" "$common/gateway.toml" -if [[ -e "$common/gateway.toml.pre-mtls" ]]; then - echo "FAIL: secure operator config should not be backed up or replaced" >&2 - exit 1 -fi common="${work}/post-refresh" mkdir -p "$common" "${work}/snap/meta/hooks" @@ -125,7 +105,6 @@ if ! cmp -s "$expected" "$common/gateway.toml"; then echo "FAIL: post-refresh hook must migrate an edited insecure config" >&2 exit 1 fi -cmp -s "${work}/legacy-edited-before" "$common/gateway.toml.pre-mtls" common="${work}/broken-link" mkdir -p "$common" @@ -144,4 +123,9 @@ if [[ ! -d "$common/gateway.toml" ]]; then exit 1 fi +if [[ -n $(find "$work" -name 'gateway.toml.pre-mtls*') ]]; then + echo "FAIL: install hook must not keep copies of replaced configs" >&2 + exit 1 +fi + echo "Snap install hook tests passed" From 668babb5d3a8baac2bd49cf3abc0c0f9fe95cb53 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 17:40:15 -0700 Subject: [PATCH 07/11] feat(install): make the snap an opt-in install method Stop selecting the OpenShell snap just because the snap command exists. Linux installs default to the Debian or RPM package; OPENSHELL_INSTALL_METHOD=snap (or deb, rpm) selects the package explicitly. Hosts that already have the OpenShell snap keep refreshing it rather than gaining a second gateway on the same port. The release canary and snap repro script opt in explicitly. Signed-off-by: Drew Newberry --- .github/workflows/release-canary.yml | 4 ++ architecture/build.md | 15 +++--- docs/about/installation.mdx | 6 +-- install.sh | 32 ++++++++---- nix/test-guest/scripts/snap-gateway-repro.sh | 4 +- tasks/scripts/test-install-sh.sh | 51 ++++++++++++++------ tasks/scripts/test-packaging-assets.sh | 2 +- 7 files changed, 75 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index e81094098f..f6e729fb40 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -195,6 +195,8 @@ jobs: name: Ubuntu Snap with system Docker if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest + env: + OPENSHELL_INSTALL_METHOD: snap timeout-minutes: 20 steps: - name: Install snapd @@ -265,6 +267,8 @@ jobs: name: Ubuntu Snap Docker preflight if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest + env: + OPENSHELL_INSTALL_METHOD: snap timeout-minutes: 20 steps: - name: Install snapd diff --git a/architecture/build.md b/architecture/build.md index e1a18ed548..76a20c5d6d 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -324,13 +324,14 @@ for direct executable installation on every environment. Release Dev and Release Tag run Ubuntu conformance through the Debian package, while Fedora continues using direct executable installation until RPM coverage is available. The release canary separately exercises the public installer on Ubuntu. The -OpenShell Snap requires a compatible, preinstalled non-Snap Docker daemon. Its -positive canary uses system Docker; negative preflight coverage verifies that -the installer rejects both missing Docker and the Docker Snap before installing -OpenShell. Its Debian lane removes snapd before running the installer so Snap -precedence cannot change the package under test. -Explicit release tags and the `pre` alias bypass Snap selection and use the -native Debian or RPM package path even when `snap` is available. The `pre` alias +installer selects the OpenShell Snap only with `OPENSHELL_INSTALL_METHOD=snap` +or when the Snap is already installed; otherwise it uses the native Debian or +RPM package. The Snap requires a compatible, preinstalled non-Snap Docker +daemon. Its positive canary uses system Docker; negative preflight coverage +verifies that the installer rejects both missing Docker and the Docker Snap +before installing OpenShell. +Explicit release tags and the `pre` alias always use the native Debian or RPM +package path. The `pre` alias checks matching Git tags in version order, then looks up the exact platform artifact and verifies the release run instead of listing every repository artifact. diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index ef9933790b..5fe49ec7b2 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -38,7 +38,7 @@ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=pre sh ``` -The installer checks prerelease tags from newest to oldest, selects an unexpired artifact from a successful release run for the current platform, and downloads only that artifact. Installed packages keep the candidate's exact version, such as `0.1.0-pre.3`. Prerelease tags do not create entries on the GitHub Releases page. On Linux, prereleases and explicit release tags use Debian or RPM packages even if `snap` is installed. +The installer checks prerelease tags from newest to oldest, selects an unexpired artifact from a successful release run for the current platform, and downloads only that artifact. Installed packages keep the candidate's exact version, such as `0.1.0-pre.3`. Prerelease tags do not create entries on the GitHub Releases page. On Linux, prereleases and explicit release tags use Debian or RPM packages. The rolling [`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) does not require GitHub authentication: @@ -86,7 +86,7 @@ The gateway reads `~/.config/openshell/gateway.toml` if it exists, otherwise the ## Linux -The script uses the [Snap](#snap) package when `snap` is available. Otherwise, or when you set `OPENSHELL_VERSION` to a release tag, it installs a Debian package on Debian and Ubuntu or an RPM package on Fedora and RHEL. Linux packages require glibc 2.28 or newer. +The script installs a Debian package on Debian and Ubuntu or an RPM package on Fedora and RHEL. Set `OPENSHELL_INSTALL_METHOD=snap` to install the [Snap](#snap) package instead; hosts that already have the OpenShell snap keep refreshing it. Linux packages require glibc 2.28 or newer. The gateway runs as a systemd user service at `https://127.0.0.1:17670` and reads `~/.config/openshell/gateway.toml`. @@ -110,7 +110,7 @@ The snap requires Docker Engine installed from your distribution or Docker's pac sudo snap install openshell ``` -The snap does not migrate existing Debian, RPM, or Homebrew installs. Remove any existing installation first, then rerun the script with `OPENSHELL_ACK_BREAKING_UPGRADE=1`. +The snap does not migrate existing Debian, RPM, or Homebrew installs. Remove any existing installation first, then rerun the script with `OPENSHELL_INSTALL_METHOD=snap OPENSHELL_ACK_BREAKING_UPGRADE=1`. The gateway runs as a system service at `https://127.0.0.1:17670` and reads `/var/snap/openshell/common/gateway.toml`. It requires a client certificate. The install script copies that certificate to the installing user's Snap state and registers the gateway automatically. If you installed with `sudo snap install openshell`, give each trusted user the certificate and register the gateway from that user's account: diff --git a/install.sh b/install.sh index 709cf750b2..83b7c479be 100755 --- a/install.sh +++ b/install.sh @@ -61,21 +61,22 @@ ENVIRONMENT VARIABLES: OPENSHELL_ACK_BREAKING_UPGRADE Set to 1 only after backing up and cleaning up a pre-v0.0.37 or non-snap installation. + OPENSHELL_INSTALL_METHOD + Linux package to install: snap, deb, or rpm. Unset + selects deb or rpm from the host package manager. NOTES: When OPENSHELL_VERSION is unset, this resolves the latest tagged release from ${GITHUB_URL}/releases/latest. - On Linux, the installer uses the OpenShell snap when the snap command is - available and OPENSHELL_VERSION is unset or dev. Snap installs use - latest/stable by default and latest/edge for dev. Explicit release tags - and prereleases use Debian or RPM packages. The OpenShell snap requires a - running Docker Engine installed from a system package or Docker's package + Linux installs the Debian package on amd64/arm64 or the RPM packages on + x86_64/aarch64, depending on the host package manager. Set + OPENSHELL_INSTALL_METHOD=snap to install the OpenShell snap instead; hosts + that already have the OpenShell snap keep refreshing it. Snap installs use + latest/stable by default and latest/edge for dev, and do not support + explicit release tags or prereleases. The OpenShell snap requires a running + Docker Engine installed from a system package or Docker's package repository. The Docker snap is not currently compatible with OpenShell. - - For explicit versions or without snap, Linux installs the Debian package - on amd64/arm64 or the RPM packages on x86_64/aarch64, depending on the - host package manager. macOS installs the release Homebrew formula on Apple Silicon and starts a brew services-backed local gateway. EOF @@ -658,9 +659,20 @@ local_gateway_endpoint() { } linux_package_method() { + case "${OPENSHELL_INSTALL_METHOD:-}" in + snap | deb | rpm) + echo "$OPENSHELL_INSTALL_METHOD" + return 0 + ;; + '') ;; + *) error "unsupported OPENSHELL_INSTALL_METHOD=${OPENSHELL_INSTALL_METHOD}; use snap, deb, or rpm" ;; + esac + + # Keep refreshing an existing snap install instead of adding a second + # gateway on the same port. case "${OPENSHELL_VERSION:-}" in '' | dev) - if has_cmd snap; then + if has_cmd snap && snap list openshell >/dev/null 2>&1; then echo "snap" return 0 fi diff --git a/nix/test-guest/scripts/snap-gateway-repro.sh b/nix/test-guest/scripts/snap-gateway-repro.sh index d92800b523..43b7f3949c 100755 --- a/nix/test-guest/scripts/snap-gateway-repro.sh +++ b/nix/test-guest/scripts/snap-gateway-repro.sh @@ -99,7 +99,7 @@ for attempt in $(seq 1 "${attempts}"); do echo "==> install.sh Snap ${mode} reproduction attempt ${attempt}/${attempts}" if [ "${mode}" != system-docker ]; then output=$(mktemp) - if OPENSHELL_VERSION=dev sh "${install_script}" >"${output}" 2>&1; then + if OPENSHELL_INSTALL_METHOD=snap OPENSHELL_VERSION=dev sh "${install_script}" >"${output}" 2>&1; then echo "install.sh unexpectedly succeeded in ${mode} mode" >&2 cat "${output}" >&2 rm -f "${output}" @@ -127,7 +127,7 @@ for attempt in $(seq 1 "${attempts}"); do fi sandbox="snap-${attempt}-$$" - if ! OPENSHELL_VERSION=dev sh "${install_script}" || + if ! OPENSHELL_INSTALL_METHOD=snap OPENSHELL_VERSION=dev sh "${install_script}" || ! sudo snap list openshell >/dev/null || ! snap info openshell | grep -Eq '^tracking: +latest/edge$' || ! docker_is_ready || diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 48597aa213..286fa3f1ff 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -100,25 +100,30 @@ assert_glibc_preflight_fails \ "OpenShell Linux packages require glibc >= 2.28; detected musl or unsupported libc." \ setup_ldd_musl +# snap_state: 0 = no snap command, 1 = snap command only, +# installed = the OpenShell snap is already installed. assert_linux_package_method() { local name=$1 - local requested_version=$2 - local snap_present=$3 - local dpkg_present=$4 - local rpm_present=$5 - local expected=$6 + local install_method=$2 + local requested_version=$3 + local snap_state=$4 + local dpkg_present=$5 + local rpm_present=$6 + local expected=$7 local actual actual="$( + export OPENSHELL_INSTALL_METHOD="$install_method" export OPENSHELL_VERSION="$requested_version" has_cmd() { case "$1" in - snap) [ "$snap_present" = "1" ] ;; + snap) [ "$snap_state" != "0" ] ;; dpkg) [ "$dpkg_present" = "1" ] ;; rpm) [ "$rpm_present" = "1" ] ;; *) return 1 ;; esac } + snap() { [ "$*" = "list openshell" ] && [ "$snap_state" = "installed" ]; } linux_package_method )" if [ "$actual" != "$expected" ]; then @@ -127,16 +132,30 @@ assert_linux_package_method() { fi } -assert_linux_package_method "snap takes precedence over deb and rpm" "" 1 1 1 snap -assert_linux_package_method "dev uses snap" dev 1 1 1 snap -assert_linux_package_method "pre uses deb despite snap" pre 1 1 1 deb -assert_linux_package_method "numbered prerelease uses deb despite snap" v0.1.0-pre.3 1 1 1 deb -assert_linux_package_method "pre uses rpm despite snap" pre 1 0 1 rpm -assert_linux_package_method "pinned stable uses deb despite snap" v1.2.3 1 1 1 deb -assert_linux_package_method "pinned stable uses rpm despite snap" v1.2.3 1 0 1 rpm -assert_linux_package_method "deb is selected without snap" "" 0 1 1 deb -assert_linux_package_method "dev uses deb without snap" dev 0 1 1 deb -assert_linux_package_method "rpm is selected without snap or deb" "" 0 0 1 rpm +assert_linux_package_method "deb is the default despite snap" "" "" 1 1 1 deb +assert_linux_package_method "rpm is the default despite snap" "" "" 1 0 1 rpm +assert_linux_package_method "dev uses deb despite snap" "" dev 1 1 1 deb +assert_linux_package_method "snap is opt-in" snap "" 1 1 1 snap +assert_linux_package_method "snap opt-in with dev" snap dev 1 1 1 snap +assert_linux_package_method "explicit deb" deb "" installed 0 1 deb +assert_linux_package_method "explicit rpm" rpm "" 1 1 1 rpm +assert_linux_package_method "existing snap install keeps refreshing" "" "" installed 1 1 snap +assert_linux_package_method "existing snap install keeps refreshing dev" "" dev installed 1 1 snap +assert_linux_package_method "pre uses deb despite existing snap" "" pre installed 1 1 deb +assert_linux_package_method "numbered prerelease uses deb despite existing snap" "" v0.1.0-pre.3 installed 1 1 deb +assert_linux_package_method "pinned stable uses rpm despite existing snap" "" v1.2.3 installed 0 1 rpm +assert_linux_package_method "deb is selected without snap" "" "" 0 1 1 deb +assert_linux_package_method "rpm is selected without snap or deb" "" "" 0 0 1 rpm + +if (OPENSHELL_INSTALL_METHOD=flatpak linux_package_method) >"$out" 2>"$err"; then + echo "FAIL: unsupported OPENSHELL_INSTALL_METHOD should be rejected" >&2 + exit 1 +fi +if ! grep -Fq "unsupported OPENSHELL_INSTALL_METHOD=flatpak" "$err"; then + echo "FAIL: unsupported OPENSHELL_INSTALL_METHOD was not explained" >&2 + cat "$err" >&2 + exit 1 +fi if ! ( find_existing_native_openshell_bin() { return 1; } diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index 51ba8d0258..97acea4181 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -138,7 +138,7 @@ assert_contains "$snap_install_docs" "snap connect openshell:docker :docker" assert_contains "$snap_canary" "install.sh | sh" assert_contains "$snap_canary" "ubuntu-snap-system-docker:" assert_contains "$snap_canary" "ubuntu-snap-docker-preflight:" -assert_contains "$snap_repro" 'OPENSHELL_VERSION=dev sh "${install_script}"' +assert_contains "$snap_repro" 'OPENSHELL_INSTALL_METHOD=snap OPENSHELL_VERSION=dev sh "${install_script}"' assert_contains "$snap_repro" "system-docker" assert_contains "$snap_repro" "missing-docker" assert_contains "$snap_repro" "docker-snap" From 72496a6737df31e5b56d14f41b618a94c010f8bc Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 18:06:13 -0700 Subject: [PATCH 08/11] fix(snap): let the gateway auto-detect its compute driver Signed-off-by: Drew Newberry --- architecture/build.md | 2 +- docs/about/installation.mdx | 2 +- snap/hooks/install | 1 - tasks/scripts/test-packaging-assets.sh | 3 ++- tasks/scripts/test-snap-install-hook.sh | 1 - 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/architecture/build.md b/architecture/build.md index 76a20c5d6d..639dde8ba8 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -341,7 +341,7 @@ certificates reside in root-owned snap state. The installer copies the client bundle into the target user's private Snap state and registers the TLS endpoint; direct Snap installs require the same enrollment. The install and post-refresh hooks replace configs that explicitly enable plaintext or unauthenticated access -with the secure Docker default. Snap refreshes +with the secure default. Snap refreshes restart the gateway so the migrated config takes effect immediately. Debian and RPM packages instead run systemd user services with user-owned mTLS diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 5fe49ec7b2..f5157545a3 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -126,7 +126,7 @@ openshell status Keep the client key private. Anyone who can read it can authenticate to the local gateway. -On refresh, the Snap replaces any regular gateway config that explicitly enables unauthenticated access or disables TLS with the secure Docker default. Other settings in that file are not carried over. +On refresh, the Snap replaces any regular gateway config that explicitly enables unauthenticated access or disables TLS with the secure default. Other settings in that file are not carried over. Snap refreshes restart the gateway to apply the migrated config immediately. This interrupts active sandbox sessions. diff --git a/snap/hooks/install b/snap/hooks/install index 4ee70c32a5..78efeefdf8 100755 --- a/snap/hooks/install +++ b/snap/hooks/install @@ -30,7 +30,6 @@ cat >"$temporary_file" <<'CONFIG' version = 2 [openshell.gateway] -compute_driver = "docker" CONFIG mv -f "$temporary_file" "$config_file" trap - 0 HUP INT TERM diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index 97acea4181..04f69b08be 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -123,7 +123,8 @@ if [[ ! -x "$snap_install_hook" ]]; then echo "FAIL: Snap install hook must be executable" >&2 exit 1 fi -assert_contains "$snap_install_hook" 'compute_driver = "docker"' +assert_not_contains "$snap_install_hook" 'compute_driver' +assert_not_contains "$snap_install_hook" 'allow_unauthenticated_users = true' assert_contains "$snapcraft" 'refresh-mode: restart' if [[ ! -x "$(dirname "$snap_install_hook")/post-refresh" ]]; then echo "FAIL: Snap post-refresh hook must be executable" >&2 diff --git a/tasks/scripts/test-snap-install-hook.sh b/tasks/scripts/test-snap-install-hook.sh index 16c4d0565d..27c8ab1975 100755 --- a/tasks/scripts/test-snap-install-hook.sh +++ b/tasks/scripts/test-snap-install-hook.sh @@ -16,7 +16,6 @@ cat >"$expected" <<'EOF' version = 2 [openshell.gateway] -compute_driver = "docker" EOF legacy="${work}/legacy.toml" From e3081511814ab924f0ff510893c658c7ca4acf2d Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 18:18:56 -0700 Subject: [PATCH 09/11] fix(snap): restart the gateway after refresh Published revisions use refresh-mode: endure, and snapd honors the old revision's setting during a refresh, so the plaintext gateway kept running with the migrated config unused until a manual restart. Restart the gateway from the post-refresh hook so the mTLS config takes effect immediately. Signed-off-by: Drew Newberry --- snap/hooks/post-refresh | 10 ++++++++-- tasks/scripts/test-snap-install-hook.sh | 14 +++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/snap/hooks/post-refresh b/snap/hooks/post-refresh index 1e46280d6d..28eb20373d 100755 --- a/snap/hooks/post-refresh +++ b/snap/hooks/post-refresh @@ -2,5 +2,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Replace insecure gateway configs on refresh. -exec "${SNAP}/meta/hooks/install" +# Replace insecure gateway configs on refresh, then restart the gateway so the +# new config takes effect even when the previous revision used +# refresh-mode: endure and kept its plaintext gateway running. + +set -eu + +"${SNAP}/meta/hooks/install" +snapctl restart "${SNAP_INSTANCE_NAME}.gateway" diff --git a/tasks/scripts/test-snap-install-hook.sh b/tasks/scripts/test-snap-install-hook.sh index 27c8ab1975..e82cb64b55 100755 --- a/tasks/scripts/test-snap-install-hook.sh +++ b/tasks/scripts/test-snap-install-hook.sh @@ -99,11 +99,23 @@ common="${work}/post-refresh" mkdir -p "$common" "${work}/snap/meta/hooks" cp "$hook" "${work}/snap/meta/hooks/install" cp "${work}/legacy-edited-before" "$common/gateway.toml" -SNAP="${work}/snap" SNAP_COMMON="$common" "${hook_dir}/post-refresh" +mkdir -p "${work}/bin" +cat >"${work}/bin/snapctl" <>"${work}/snapctl.log" +EOF +chmod 755 "${work}/bin/snapctl" +PATH="${work}/bin:$PATH" SNAP="${work}/snap" SNAP_COMMON="$common" \ + SNAP_INSTANCE_NAME=openshell "${hook_dir}/post-refresh" if ! cmp -s "$expected" "$common/gateway.toml"; then echo "FAIL: post-refresh hook must migrate an edited insecure config" >&2 exit 1 fi +if [[ $(cat "${work}/snapctl.log") != "restart openshell.gateway" ]]; then + echo "FAIL: post-refresh hook must restart the gateway" >&2 + cat "${work}/snapctl.log" >&2 + exit 1 +fi common="${work}/broken-link" mkdir -p "$common" From f461d77faa1b7ce194401e76863a2a6f92476fbd Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 18:21:25 -0700 Subject: [PATCH 10/11] docs(snap): drop refresh notes from the snap description Signed-off-by: Drew Newberry --- snapcraft.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/snapcraft.yaml b/snapcraft.yaml index 0f0456151f..6c943c1387 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -42,13 +42,6 @@ description: | openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status - On refresh, configs that explicitly enable plaintext or unauthenticated - access are replaced with the secure default. The gateway restarts - automatically during refresh to apply the new revision and config. This - interrupts active sandbox sessions. Users registered against the old HTTP - endpoint must remove that gateway registration and repeat step 2, or - rerun install.sh. - base: core24 grade: stable confinement: strict From 014df77dd46cc1afb6e379336e14d55e5acd3e8f Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 25 Sep 2026 18:24:05 -0700 Subject: [PATCH 11/11] docs(snap): trim snap refresh notes from installation docs Signed-off-by: Drew Newberry --- docs/about/installation.mdx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index f5157545a3..d41639cddc 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -124,13 +124,7 @@ openshell gateway add https://127.0.0.1:17670 --local --name openshell openshell status ``` -Keep the client key private. Anyone who can read it can authenticate to the local gateway. - -On refresh, the Snap replaces any regular gateway config that explicitly enables unauthenticated access or disables TLS with the secure default. Other settings in that file are not carried over. - -Snap refreshes restart the gateway to apply the migrated config immediately. This interrupts active sandbox sessions. - -If you previously registered the plaintext endpoint, run the certificate and HTTPS registration steps above after the refresh. Remove the old registration first with `openshell gateway remove openshell` if the name already exists. You can also rerun the install script to refresh the registration automatically. +Keep the client key private. To install a locally built snap, connect its interfaces manually: