From 267fe245f841829245dd5912d48cff247431c7f9 Mon Sep 17 00:00:00 2001 From: Taylor Mutch Date: Fri, 8 May 2026 10:02:45 -0700 Subject: [PATCH] feat(rpm): replace init-pki.sh with openshell-gateway generate-certs Cuts the RPM gateway over to the unified Rust certgen path. The systemd user unit's first ExecStartPre now invokes: /usr/bin/openshell-gateway generate-certs --output-dir %S/openshell/tls producing the same six-PEM layout init-pki.sh built (ca.{crt,key}, server/tls.{crt,key}, client/tls.{crt,key}) and the same CLI mTLS copy under $XDG_CONFIG_HOME/openshell/gateways/openshell/mtls/. None of the OPENSHELL_TLS_* / OPENSHELL_PODMAN_TLS_* paths in the unit change. Adds host.containers.internal to the gateway's built-in SAN list so podman containers reaching their host validate cleanly with no per-deployment --server-san flag. Docker (host.docker.internal) and Kubernetes (cluster.local DNS) were already covered. Drops 197 lines of openssl shell, the install/file lines for the script itself, and updates the docs (man page, RPM CONFIGURATION.md, env-file generator comment) to point at the new entrypoint. The %S state dir, unit security hardening, and consumer paths are untouched. --- crates/openshell-bootstrap/src/pki.rs | 6 +- deploy/man/openshell-gateway.8.md | 10 +- deploy/rpm/CONFIGURATION.md | 8 +- deploy/rpm/init-gateway-env.sh | 7 +- deploy/rpm/init-pki.sh | 197 -------------------------- openshell.spec | 11 +- 6 files changed, 23 insertions(+), 216 deletions(-) delete mode 100755 deploy/rpm/init-pki.sh diff --git a/crates/openshell-bootstrap/src/pki.rs b/crates/openshell-bootstrap/src/pki.rs index f3a30211e..289f9827d 100644 --- a/crates/openshell-bootstrap/src/pki.rs +++ b/crates/openshell-bootstrap/src/pki.rs @@ -17,13 +17,17 @@ pub struct PkiBundle { pub client_key_pem: String, } -/// Default SANs always included on the server certificate. +/// Default SANs always included on the server certificate. Covers the host +/// aliases used by every supported runtime: Kubernetes service DNS, +/// `host.docker.internal` for Docker Desktop and rootless Docker on Linux, +/// and `host.containers.internal` for Podman containers reaching their host. const DEFAULT_SERVER_SANS: &[&str] = &[ "openshell", "openshell.openshell.svc", "openshell.openshell.svc.cluster.local", "localhost", "host.docker.internal", + "host.containers.internal", "127.0.0.1", ]; diff --git a/deploy/man/openshell-gateway.8.md b/deploy/man/openshell-gateway.8.md index 5e3c4eef2..7b248f9af 100644 --- a/deploy/man/openshell-gateway.8.md +++ b/deploy/man/openshell-gateway.8.md @@ -134,13 +134,14 @@ View logs: journalctl --user -u openshell-gateway journalctl --user -u openshell-gateway -f -The unit runs two **ExecStartPre** scripts on first start: +The unit runs two **ExecStartPre** steps on first start: -1. **init-pki.sh** generates a self-signed PKI bundle for mTLS. +1. **openshell-gateway generate-certs --output-dir** generates a + self-signed PKI bundle for mTLS. 2. **init-gateway-env.sh** generates the environment configuration file with an auto-generated SSH handshake secret. -Both scripts are idempotent and skip generation if their output files +Both steps are idempotent and skip generation if their output files already exist. To persist the service across logouts: @@ -167,9 +168,6 @@ This creates a drop-in override that persists across package upgrades. */usr/lib/systemd/user/openshell-gateway.service* : Systemd user unit file. -*/usr/libexec/openshell/init-pki.sh* -: PKI bootstrap script. - */usr/libexec/openshell/init-gateway-env.sh* : Gateway environment file generator. diff --git a/deploy/rpm/CONFIGURATION.md b/deploy/rpm/CONFIGURATION.md index 04caaaae5..24d5ad2d1 100644 --- a/deploy/rpm/CONFIGURATION.md +++ b/deploy/rpm/CONFIGURATION.md @@ -14,8 +14,10 @@ though it listens on all interfaces (`0.0.0.0`). ### Auto-generated certificates -On first start, the `init-pki.sh` script generates certificates using -OpenSSL: +On first start, the gateway's `ExecStartPre` runs +`openshell-gateway generate-certs --output-dir /openshell/tls`, +which generates the certificates with `rcgen` (the same routine the CLI +uses for local mTLS bundles): | File | Purpose | Location | |------|---------|----------| @@ -245,7 +247,7 @@ 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` | -| PKI bootstrap script | `/usr/libexec/openshell/init-pki.sh` | +| PKI bootstrap | `openshell-gateway generate-certs` (run from `ExecStartPre`) | | Env generator script | `/usr/libexec/openshell/init-gateway-env.sh` | | TLS certificates | `~/.local/state/openshell/tls/` | | CLI client certs | `~/.config/openshell/gateways/openshell/mtls/` | diff --git a/deploy/rpm/init-gateway-env.sh b/deploy/rpm/init-gateway-env.sh index 299a19041..371eae0fc 100644 --- a/deploy/rpm/init-gateway-env.sh +++ b/deploy/rpm/init-gateway-env.sh @@ -77,9 +77,10 @@ OPENSHELL_SSH_HANDSHAKE_SECRET=${SECRET} #OPENSHELL_SANDBOX_IMAGE_PULL_POLICY=missing # ---- TLS (mTLS enabled by default) ---- -# PKI is auto-generated by init-pki.sh on first start. Client certs are -# placed in ~/.config/openshell/gateways/openshell/mtls/ so the CLI -# discovers them automatically. +# PKI is auto-generated by 'openshell-gateway generate-certs' from the +# unit's ExecStartPre on first start. Client certs are placed in +# ~/.config/openshell/gateways/openshell/mtls/ so the CLI discovers them +# automatically. # # To use externally-managed certs, uncomment and edit the paths below. # To rotate certs, delete ~/.local/state/openshell/tls/ and restart. diff --git a/deploy/rpm/init-pki.sh b/deploy/rpm/init-pki.sh deleted file mode 100755 index 900ec20a6..000000000 --- a/deploy/rpm/init-pki.sh +++ /dev/null @@ -1,197 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -# Generate a self-signed PKI bundle for the OpenShell gateway. -# -# Called from the systemd ExecStartPre directive to bootstrap mTLS on -# first start. Idempotent: exits immediately if all cert files exist. -# Detects and recovers from partial PKI state (e.g. interrupted runs). -# -# All files are generated in a temporary staging directory first and -# moved into place only after the full PKI is complete, preventing -# partial state from persisting across failures. -# -# Usage: -# init-pki.sh -# -# Output layout: -# /ca.crt CA certificate -# /ca.key CA private key (mode 0600) -# /server/tls.crt Server certificate -# /server/tls.key Server private key (mode 0600) -# /client/tls.crt Client certificate -# /client/tls.key Client private key (mode 0600) -# -# Client certs are also copied to the CLI's auto-discovery directory: -# $XDG_CONFIG_HOME/openshell/gateways/openshell/mtls/{ca.crt,tls.crt,tls.key} - -set -euo pipefail - -PKI_DIR="${1:?Usage: init-pki.sh }" - -# ── Resolve CLI cert directory ─────────────────────────────────────── -CLI_MTLS_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/openshell/gateways/openshell/mtls" - -# ── Required PKI files ─────────────────────────────────────────────── -PKI_FILES=( - "${PKI_DIR}/ca.crt" - "${PKI_DIR}/ca.key" - "${PKI_DIR}/server/tls.crt" - "${PKI_DIR}/server/tls.key" - "${PKI_DIR}/client/tls.crt" - "${PKI_DIR}/client/tls.key" -) - -CLI_FILES=( - "${CLI_MTLS_DIR}/ca.crt" - "${CLI_MTLS_DIR}/tls.crt" - "${CLI_MTLS_DIR}/tls.key" -) - -# ── Idempotent: skip if all PKI files exist ────────────────────────── -all_pki_exist=true -for f in "${PKI_FILES[@]}"; do - if [ ! -f "$f" ]; then - all_pki_exist=false - break - fi -done - -if [ "$all_pki_exist" = true ]; then - # PKI is complete. Ensure CLI copies also exist (they may have been - # deleted independently, e.g. user cleared their config directory). - cli_ok=true - for f in "${CLI_FILES[@]}"; do - if [ ! -f "$f" ]; then - cli_ok=false - break - fi - done - if [ "$cli_ok" = false ]; then - echo "PKI exists but CLI auto-discovery certs missing; re-copying..." - mkdir -p "${CLI_MTLS_DIR}" - cp "${PKI_DIR}/ca.crt" "${CLI_MTLS_DIR}/ca.crt" - cp "${PKI_DIR}/client/tls.crt" "${CLI_MTLS_DIR}/tls.crt" - cp "${PKI_DIR}/client/tls.key" "${CLI_MTLS_DIR}/tls.key" - chmod 600 "${CLI_MTLS_DIR}/tls.key" - fi - exit 0 -fi - -# ── Partial state recovery ─────────────────────────────────────────── -# If some PKI files exist but not all, a previous run was interrupted. -# Remove the partial state so we can regenerate cleanly. -partial=false -for f in "${PKI_FILES[@]}"; do - if [ -f "$f" ]; then - partial=true - break - fi -done -if [ "$partial" = true ]; then - echo "WARNING: Partial PKI detected in ${PKI_DIR}, regenerating..." - rm -f "${PKI_DIR}/ca.crt" "${PKI_DIR}/ca.key" "${PKI_DIR}/ca.srl" - rm -rf "${PKI_DIR}/server" "${PKI_DIR}/client" -fi - -# ── Temporary workspace (cleaned up on exit) ───────────────────────── -WORK=$(mktemp -d) -trap 'rm -rf "${WORK}"' EXIT - -# Stage directory mirrors the final PKI layout. -STAGE="${WORK}/pki" -mkdir -p "${STAGE}/server" "${STAGE}/client" - -# ── Server certificate SANs ───────────────────────────────────────── -# These must match what the supervisor connects to. The CLI also -# connects using localhost/127.0.0.1 by default. -cat > "${WORK}/server-san.cnf" <<'EOF' -[req] -distinguished_name = req_dn -req_extensions = v3_req -prompt = no - -[req_dn] -O = openshell -CN = openshell-server - -[v3_req] -subjectAltName = @alt_names - -[alt_names] -DNS.1 = localhost -DNS.2 = openshell -DNS.3 = openshell.openshell.svc -DNS.4 = openshell.openshell.svc.cluster.local -DNS.5 = host.containers.internal -DNS.6 = host.docker.internal -IP.1 = 127.0.0.1 -EOF - -# ── Generate CA (into staging) ─────────────────────────────────────── -openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ - -keyout "${STAGE}/ca.key" \ - -out "${STAGE}/ca.crt" \ - -days 3650 -nodes \ - -subj "/O=openshell/CN=openshell-ca" \ - 2>/dev/null -chmod 600 "${STAGE}/ca.key" - -# ── Generate server certificate (into staging) ─────────────────────── -openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ - -keyout "${STAGE}/server/tls.key" \ - -out "${WORK}/server.csr" \ - -nodes \ - -config "${WORK}/server-san.cnf" \ - 2>/dev/null - -openssl x509 -req \ - -in "${WORK}/server.csr" \ - -CA "${STAGE}/ca.crt" -CAkey "${STAGE}/ca.key" -CAcreateserial \ - -out "${STAGE}/server/tls.crt" \ - -days 3650 \ - -extensions v3_req \ - -extfile "${WORK}/server-san.cnf" \ - 2>/dev/null -chmod 600 "${STAGE}/server/tls.key" - -# ── Generate client certificate (into staging) ─────────────────────── -openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ - -keyout "${STAGE}/client/tls.key" \ - -out "${WORK}/client.csr" \ - -nodes \ - -subj "/O=openshell/CN=openshell-client" \ - 2>/dev/null - -openssl x509 -req \ - -in "${WORK}/client.csr" \ - -CA "${STAGE}/ca.crt" -CAkey "${STAGE}/ca.key" -CAcreateserial \ - -out "${STAGE}/client/tls.crt" \ - -days 3650 \ - 2>/dev/null -chmod 600 "${STAGE}/client/tls.key" - -# ── Move staged PKI into final location ────────────────────────────── -# Create parent directories and move files individually. Using mv on -# individual files rather than whole directories so we do not clobber -# the target directory if it already exists. -mkdir -p "${PKI_DIR}/server" "${PKI_DIR}/client" -mv "${STAGE}/ca.crt" "${PKI_DIR}/ca.crt" -mv "${STAGE}/ca.key" "${PKI_DIR}/ca.key" -mv "${STAGE}/server/tls.crt" "${PKI_DIR}/server/tls.crt" -mv "${STAGE}/server/tls.key" "${PKI_DIR}/server/tls.key" -mv "${STAGE}/client/tls.crt" "${PKI_DIR}/client/tls.crt" -mv "${STAGE}/client/tls.key" "${PKI_DIR}/client/tls.key" - -# ── Copy client certs to CLI auto-discovery directory ──────────────── -# The CLI automatically looks for certs at: -# $XDG_CONFIG_HOME/openshell/gateways//mtls/{ca.crt,tls.crt,tls.key} -# For localhost gateways, defaults to "openshell". -mkdir -p "${CLI_MTLS_DIR}" -cp "${PKI_DIR}/ca.crt" "${CLI_MTLS_DIR}/ca.crt" -cp "${PKI_DIR}/client/tls.crt" "${CLI_MTLS_DIR}/tls.crt" -cp "${PKI_DIR}/client/tls.key" "${CLI_MTLS_DIR}/tls.key" -chmod 600 "${CLI_MTLS_DIR}/tls.key" - -echo "PKI bootstrap complete: ${PKI_DIR}" diff --git a/openshell.spec b/openshell.spec index 966bd5205..293911b98 100644 --- a/openshell.spec +++ b/openshell.spec @@ -149,9 +149,10 @@ Type=exec # CLI discovers them automatically. # See /usr/share/doc/openshell-gateway/ for details. -# Auto-generate PKI on first start if not present. -# %%S expands to $XDG_STATE_HOME (~/.local/state) in user units. -ExecStartPre=%{_libexecdir}/openshell/init-pki.sh %%S/openshell/tls +# Auto-generate PKI on first start. Idempotent: skips when all six PEMs are +# already in place. %%S expands to $XDG_STATE_HOME (~/.local/state) in user +# units. +ExecStartPre=/usr/bin/openshell-gateway generate-certs --output-dir %%S/openshell/tls # Auto-generate gateway.env (SSH handshake secret + commented config # reference) on first start if not present. @@ -186,9 +187,8 @@ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX WantedBy=default.target EOF -# --- PKI bootstrap script and gateway env generator --- +# --- Gateway env generator --- install -d %{buildroot}%{_libexecdir}/%{name} -install -pm 0755 deploy/rpm/init-pki.sh %{buildroot}%{_libexecdir}/%{name}/init-pki.sh install -pm 0755 deploy/rpm/init-gateway-env.sh %{buildroot}%{_libexecdir}/%{name}/init-gateway-env.sh # Patch commented image defaults to match the build type (dev or latest). # The source file uses :latest as a generic reference; the installed copy @@ -275,7 +275,6 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata %doc %{_docdir}/%{name}-gateway/TROUBLESHOOTING.md %{_bindir}/%{name}-gateway %{_userunitdir}/%{name}-gateway.service -%{_libexecdir}/%{name}/init-pki.sh %{_libexecdir}/%{name}/init-gateway-env.sh %{_mandir}/man8/openshell-gateway.8* %{_mandir}/man5/openshell-gateway.env.5*