fix(packaging): enable mTLS for local packages (#1271)

This commit is contained in:
Drew Newberry
2026-05-08 13:02:12 -07:00
committed by GitHub
parent 31f03456a9
commit daa2a362d5
17 changed files with 308 additions and 57 deletions
+1 -1
View File
@@ -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
+1
View File
@@ -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/ \
+1
View File
@@ -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/ \
+7
View File
@@ -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.
+134 -24
View File
@@ -643,6 +643,60 @@ fn mtls_certs_exist_for_endpoint(name: &str, endpoint: &str) -> bool {
})
}
fn package_managed_tls_dirs() -> Vec<PathBuf> {
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<Option<PathBuf>> {
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<Option<Stat
Ok(Some(resp.status()))
}
async fn gateway_reachable(server: &str, tls: &TlsOptions) -> 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(
+6 -4
View File
@@ -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
@@ -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");
+45 -10
View File
@@ -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<PkiBundle> {
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<String> {
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() {
+14 -3
View File
@@ -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
+1
View File
@@ -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 \
+4
View File
@@ -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
+4
View File
@@ -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/<name>/mtls/`:
| File | Purpose |
+18 -8
View File
@@ -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
+1
View File
@@ -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"]
+9
View File
@@ -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
+2 -2
View File
@@ -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}"
+44 -5
View File
@@ -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 @@ class Openshell < Formula
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 @@ class Openshell < Formula
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 @@ class Openshell < Formula
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