Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .packit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@ actions:
- 'bash -c "echo openshell-${PACKIT_PROJECT_VERSION}.tar.gz"'

fix-spec-file:
# Update Source0 to the generated tarball name
- 'bash -c "sed -i \"s|^Source0:.*|Source0: openshell-${PACKIT_PROJECT_VERSION}.tar.gz|\" openshell.spec"'
# Update Source1 to the generated vendor tarball name
- 'bash -c "sed -i \"s|^Source1:.*|Source1: openshell-${PACKIT_PROJECT_VERSION}-vendor.tar.xz|\" openshell.spec"'
# Update Version
- 'bash -c "sed -i -r \"s/^Version:(\\s*)\\S+/Version:\\1${PACKIT_RPMSPEC_VERSION}/\" openshell.spec"'
# Update the canonical version macro. Version:, Source0:, Source1:, and all
# other version references expand from %{openshell_version} so only this
# one line needs updating.
- 'bash -c "sed -i -r \"s/^%global openshell_version .*/%global openshell_version ${PACKIT_RPMSPEC_VERSION}/\" openshell.spec"'
# Update Release
- 'bash -c "RELEASE=${OPENSHELL_RPM_RELEASE:-${PACKIT_RPMSPEC_RELEASE}} && sed -i -r \"s/^Release:(\\s*)\\S+/Release:\\1${RELEASE}%{?dist}/\" openshell.spec"'
# Keep embedded binary metadata aligned with the release workflow. Python
Expand Down
43 changes: 43 additions & 0 deletions crates/openshell-server/src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,4 +515,47 @@ version = 2
.expect_err("missing file must be io error");
assert!(matches!(err, ConfigFileError::Io { .. }));
}

/// Contract test: the RPM default config template must parse against the
/// current schema and must pin the settings that Podman deployments require.
///
/// This test loads `deploy/rpm/gateway.toml.default` through the same
/// `load()` path that the gateway uses at runtime, catching:
/// - template corruption or unknown fields (`deny_unknown_fields`)
/// - schema drift (version bump or field renames)
/// - accidental changes to the bind address or compute driver list
#[test]
fn rpm_default_config_parses_and_has_podman_defaults() {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../deploy/rpm/gateway.toml.default");
let config =
load(&path).expect("deploy/rpm/gateway.toml.default must parse against current schema");
let gw = &config.openshell.gateway;

let addr = gw
.bind_address
.expect("bind_address must be explicitly set in the RPM default config");
assert!(
addr.ip().is_unspecified(),
"RPM default bind_address must be 0.0.0.0 so Podman sandbox containers \
can reach the gateway over the host network bridge, got {addr}"
);
assert_eq!(
addr.port(),
openshell_core::config::DEFAULT_SERVER_PORT,
"RPM default port must match DEFAULT_SERVER_PORT ({})",
openshell_core::config::DEFAULT_SERVER_PORT
);

let drivers = gw
.compute_drivers
.as_ref()
.expect("compute_drivers must be explicitly set in the RPM default config");
assert_eq!(
drivers,
&[ComputeDriverKind::Podman],
"RPM default must pin compute_drivers to [podman] to prevent unexpected \
driver selection when Docker is also installed"
);
}
}
69 changes: 62 additions & 7 deletions deploy/rpm/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,65 @@ the RPM package on Fedora and RHEL systems.
For first-time setup, see QUICKSTART.md. For troubleshooting, see
TROUBLESHOOTING.md.

## Default configuration

The RPM ships a default TOML configuration template at
`/usr/share/openshell-gateway/gateway.toml.default`. On first start of
`openshell-gateway.service`, the systemd unit copies this template to
`~/.config/openshell/gateway.toml` if no config file exists there yet.

The defaults are tuned for rootless Podman use:

```toml
[openshell]
version = 1

[openshell.gateway]
bind_address = "0.0.0.0:17670"
compute_drivers = ["podman"]
```

`bind_address = "0.0.0.0:17670"` is required because Podman sandbox
containers reach the gateway over the host network bridge and cannot
connect to `127.0.0.1` inside the gateway's network namespace. mTLS is
enabled by default and protects all connections.

`compute_drivers = ["podman"]` pins the compute driver to Podman. Without
this, the gateway auto-detects in order: Kubernetes, Podman, Docker. Pinning
prevents unexpected driver selection if Docker is also installed on the host.

### Customizing the configuration

Edit `~/.config/openshell/gateway.toml` directly. The template at
`/usr/share/openshell-gateway/gateway.toml.default` is not read at runtime
and is not overwritten by RPM upgrades.

To apply environment variable overrides that persist across upgrades without
editing the TOML file, add them to `~/.config/openshell/gateway.env`:

```shell
# Example: restrict to loopback only
OPENSHELL_BIND_ADDRESS=127.0.0.1
```

To override the path to the TOML config file entirely:

```shell
# In ~/.config/openshell/gateway.env
OPENSHELL_GATEWAY_CONFIG=/path/to/custom/gateway.toml
```

For one-off service overrides that persist across package upgrades:

```shell
systemctl --user edit openshell-gateway
```

## TLS (mTLS)

The RPM enables mutual TLS by default. The gateway requires a valid
client certificate for all API connections and listens on
`127.0.0.1:17670` by default.
`0.0.0.0:17670` by default (see "Default configuration" above).

### Auto-generated certificates

Expand Down Expand Up @@ -152,8 +206,8 @@ overrides that persist across package upgrades.

| TOML option | Default | Description |
|-------------|---------|-------------|
| `bind_address` | `127.0.0.1:17670` | Address for the gRPC/HTTP API. |
| `compute_drivers` | unset | When unset, the gateway auto-detects Kubernetes, then Podman, then Docker. Set `compute_drivers = ["podman"]` to force Podman. |
| `bind_address` | `0.0.0.0:17670` (RPM default) | Address for the gRPC/HTTP API. |
| `compute_drivers` | `["podman"]` (RPM default) | When unset, the gateway auto-detects Kubernetes, then Podman, then Docker. The RPM default pins to Podman. |
| `default_image` | `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` | Default sandbox image. |
| `supervisor_image` | `ghcr.io/nvidia/openshell/supervisor:latest` | Supervisor image mounted into Podman sandboxes. |
| `guest_tls_ca`, `guest_tls_cert`, `guest_tls_key` | auto-generated paths | Client TLS material bind-mounted into sandbox containers. |
Expand All @@ -173,9 +227,8 @@ settings:
version = 1

[openshell.gateway]
bind_address = "127.0.0.1:17670"
# Leave unset to auto-detect the compute driver.
# compute_drivers = ["podman"]
bind_address = "0.0.0.0:17670"
compute_drivers = ["podman"]
default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"

[openshell.drivers.podman]
Expand Down Expand Up @@ -239,7 +292,9 @@ For air-gapped environments:
| Gateway binary | `/usr/bin/openshell-gateway` |
| CLI binary | `/usr/bin/openshell` |
| Systemd user unit | `/usr/lib/systemd/user/openshell-gateway.service` |
| Default TOML config template (read-only) | `/usr/share/openshell-gateway/gateway.toml.default` |
| Active gateway TOML configuration | `~/.config/openshell/gateway.toml` |
| Optional environment variable overrides | `~/.config/openshell/gateway.env` |
| TLS certificates | `~/.local/state/openshell/tls/` |
| CLI client certs | `~/.config/openshell/gateways/openshell/mtls/` |
| Gateway database | `~/.local/state/openshell/gateway/openshell.db` |
| Optional gateway TOML configuration | `~/.config/openshell/gateway.toml` |
9 changes: 5 additions & 4 deletions deploy/rpm/QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ On first start, the gateway automatically generates:

- A self-signed PKI bundle (CA, server cert, client cert) for mTLS

> **Note:** The gateway binds to `127.0.0.1:17670` by default. Mutual
> TLS (mTLS) is enabled automatically on first start, requiring a valid
> client certificate for every connection. See CONFIGURATION.md for
> details.
> **Note:** The RPM default configuration binds to `0.0.0.0:17670` so
> Podman sandbox containers can reach the gateway over the host network
> bridge. Mutual TLS (mTLS) is enabled automatically on first start,
> requiring a valid client certificate for every connection. See
> CONFIGURATION.md for details.

Verify the service is running:

Expand Down
30 changes: 30 additions & 0 deletions deploy/rpm/gateway.toml.default
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Default gateway configuration for RPM installs.
#
# This file is seeded to ~/.config/openshell/gateway.toml on first start
# of the openshell-gateway.service systemd user unit. Edit that copy to
# customize. This file is not read directly at runtime.
#
# Configuration precedence (highest to lowest):
# CLI flag > OPENSHELL_* env var > TOML file > built-in default
#
# To override settings without editing this file, set OPENSHELL_* variables
# in ~/.config/openshell/gateway.env or run:
# systemctl --user edit openshell-gateway

[openshell]
version = 1

[openshell.gateway]
# Podman sandbox containers reach the gateway over the host network bridge,
# which requires binding to all interfaces. Override to 127.0.0.1:17670 if
# you don't use Podman or want loopback-only access (e.g. behind a reverse
# proxy). mTLS is enabled by default and protects all connections.
bind_address = "0.0.0.0:17670"

# Pin to the Podman compute driver. Without this, the gateway auto-detects
# in order: Kubernetes, Podman, Docker. Pinning prevents unexpected driver
# selection if Docker is also installed on the host.
compute_drivers = ["podman"]
20 changes: 14 additions & 6 deletions e2e/with-podman-gateway.sh
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,18 @@ toml_string() {
}

GATEWAY_CONFIG="${STATE_DIR}/gateway.toml"

# Start from the RPM default template so this e2e test exercises the same
# TOML config path that RPM users get on first start. The template sets
# bind_address = "0.0.0.0:17670" and compute_drivers = ["podman"]; those
# values must be correct for Podman e2e to pass, which means a regression
# to the template (wrong bind address, wrong driver) will surface here.
#
# We append the driver-specific table and override the port via CLI flag
# (CLI > TOML in the merge precedence) so the test can use an ephemeral port.
cp "${ROOT}/deploy/rpm/gateway.toml.default" "${GATEWAY_CONFIG}"
{
printf '[openshell]\nversion = 1\n\n'
printf '[openshell.gateway]\nlog_level = "info"\n\n'
printf '[openshell.drivers.podman]\n'
printf '\n[openshell.drivers.podman]\n'
# The Podman driver scopes isolation by network rather than namespace.
printf 'network_name = %s\n' "$(toml_string "${PODMAN_NETWORK_NAME}")"
printf 'gateway_port = %s\n' "${HOST_PORT}"
Expand All @@ -380,14 +388,14 @@ GATEWAY_CONFIG="${STATE_DIR}/gateway.toml"
if [ -n "${OPENSHELL_PODMAN_SOCKET:-}" ]; then
printf 'socket_path = %s\n' "$(toml_string "${OPENSHELL_PODMAN_SOCKET}")"
fi
} > "${GATEWAY_CONFIG}"
} >> "${GATEWAY_CONFIG}"

GATEWAY_ARGS=(
--config "${GATEWAY_CONFIG}"
--bind-address 0.0.0.0
# bind_address and compute_drivers come from the RPM template; no CLI flags
# needed. Port is overridden via CLI (CLI > TOML) for ephemeral port selection.
--port "${HOST_PORT}"
--health-port "${HEALTH_PORT}"
--drivers podman
--tls-cert "${PKI_DIR}/server/tls.crt"
--tls-key "${PKI_DIR}/server/tls.key"
--tls-client-ca "${PKI_DIR}/ca.crt"
Expand Down
41 changes: 31 additions & 10 deletions openshell.spec
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
# SPDX-License-Identifier: Apache-2.0

%global crate openshell
%global openshell_cargo_version %{version}
%global openshell_version 0.0.37
%global openshell_cargo_version %{openshell_version}
# Python dist-info metadata intentionally follows the RPM Version. Dev build
# identity is represented by Release for RPM packages.
%global openshell_python_version %{version}
%global openshell_python_version %{openshell_version}

# Cargo/Rust builds with vendored deps do not produce debugsource listings
# in the format redhat-rpm-config expects (especially on EPEL).
Expand All @@ -18,14 +19,14 @@
%global image_tag dev

Name: openshell
Version: 0.0.37
Release: 1.20260506170246815148.rpm.dev.106.g99e94469%{?dist}
Version: %{openshell_version}
Release: 1.20260518180028805757.podman.toml.gateway.listener.11.g8c0cb7c8%{?dist}
Summary: Safe, sandboxed runtimes for autonomous AI agents

License: Apache-2.0
URL: https://github.com/NVIDIA/OpenShell
Source0: openshell-0.0.37.tar.gz
Source1: openshell-0.0.37-vendor.tar.xz
Source0: openshell-%{openshell_version}.tar.gz
Source1: openshell-%{openshell_version}-vendor.tar.xz

ExclusiveArch: x86_64 aarch64

Expand Down Expand Up @@ -127,6 +128,11 @@ install -Dpm 0755 target/release/%{name} %{buildroot}%{_bindir}/%{name}
# --- Gateway binary ---
install -Dpm 0755 target/release/%{name}-gateway %{buildroot}%{_bindir}/%{name}-gateway

# --- Default gateway TOML config template ---
# Shipped as a read-only reference in %{_datadir}. The systemd unit seeds a
# user-level copy at ~/.config/openshell/gateway.toml on first start.
install -Dpm 0644 deploy/rpm/gateway.toml.default %{buildroot}%{_datadir}/%{name}-gateway/gateway.toml.default

# --- Gateway systemd user unit ---
# Installed to the systemd user unit directory so any user can run:
# systemctl --user enable --now openshell-gateway.service
Expand All @@ -140,12 +146,17 @@ Wants=podman.socket

[Service]
Type=exec
# PKI is auto-generated on first start. Client certs are placed in
# ~/.config/openshell/gateways/openshell/mtls/ so the CLI discovers them
# automatically. Gateway runtime defaults are used unless a TOML config
# exists in the default user config location or OPENSHELL_GATEWAY_CONFIG is set.
# On first start the unit seeds a default TOML config and generates PKI.
# Client certs are placed in ~/.config/openshell/gateways/openshell/mtls/ so
# the CLI discovers them automatically.
# See /usr/share/doc/openshell-gateway/ for details.

# Seed a default TOML config on first start if the user has not created one.
# The template ships at /usr/share/openshell-gateway/gateway.toml.default.
# Edit ~/.config/openshell/gateway.toml to customize.
# %%E expands to $XDG_CONFIG_HOME (~/.config) in user units.
ExecStartPre=/bin/sh -c 'test -f %%E/openshell/gateway.toml || install -Dm644 /usr/share/openshell-gateway/gateway.toml.default %%E/openshell/gateway.toml'

# Auto-generate PKI on first start if not present.
# %%S expands to $XDG_STATE_HOME (~/.local/state) in user units.
ExecStartPre=/usr/bin/openshell-gateway generate-certs --output-dir %%S/openshell/tls --server-san host.openshell.internal
Expand Down Expand Up @@ -220,6 +231,15 @@ touch %{buildroot}%{python3_sitelib}/%{name}-%{openshell_python_version}.dist-in
# build environment.
PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata import version; v = version('openshell'); print(v); assert v == '%{openshell_python_version}', f'expected %{openshell_python_version}, got {v}'"

# Verify the RPM default TOML config template was installed.
# A missing template means first-start seeding silently falls back to the
# binary default of 127.0.0.1, which breaks Podman sandbox connectivity.
test -f %{buildroot}%{_datadir}/%{name}-gateway/gateway.toml.default

# Verify the systemd unit references the template in its ExecStartPre seed step.
# If this grep fails, the first-start seeding logic was removed from the unit.
grep -q 'gateway.toml.default' %{buildroot}%{_userunitdir}/%{name}-gateway.service

%post gateway
%systemd_user_post %{name}-gateway.service

Expand All @@ -246,6 +266,7 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata
%doc %{_docdir}/%{name}-gateway/TROUBLESHOOTING.md
%{_bindir}/%{name}-gateway
%{_userunitdir}/%{name}-gateway.service
%{_datadir}/%{name}-gateway/gateway.toml.default
%{_mandir}/man8/openshell-gateway.8*

%files -n python3-%{name}
Expand Down
Loading