diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index abbcef423..f7bc6ad1f 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -128,7 +128,7 @@ jobs: stats_bin="${SCCACHE_PATH:-sccache}" "$stats_bin" --show-stats status=$? - if [[ $status -ne 0 ]]; then + if [ "$status" -ne 0 ]; then echo "::warning::sccache stats unavailable (exit $status)" fi exit 0 diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 520a51c65..f1df71b3f 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -501,6 +501,7 @@ jobs: docker buildx build \ --file deploy/docker/Dockerfile.gateway-macos \ --build-arg OPENSHELL_CARGO_VERSION="${{ needs.compute-versions.outputs.cargo_version }}" \ + --build-arg OPENSHELL_IMAGE_TAG=dev \ --build-arg CARGO_TARGET_CACHE_SCOPE="${{ github.sha }}" \ --target binary \ --output type=local,dest=out/ \ diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 19d9df47f..18bf74db5 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -620,6 +620,7 @@ jobs: docker buildx build \ --file deploy/docker/Dockerfile.gateway-macos \ --build-arg OPENSHELL_CARGO_VERSION="${{ needs.compute-versions.outputs.cargo_version }}" \ + --build-arg OPENSHELL_IMAGE_TAG="${{ needs.compute-versions.outputs.semver }}" \ --build-arg CARGO_TARGET_CACHE_SCOPE="${{ github.sha }}" \ --target binary \ --output type=local,dest=out/ \ diff --git a/architecture/build.md b/architecture/build.md index 266575efb..cfe13c4b1 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -27,6 +27,13 @@ target architecture, stages them under `deploy/docker/.build/`, and then uses Buildx to publish per-architecture images and multi-architecture tags. Gateway image builds bake the corresponding supervisor image tag into the gateway binary so Docker sandboxes do not depend on `:latest` by default. +Package formulas also pin Docker supervisor extraction to the matching release +image tag so standalone gateway binaries do not infer image tags from package +versions. +The Homebrew service keeps gateway TLS under the Homebrew state directory but +mirrors Docker sandbox client TLS into `$HOME/.local/state/openshell/homebrew/tls` +at service start, because Docker Desktop bind mounts must use paths visible to +the macOS user's shared home directory. Local image work should use `mise` tasks rather than direct Docker commands so the same staging and tagging assumptions are used locally and in CI. diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index dd1b6721d..fc30b03d6 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -643,6 +643,60 @@ fn mtls_certs_exist_for_endpoint(name: &str, endpoint: &str) -> bool { }) } +fn package_managed_tls_dirs() -> Vec { + if let Some(path) = std::env::var_os("OPENSHELL_LOCAL_TLS_DIR") { + return vec![PathBuf::from(path)]; + } + + let mut dirs = Vec::new(); + + if cfg!(target_os = "macos") { + dirs.push(PathBuf::from("/opt/homebrew/var/openshell/tls")); + dirs.push(PathBuf::from("/usr/local/var/openshell/tls")); + } + + let state_dir = std::env::var_os("XDG_STATE_HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/state"))); + if let Some(state_dir) = state_dir { + dirs.push(state_dir.join("openshell/tls")); + } + + dirs +} + +fn import_local_package_mtls_bundle(name: &str) -> Result> { + for dir in package_managed_tls_dirs() { + let ca = dir.join("ca.crt"); + let cert = dir.join("client/tls.crt"); + let key = dir.join("client/tls.key"); + if !(ca.is_file() && cert.is_file() && key.is_file()) { + continue; + } + + let bundle = openshell_bootstrap::pki::PkiBundle { + ca_cert_pem: std::fs::read_to_string(&ca) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {}", ca.display()))?, + ca_key_pem: String::new(), + server_cert_pem: String::new(), + server_key_pem: String::new(), + client_cert_pem: std::fs::read_to_string(&cert) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {}", cert.display()))?, + client_key_pem: std::fs::read_to_string(&key) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {}", key.display()))?, + }; + openshell_bootstrap::mtls::store_pki_bundle(name, &bundle) + .wrap_err_with(|| format!("failed to store mTLS bundle for gateway '{name}'"))?; + + return Ok(Some(dir)); + } + + Ok(None) +} + fn plaintext_gateway_is_remote(endpoint: &str, remote: Option<&str>, local: bool) -> bool { if local { return false; @@ -924,16 +978,13 @@ pub async fn gateway_add( // Verify the gateway is reachable. let tls = TlsOptions::default(); - match http_health_check(&endpoint, &tls).await { - Ok(Some(status)) if status.is_success() => {} - _ => { - eprintln!( - "{} Gateway is not reachable at {endpoint}", - "⚠".yellow().bold(), - ); - if !has_mtls_certs { - eprintln!(" Verify the gateway is running and the endpoint is correct."); - } + if !gateway_reachable(&endpoint, &tls).await { + eprintln!( + "{} Gateway is not reachable at {endpoint}", + "⚠".yellow().bold(), + ); + if !has_mtls_certs { + eprintln!(" Verify the gateway is running and the endpoint is correct."); } } @@ -951,7 +1002,13 @@ pub async fn gateway_add( if remote.is_some() || local { // mTLS gateway (remote or local). - let certs_on_disk = mtls_certs_exist_for_endpoint(name, &endpoint); + let imported_mtls_dir = if local { + import_local_package_mtls_bundle(name)? + } else { + None + }; + let certs_on_disk = + imported_mtls_dir.is_some() || mtls_certs_exist_for_endpoint(name, &endpoint); if !certs_on_disk { return Err(miette::miette!( "mTLS certificates for gateway '{name}' were not found.\n\ @@ -984,14 +1041,11 @@ pub async fn gateway_add( // Verify the gateway is reachable over mTLS. let tls = TlsOptions::default().with_gateway_name(name); - match http_health_check(&endpoint, &tls).await { - Ok(Some(status)) if status.is_success() => {} - _ => { - eprintln!( - "{} Gateway is not reachable at {endpoint}. Verify the gateway is running.", - "⚠".yellow().bold(), - ); - } + if !gateway_reachable(&endpoint, &tls).await { + eprintln!( + "{} Gateway is not reachable at {endpoint}. Verify the gateway is running.", + "⚠".yellow().bold(), + ); } eprintln!( @@ -1252,6 +1306,16 @@ async fn http_health_check(server: &str, tls: &TlsOptions) -> Result bool { + if let Ok(mut client) = grpc_client(server, tls).await + && client.health(HealthRequest {}).await.is_ok() + { + return true; + } + + matches!(http_health_check(server, tls).await, Ok(Some(status)) if status.is_success()) +} + fn remove_gateway_registration(name: &str) { if let Err(err) = openshell_bootstrap::edge_token::remove_edge_token(name) { tracing::debug!("failed to remove edge token: {err}"); @@ -5391,10 +5455,10 @@ mod tests { TlsOptions, dockerfile_sources_supported_for_gateway, format_gateway_select_header, format_gateway_select_items, format_provider_attachment_table, gateway_add, gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label, - git_sync_files, http_health_check, image_requests_gpu, inferred_provider_type, - parse_cli_setting_value, parse_credential_pairs, plaintext_gateway_is_remote, - provisioning_timeout_message, ready_false_condition_message, resolve_from, - sandbox_should_persist, + git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle, + inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value, + parse_credential_pairs, plaintext_gateway_is_remote, provisioning_timeout_message, + ready_false_condition_message, resolve_from, sandbox_should_persist, }; use crate::TEST_ENV_LOCK; use hyper::StatusCode; @@ -5402,7 +5466,7 @@ mod tests { use std::fs; use std::io::{Read, Write}; use std::net::TcpListener; - use std::path::Path; + use std::path::{Path, PathBuf}; use std::process::Command; use std::thread; @@ -6023,6 +6087,52 @@ mod tests { assert_eq!(gateway_auth_label(&gateway), "mtls"); } + #[test] + fn package_managed_tls_dirs_respects_override() { + let _guard = TEST_ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let _tls_dir = EnvVarGuard::set("OPENSHELL_LOCAL_TLS_DIR", "/tmp/openshell-test-tls"); + + assert_eq!( + package_managed_tls_dirs(), + vec![PathBuf::from("/tmp/openshell-test-tls")], + ); + } + + #[test] + fn import_local_package_mtls_bundle_copies_client_materials() { + let tmpdir = tempfile::tempdir().expect("create tmpdir"); + let package_tls = tmpdir.path().join("package-tls"); + fs::create_dir_all(package_tls.join("client")).expect("create package tls dir"); + fs::write(package_tls.join("ca.crt"), "ca").expect("write ca"); + fs::write(package_tls.join("client/tls.crt"), "client cert").expect("write cert"); + fs::write(package_tls.join("client/tls.key"), "client key").expect("write key"); + + with_tmp_xdg(tmpdir.path(), || { + let _tls_dir = EnvVarGuard::set( + "OPENSHELL_LOCAL_TLS_DIR", + package_tls.to_str().expect("temp path should be utf-8"), + ); + + let imported = + import_local_package_mtls_bundle("openshell").expect("import local bundle"); + + assert_eq!(imported.as_deref(), Some(package_tls.as_path())); + + let mtls = tmpdir.path().join("openshell/gateways/openshell/mtls"); + assert_eq!(fs::read_to_string(mtls.join("ca.crt")).unwrap(), "ca"); + assert_eq!( + fs::read_to_string(mtls.join("tls.crt")).unwrap(), + "client cert", + ); + assert_eq!( + fs::read_to_string(mtls.join("tls.key")).unwrap(), + "client key", + ); + }); + } + #[test] fn plaintext_gateway_locality_infers_loopback_endpoints_as_local() { assert!(!plaintext_gateway_is_remote( diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index a864a3eb6..db197685d 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -89,7 +89,7 @@ pub fn default_docker_supervisor_image() -> String { /// fallback covers image build wrappers that already tag the gateway and /// supervisor together. Standalone release binaries also patch the Cargo /// package version, so use it when it has been set to a real release value. -fn default_docker_supervisor_image_tag() -> &'static str { +fn default_docker_supervisor_image_tag() -> String { resolve_default_docker_supervisor_image_tag( option_env!("OPENSHELL_IMAGE_TAG"), option_env!("IMAGE_TAG"), @@ -101,8 +101,8 @@ fn resolve_default_docker_supervisor_image_tag( openshell_image_tag: Option<&'static str>, image_tag: Option<&'static str>, cargo_pkg_version: &'static str, -) -> &'static str { - openshell_image_tag +) -> String { + let tag = openshell_image_tag .filter(|tag| !tag.is_empty()) .or_else(|| image_tag.filter(|tag| !tag.is_empty())) .unwrap_or_else(|| { @@ -111,7 +111,9 @@ fn resolve_default_docker_supervisor_image_tag( } else { cargo_pkg_version } - }) + }); + + tag.replace('+', "-") } /// Queried by the Docker driver to decide when a sandbox's supervisor diff --git a/crates/openshell-driver-docker/src/tests.rs b/crates/openshell-driver-docker/src/tests.rs index e41f2688e..41c9a5901 100644 --- a/crates/openshell-driver-docker/src/tests.rs +++ b/crates/openshell-driver-docker/src/tests.rs @@ -722,6 +722,22 @@ fn docker_supervisor_image_tag_prefers_explicit_build_tags() { ); } +#[test] +fn docker_supervisor_image_tag_sanitizes_build_metadata_for_docker() { + assert_eq!( + resolve_default_docker_supervisor_image_tag(None, None, "0.0.37-dev.156+g1d3b741ee"), + "0.0.37-dev.156-g1d3b741ee", + ); + assert_eq!( + resolve_default_docker_supervisor_image_tag( + Some("0.0.37-dev.156+g1d3b741ee"), + None, + "0.0.0", + ), + "0.0.37-dev.156-g1d3b741ee", + ); +} + #[test] fn supervisor_cache_path_namespaces_by_digest_under_openshell_data_dir() { let base = PathBuf::from("/var/cache/share"); diff --git a/crates/openshell-server/src/certgen.rs b/crates/openshell-server/src/certgen.rs index b9e4d7bd5..f7dcc0803 100644 --- a/crates/openshell-server/src/certgen.rs +++ b/crates/openshell-server/src/certgen.rs @@ -70,16 +70,16 @@ pub async fn run(args: CertgenArgs) -> Result<()> { ) .init(); - let bundle = generate_pki(&args.server_sans)?; - if args.dry_run { + let bundle = generate_pki(&args.server_sans)?; print_bundle(&bundle); return Ok(()); } if let Some(dir) = args.output_dir.as_deref() { - run_local(dir, &bundle) + run_local(dir, &args.server_sans) } else { + let bundle = generate_pki(&args.server_sans)?; run_kubernetes(&args, &bundle).await } } @@ -277,12 +277,13 @@ fn decide_local(present: usize) -> LocalAction { } } -fn run_local(dir: &Path, bundle: &PkiBundle) -> Result<()> { +fn run_local(dir: &Path, server_sans: &[String]) -> Result<()> { let paths = LocalPaths::resolve(dir); - match decide_local(paths.existence_count()) { + let bundle = match decide_local(paths.existence_count()) { LocalAction::Skip => { info!(dir = %dir.display(), "PKI files already exist, skipping."); + read_local_bundle(&paths)? } LocalAction::PartialState => { return Err(miette::miette!( @@ -292,21 +293,40 @@ fn run_local(dir: &Path, bundle: &PkiBundle) -> Result<()> { )); } LocalAction::Create => { - write_local_bundle(dir, bundle, &paths)?; + let bundle = generate_pki(server_sans)?; + write_local_bundle(dir, &bundle, &paths)?; info!(dir = %dir.display(), "PKI files created."); + bundle } - } + }; // Always make sure the CLI auto-discovery copy is in place. This // self-heals the case where the operator wiped ~/.config/openshell but // left the gateway state directory intact. - if let Err(e) = openshell_bootstrap::mtls::store_pki_bundle("openshell", bundle) { + if let Err(e) = openshell_bootstrap::mtls::store_pki_bundle("openshell", &bundle) { warn!(error = %e, "failed to copy client mTLS materials for CLI auto-discovery"); } Ok(()) } +fn read_local_bundle(paths: &LocalPaths) -> Result { + Ok(PkiBundle { + ca_cert_pem: read_pem(&paths.ca_crt)?, + ca_key_pem: read_pem(&paths.ca_key)?, + server_cert_pem: read_pem(&paths.server_crt)?, + server_key_pem: read_pem(&paths.server_key)?, + client_cert_pem: read_pem(&paths.client_crt)?, + client_key_pem: read_pem(&paths.client_key)?, + }) +} + +fn read_pem(path: &Path) -> Result { + std::fs::read_to_string(path) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {}", path.display())) +} + fn write_local_bundle(dir: &Path, bundle: &PkiBundle, paths: &LocalPaths) -> Result<()> { // Stage to a sibling tmp dir so individual renames into the final layout // are atomic on the same filesystem. @@ -386,8 +406,8 @@ fn print_bundle(bundle: &PkiBundle) { #[cfg(test)] mod tests { use super::{ - K8sAction, LocalAction, LocalPaths, decide_k8s, decide_local, sibling_temp_dir, tls_secret, - write_local_bundle, + K8sAction, LocalAction, LocalPaths, decide_k8s, decide_local, read_local_bundle, + sibling_temp_dir, tls_secret, write_local_bundle, }; use openshell_bootstrap::pki::generate_pki; use std::path::Path; @@ -490,6 +510,21 @@ mod tests { assert!(server_key.contains("BEGIN PRIVATE KEY")); } + #[test] + fn read_local_bundle_uses_existing_files() { + let parent = tempfile::tempdir().expect("tempdir"); + let dir = parent.path().join("tls"); + let bundle = generate_pki(&[]).expect("generate_pki"); + let paths = LocalPaths::resolve(&dir); + + write_local_bundle(&dir, &bundle, &paths).expect("write_local_bundle"); + + let read = read_local_bundle(&paths).expect("read_local_bundle"); + assert_eq!(read.ca_cert_pem, bundle.ca_cert_pem); + assert_eq!(read.client_cert_pem, bundle.client_cert_pem); + assert_eq!(read.client_key_pem, bundle.client_key_pem); + } + #[cfg(unix)] #[test] fn write_local_bundle_sets_owner_only_on_keys() { diff --git a/deploy/deb/openshell-gateway.service b/deploy/deb/openshell-gateway.service index 26e3c07be..9de94da22 100644 --- a/deploy/deb/openshell-gateway.service +++ b/deploy/deb/openshell-gateway.service @@ -9,14 +9,25 @@ StateDirectory=openshell/gateway # %S resolves to $XDG_STATE_HOME for user services. Environment=OPENSHELL_BIND_ADDRESS=127.0.0.1 Environment=OPENSHELL_SERVER_PORT=17670 -Environment=OPENSHELL_DISABLE_TLS=true -Environment=OPENSHELL_DISABLE_GATEWAY_AUTH=true +Environment=OPENSHELL_TLS_CERT=%S/openshell/tls/server/tls.crt +Environment=OPENSHELL_TLS_KEY=%S/openshell/tls/server/tls.key +Environment=OPENSHELL_TLS_CLIENT_CA=%S/openshell/tls/ca.crt Environment=OPENSHELL_DB_URL=sqlite:%S/openshell/gateway/openshell.db -Environment=OPENSHELL_GRPC_ENDPOINT=http://127.0.0.1:17670 +Environment=OPENSHELL_GRPC_ENDPOINT=https://127.0.0.1:17670 Environment=OPENSHELL_SSH_GATEWAY_HOST=127.0.0.1 Environment=OPENSHELL_SSH_GATEWAY_PORT=17670 Environment=OPENSHELL_VM_DRIVER_STATE_DIR=%S/openshell/vm-driver +Environment=OPENSHELL_VM_TLS_CA=%S/openshell/tls/ca.crt +Environment=OPENSHELL_VM_TLS_CERT=%S/openshell/tls/client/tls.crt +Environment=OPENSHELL_VM_TLS_KEY=%S/openshell/tls/client/tls.key +Environment=OPENSHELL_DOCKER_TLS_CA=%S/openshell/tls/ca.crt +Environment=OPENSHELL_DOCKER_TLS_CERT=%S/openshell/tls/client/tls.crt +Environment=OPENSHELL_DOCKER_TLS_KEY=%S/openshell/tls/client/tls.key +Environment=OPENSHELL_PODMAN_TLS_CA=%S/openshell/tls/ca.crt +Environment=OPENSHELL_PODMAN_TLS_CERT=%S/openshell/tls/client/tls.crt +Environment=OPENSHELL_PODMAN_TLS_KEY=%S/openshell/tls/client/tls.key EnvironmentFile=-%h/.config/openshell/gateway.env +ExecStartPre=/usr/bin/openshell-gateway generate-certs --output-dir %S/openshell/tls --server-san host.openshell.internal ExecStart=/usr/bin/openshell-gateway Restart=on-failure RestartSec=5s diff --git a/deploy/docker/Dockerfile.gateway-macos b/deploy/docker/Dockerfile.gateway-macos index 4cae2f0e7..27d2ffbba 100644 --- a/deploy/docker/Dockerfile.gateway-macos +++ b/deploy/docker/Dockerfile.gateway-macos @@ -94,6 +94,7 @@ RUN touch crates/openshell-core/src/lib.rs \ proto/*.proto ARG OPENSHELL_CARGO_VERSION +ARG OPENSHELL_IMAGE_TAG RUN --mount=type=cache,id=cargo-registry-gateway-macos,sharing=locked,target=/root/.cargo/registry \ --mount=type=cache,id=cargo-git-gateway-macos,sharing=locked,target=/root/.cargo/git \ --mount=type=cache,id=cargo-target-gateway-macos-${CARGO_TARGET_CACHE_SCOPE},sharing=locked,target=/build/target \ diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 378439758..25bf96480 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -38,6 +38,8 @@ For detailed driver behavior, refer to [Sandbox Compute Drivers](/reference/sand On macOS, the install script uses Homebrew. The Homebrew package installs the `openshell` CLI, the gateway binary, and a Homebrew-managed gateway service. +The Homebrew service listens on `https://127.0.0.1:17670` and generates a local mTLS bundle on install. The CLI reads the client bundle from `~/.config/openshell/gateways/openshell/mtls/`. + The installer starts the service for you. Use Homebrew service commands when you need to inspect, restart, or stop the gateway service: ```shell @@ -51,6 +53,8 @@ On Fedora and RHEL, the install script uses RPM packages. The RPM installs the ` On Debian and Ubuntu, the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. +The Debian user service listens on `https://127.0.0.1:17670` and generates a local mTLS bundle before the gateway starts. The CLI reads the client bundle from `~/.config/openshell/gateways/openshell/mtls/`. + The installer starts the service for you. Use systemd user commands when you need to inspect, restart, or stop the gateway service: ```shell diff --git a/docs/reference/gateway-auth.mdx b/docs/reference/gateway-auth.mdx index bd2f27f4e..e95ce854b 100644 --- a/docs/reference/gateway-auth.mdx +++ b/docs/reference/gateway-auth.mdx @@ -38,6 +38,10 @@ Set these environment variables before starting the gateway: For local access, the server certificate must be valid for the endpoint the CLI uses. Include `localhost` and `127.0.0.1` in the certificate SANs when users connect to a local gateway through loopback. +Package-managed local gateways on Homebrew and Debian generate this bundle automatically for the `openshell` gateway name and use `https://127.0.0.1:17670` by default. +When you register a package-managed local gateway with `openshell gateway add https://127.0.0.1:17670 --local --name openshell`, the CLI refreshes its mTLS bundle from the package-managed TLS directory. +On Homebrew, the gateway service also mirrors the Docker sandbox client bundle into `$HOME/.local/state/openshell/homebrew/tls` before startup so Docker Desktop can bind-mount the files into sandbox containers. + The CLI loads its mTLS bundle from `~/.config/openshell/gateways//mtls/`: | File | Purpose | diff --git a/install-dev.sh b/install-dev.sh index 87234a4eb..660795949 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -420,7 +420,7 @@ start_user_gateway() { if ! as_target_user systemctl --user daemon-reload; then info "could not reach the user systemd manager for ${TARGET_USER}" info "restart the gateway later with: systemctl --user enable openshell-gateway && systemctl --user restart openshell-gateway" - info "then register it with: openshell gateway add http://127.0.0.1:17670 --local --name local" + info "then register it with: openshell gateway add https://127.0.0.1:17670 --local --name openshell" return 0 fi @@ -438,11 +438,14 @@ wait_for_local_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}/" + _mtls_dir="${TARGET_HOME}/.config/openshell/gateways/openshell/mtls" info "waiting for local gateway listener to become reachable..." while [ "$_elapsed" -lt "$_timeout" ]; do - if _last_output="$(as_target_user curl -sS --max-time 2 -o /dev/null "$_probe_url" 2>&1)"; then + if [ ! -f "${_mtls_dir}/ca.crt" ] || [ ! -f "${_mtls_dir}/tls.crt" ] || [ ! -f "${_mtls_dir}/tls.key" ]; then + _last_output="mTLS client bundle is not ready under ${_mtls_dir}" + elif _last_output="$(as_target_user curl -sS --max-time 2 --cacert "${_mtls_dir}/ca.crt" --cert "${_mtls_dir}/tls.crt" --key "${_mtls_dir}/tls.key" -o /dev/null "$_probe_url" 2>&1)"; then info "local gateway listener is reachable" return 0 fi @@ -488,8 +491,15 @@ remove_local_gateway_registration() { as_target_user sh -c ' config_dir=$1 rm -rf "${config_dir}/gateways/local" + mkdir -p "${config_dir}/gateways/openshell" + rm -f \ + "${config_dir}/gateways/openshell/metadata.json" \ + "${config_dir}/gateways/openshell/edge_token" \ + "${config_dir}/gateways/openshell/cf_token" \ + "${config_dir}/gateways/openshell/oidc_token.json" active="${config_dir}/active_gateway" - if [ "$(cat "$active" 2>/dev/null || true)" = "local" ]; then + active_name="$(cat "$active" 2>/dev/null || true)" + if [ "$active_name" = "local" ] || [ "$active_name" = "openshell" ]; then rm -f "$active" fi ' sh "$_config_dir" @@ -498,7 +508,7 @@ remove_local_gateway_registration() { register_local_gateway() { _register_bin="${OPENSHELL_REGISTER_BIN:-openshell}" - if _add_output="$(as_target_user "$_register_bin" gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name local 2>&1)"; then + if _add_output="$(as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell 2>&1)"; then [ -z "$_add_output" ] || print_gateway_add_output "$_add_output" return 0 else @@ -509,7 +519,7 @@ register_local_gateway() { *"already exists"*) info "local gateway already exists; removing and re-adding it..." remove_local_gateway_registration - as_target_user "$_register_bin" gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name local + as_target_user "$_register_bin" gateway add "https://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell ;; *) printf '%s\n' "$_add_output" >&2 @@ -521,7 +531,7 @@ register_local_gateway() { print_gateway_add_output() { printf '%s\n' "$1" | while IFS= read -r _line; do case "$_line" in - *"Gateway is not reachable at http://127.0.0.1:${LOCAL_GATEWAY_PORT}"*) ;; + *"Gateway is not reachable at https://127.0.0.1:${LOCAL_GATEWAY_PORT}"*) ;; *"Verify the gateway is running and the endpoint is correct."*) ;; *) printf '%s\n' "$_line" >&2 ;; esac @@ -654,7 +664,7 @@ install_macos_homebrew() { if ! as_target_user brew services restart "$_formula_ref"; then warn "could not restart the OpenShell Homebrew service" info "restart it later with: brew services restart ${_formula_ref}" - info "then register it with: openshell gateway add http://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name local" + info "then register it with: openshell gateway add https://127.0.0.1:${LOCAL_GATEWAY_PORT} --local --name openshell" return 0 fi diff --git a/mise.lock b/mise.lock index 6ab204f6e..5110b03c9 100644 --- a/mise.lock +++ b/mise.lock @@ -287,6 +287,7 @@ version = "2.19.0" backend = "aqua:GoogleContainerTools/skaffold" [tools.skaffold."platforms.linux-arm64"] +checksum = "blake3:c62b62077ac47abb7f7d184836d37f467c8e82b47e47b5dce570e15db4bb30fe" url = "https://storage.googleapis.com/skaffold/releases/v2.19.0/skaffold-linux-arm64" [tools.skaffold."platforms.linux-arm64-musl"] diff --git a/python/openshell/release_formula_test.py b/python/openshell/release_formula_test.py index 56abd7af6..8eb02cada 100644 --- a/python/openshell/release_formula_test.py +++ b/python/openshell/release_formula_test.py @@ -53,5 +53,14 @@ def test_generate_homebrew_formula_uses_tagged_macos_driver_asset_without_defaul assert 'sha256 "' + "b" * 64 + '"' in formula assert "OPENSHELL_DRIVERS" not in formula assert 'OPENSHELL_DRIVER_DIR: "#{opt_libexec}"' in formula + assert ( + 'OPENSHELL_DOCKER_SUPERVISOR_IMAGE: "ghcr.io/nvidia/openshell/supervisor:0.0.10"' + ) in formula + assert 'run opt_libexec/"openshell-gateway-homebrew-service"' in formula + assert ( + 'docker_tls_dir="${OPENSHELL_DOCKER_TLS_DIR:-${HOME}/.local/state/openshell/homebrew/tls}"' + ) in formula + assert 'export OPENSHELL_DOCKER_TLS_CA="${docker_tls_dir}/ca.crt"' in formula + assert 'OPENSHELL_DOCKER_TLS_CA: "#{var}/openshell/tls/ca.crt"' not in formula assert "entitlements.atomic_write" in formula assert "brew services restart openshell" in formula diff --git a/tasks/scripts/package-deb-install.sh b/tasks/scripts/package-deb-install.sh index 735ce70a9..b6e473067 100755 --- a/tasks/scripts/package-deb-install.sh +++ b/tasks/scripts/package-deb-install.sh @@ -28,8 +28,8 @@ cd "$repo_root" VERSION="${OPENSHELL_DEB_VERSION:-0.0.0-local}" OUTPUT_DIR="${OPENSHELL_OUTPUT_DIR:-artifacts}" ARCH="${OPENSHELL_DEB_ARCH:-$(dpkg --print-architecture 2>/dev/null || uname -m)}" -GATEWAY_NAME="local" -GATEWAY_ENDPOINT="http://127.0.0.1:17670" +GATEWAY_NAME="openshell" +GATEWAY_ENDPOINT="https://127.0.0.1:17670" remove_existing_gateway_registration() { local config_home="${XDG_CONFIG_HOME:-${HOME}/.config}" diff --git a/tasks/scripts/release.py b/tasks/scripts/release.py index 0a6893121..cdb525e7b 100644 --- a/tasks/scripts/release.py +++ b/tasks/scripts/release.py @@ -214,6 +214,11 @@ def _asset_url(release_tag: str, filename: str) -> str: return f"{GITHUB_RELEASE_DOWNLOADS}/{release_tag}/{filename}" +def _homebrew_supervisor_image(release_tag: str) -> str: + image_tag = "dev" if release_tag == "dev" else release_tag.removeprefix("v") + return f"ghcr.io/nvidia/openshell/supervisor:{image_tag}" + + def render_homebrew_formula( *, release_tag: str, @@ -225,6 +230,7 @@ def render_homebrew_formula( raise ValueError(f"release tag contains unsupported characters: {release_tag}") version = release_tag.removeprefix("v") + docker_supervisor_image = _homebrew_supervisor_image(release_tag) return f"""# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -263,12 +269,37 @@ def install resource("openshell-driver-vm").stage do libexec.install "openshell-driver-vm" end + + (libexec/"openshell-gateway-homebrew-service").write <<~SH + #!/bin/sh + set -eu + + if [ -z "${{HOME:-}}" ]; then + echo "HOME must be set for Docker TLS bind mounts" >&2 + exit 1 + fi + + docker_tls_dir="${{OPENSHELL_DOCKER_TLS_DIR:-${{HOME}}/.local/state/openshell/homebrew/tls}}" + mkdir -p "${{docker_tls_dir}}/client" + chmod 700 "${{docker_tls_dir}}" "${{docker_tls_dir}}/client" + /usr/bin/install -m 0644 "#{{var}}/openshell/tls/ca.crt" "${{docker_tls_dir}}/ca.crt" + /usr/bin/install -m 0644 "#{{var}}/openshell/tls/client/tls.crt" "${{docker_tls_dir}}/client/tls.crt" + /usr/bin/install -m 0600 "#{{var}}/openshell/tls/client/tls.key" "${{docker_tls_dir}}/client/tls.key" + + export OPENSHELL_DOCKER_TLS_CA="${{docker_tls_dir}}/ca.crt" + export OPENSHELL_DOCKER_TLS_CERT="${{docker_tls_dir}}/client/tls.crt" + export OPENSHELL_DOCKER_TLS_KEY="${{docker_tls_dir}}/client/tls.key" + + exec "#{{opt_bin}}/openshell-gateway" + SH + chmod 0755, libexec/"openshell-gateway-homebrew-service" end def post_install (var/"openshell/gateway").mkpath (var/"openshell/vm-driver").mkpath (var/"log/openshell").mkpath + system bin/"openshell-gateway", "generate-certs", "--output-dir", var/"openshell/tls", "--server-san", "host.openshell.internal" entitlements = var/"openshell/openshell-driver-vm.entitlements.plist" entitlements.atomic_write <<~XML @@ -286,17 +317,25 @@ def post_install end service do - run opt_bin/"openshell-gateway" + run opt_libexec/"openshell-gateway-homebrew-service" environment_variables( OPENSHELL_BIND_ADDRESS: "127.0.0.1", OPENSHELL_SERVER_PORT: "{LOCAL_GATEWAY_PORT}", - OPENSHELL_DISABLE_TLS: "true", - OPENSHELL_DISABLE_GATEWAY_AUTH: "true", + OPENSHELL_TLS_CERT: "#{{var}}/openshell/tls/server/tls.crt", + OPENSHELL_TLS_KEY: "#{{var}}/openshell/tls/server/tls.key", + OPENSHELL_TLS_CLIENT_CA: "#{{var}}/openshell/tls/ca.crt", OPENSHELL_DB_URL: "sqlite:#{{var}}/openshell/gateway/openshell.db", - OPENSHELL_GRPC_ENDPOINT: "http://127.0.0.1:{LOCAL_GATEWAY_PORT}", + OPENSHELL_GRPC_ENDPOINT: "https://127.0.0.1:{LOCAL_GATEWAY_PORT}", OPENSHELL_SSH_GATEWAY_HOST: "127.0.0.1", OPENSHELL_SSH_GATEWAY_PORT: "{LOCAL_GATEWAY_PORT}", OPENSHELL_VM_DRIVER_STATE_DIR: "#{{var}}/openshell/vm-driver", + OPENSHELL_VM_TLS_CA: "#{{var}}/openshell/tls/ca.crt", + OPENSHELL_VM_TLS_CERT: "#{{var}}/openshell/tls/client/tls.crt", + OPENSHELL_VM_TLS_KEY: "#{{var}}/openshell/tls/client/tls.key", + OPENSHELL_DOCKER_SUPERVISOR_IMAGE: "{docker_supervisor_image}", + OPENSHELL_PODMAN_TLS_CA: "#{{var}}/openshell/tls/ca.crt", + OPENSHELL_PODMAN_TLS_CERT: "#{{var}}/openshell/tls/client/tls.crt", + OPENSHELL_PODMAN_TLS_KEY: "#{{var}}/openshell/tls/client/tls.key", OPENSHELL_DRIVER_DIR: "#{{opt_libexec}}", ) keep_alive successful_exit: false @@ -310,7 +349,7 @@ def caveats brew services restart openshell Register it with the OpenShell CLI: - openshell gateway add http://127.0.0.1:{LOCAL_GATEWAY_PORT} --local --name local + openshell gateway add https://127.0.0.1:{LOCAL_GATEWAY_PORT} --local --name openshell EOS end