diff --git a/crates/openshell-bootstrap/src/pki.rs b/crates/openshell-bootstrap/src/pki.rs
index ed93850df..b6747260b 100644
--- a/crates/openshell-bootstrap/src/pki.rs
+++ b/crates/openshell-bootstrap/src/pki.rs
@@ -17,7 +17,10 @@ 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",
@@ -26,6 +29,7 @@ const DEFAULT_SERVER_SANS: &[&str] = &[
"openshell.localhost",
"*.openshell.localhost",
"host.docker.internal",
+ "host.containers.internal",
"127.0.0.1",
];
diff --git a/crates/openshell-server/src/certgen.rs b/crates/openshell-server/src/certgen.rs
index f7dcc0803..c72dcd6dd 100644
--- a/crates/openshell-server/src/certgen.rs
+++ b/crates/openshell-server/src/certgen.rs
@@ -9,8 +9,8 @@
//! in the supplied namespace. Used by the Helm pre-install hook. Requires
//! `--namespace`, `--server-secret-name`, `--client-secret-name`.
//! - **Local mode** (`--output-dir
`): write PEMs to a filesystem layout
-//! matching `deploy/rpm/init-pki.sh`. Used by the RPM systemd unit's
-//! `ExecStartPre`. Also copies client materials to
+//! used by the RPM systemd unit's `ExecStartPre`. Also copies client
+//! materials to
//! `$XDG_CONFIG_HOME/openshell/gateways/openshell/mtls/` so the local CLI
//! picks them up automatically.
//!
@@ -216,7 +216,7 @@ enum LocalAction {
Create,
}
-/// Layout under `` matches `deploy/rpm/init-pki.sh`:
+/// Layout under ``:
///
/// ```text
/// /ca.crt
diff --git a/deploy/man/openshell-gateway.8.md b/deploy/man/openshell-gateway.8.md
index ee2ad8ed2..5df741ffd 100644
--- a/deploy/man/openshell-gateway.8.md
+++ b/deploy/man/openshell-gateway.8.md
@@ -114,13 +114,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.
-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:
@@ -147,9 +148,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 2bf23fd1b..95a3b2c32 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 |
|------|---------|----------|
@@ -247,7 +249,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/config 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 baf2f5564..61a6517bd 100644
--- a/deploy/rpm/init-gateway-env.sh
+++ b/deploy/rpm/init-gateway-env.sh
@@ -111,9 +111,10 @@ OPENSHELL_GATEWAY_CONFIG=${CONFIG_FILE}
# ${CONFIG_FILE}
# ---- 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 a45cee323..d3bd26d07 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 (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*