mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
fix(packaging): enable mTLS for local packages (#1271)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/ \
|
||||
|
||||
@@ -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/ \
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user