diff --git a/Makefile-quickstart b/Makefile-quickstart index f1d35f38..5fc47669 100644 --- a/Makefile-quickstart +++ b/Makefile-quickstart @@ -344,13 +344,26 @@ openshell-saw-configure-gateway: .check-saw-name ## Configure openshell CLI for fi; \ OIDC_OPTS=""; \ if [ -n "$$OIDC_ISSUER" ]; then OIDC_OPTS="--oidc-issuer $$OIDC_ISSUER --oidc-client-id $(OIDC_CLIENT_ID)"; fi; \ + GW_CONFIG_DIR="$$HOME/.config/openshell/gateways/$(OPENSHELL_SAW_NAME)"; \ + echo "Extracting CA certificate from cluster..."; \ + mkdir -p "$$GW_CONFIG_DIR/mtls"; \ + oc get secret $(OPENSHELL_SAW_NAME)-gateway-ca -n $(NS) \ + -o jsonpath='{.data.ca\.crt}' 2>/dev/null \ + | base64 -d > "$$GW_CONFIG_DIR/mtls/ca.crt" 2>/dev/null || true; \ + if [ ! -s "$$GW_CONFIG_DIR/mtls/ca.crt" ]; then \ + echo "Error: CA certificate not found. The setup job may still be running."; \ + echo " Run 'make openshell-saw-logs' to check, then re-run this target."; \ + rm -f "$$GW_CONFIG_DIR/mtls/ca.crt"; \ + exit 1; \ + fi; \ + echo "CA certificate installed."; \ echo "Registering gateway '$(OPENSHELL_SAW_NAME)'..."; \ openshell gateway remove $(OPENSHELL_SAW_NAME) 2>/dev/null || true; \ - openshell gateway add "$$GW_URL" --name $(OPENSHELL_SAW_NAME) --gateway-insecure $$OIDC_OPTS; \ + openshell gateway add "$$GW_URL" --name $(OPENSHELL_SAW_NAME) $$OIDC_OPTS; \ openshell gateway select $(OPENSHELL_SAW_NAME); \ echo "Gateway '$(OPENSHELL_SAW_NAME)' configured. Endpoint: $$GW_URL"; \ OIDC_TOKEN_FILE="$(OIDC_TOKEN_DIR)/token.json"; \ - GW_TOKEN_DIR="$$HOME/.config/openshell/gateways/$(OPENSHELL_SAW_NAME)"; \ + GW_TOKEN_DIR="$$GW_CONFIG_DIR"; \ if [ -f "$$OIDC_TOKEN_FILE" ] && [ -n "$$OIDC_ISSUER" ]; then \ echo "Copying OIDC token to gateway config..."; \ mkdir -p "$$GW_TOKEN_DIR"; \ @@ -380,7 +393,7 @@ openshell-saw-logs: .check-saw-name ## Follow sandbox setup Job logs oc -n $(NS) logs -f job/$(OPENSHELL_SAW_NAME)-setup openshell-saw-ssh: .check-saw-name ## SSH into a sandbox - @ssh -o "ProxyCommand=openshell --gateway-insecure ssh-proxy --gateway-name $(OPENSHELL_SAW_NAME) --name $(OPENSHELL_SAW_NAME) " \ + @ssh -o "ProxyCommand=openshell ssh-proxy --gateway-name $(OPENSHELL_SAW_NAME) --name $(OPENSHELL_SAW_NAME) " \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ @@ -392,11 +405,11 @@ nemoclaw-tui: .check-saw-name ## Launch NemoClaw TUI (e.g. make nemoclaw-tui SAN @if [ -n "$(SANDBOX_NAME)" ]; then \ SANDBOX="$(SANDBOX_NAME)"; \ else \ - SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) --gateway-insecure sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ + SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ fi; \ if [ -z "$$SANDBOX" ]; then echo "Error: No sandboxes found on gateway '$(OPENSHELL_SAW_NAME)' workspace '$(WORKSPACE)'."; exit 1; fi; \ echo "Connecting to sandbox '$$SANDBOX' workspace '$(WORKSPACE)' (nemoclaw)..."; \ - ssh -o "ProxyCommand=openshell --gateway-insecure ssh-proxy --gateway-name $(OPENSHELL_SAW_NAME) --name $$SANDBOX " \ + ssh -o "ProxyCommand=openshell ssh-proxy --gateway-name $(OPENSHELL_SAW_NAME) --name $$SANDBOX " \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ @@ -406,11 +419,11 @@ openclaw-tui: .check-saw-name ## Launch OpenClaw TUI (e.g. make openclaw-tui SAN @if [ -n "$(SANDBOX_NAME)" ]; then \ SANDBOX="$(SANDBOX_NAME)"; \ else \ - SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) --gateway-insecure sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ + SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ fi; \ if [ -z "$$SANDBOX" ]; then echo "Error: No sandboxes found on gateway '$(OPENSHELL_SAW_NAME)' workspace '$(WORKSPACE)'."; exit 1; fi; \ echo "Connecting to sandbox '$$SANDBOX' workspace '$(WORKSPACE)' (openclaw)..."; \ - openshell --gateway-insecure sandbox exec -n $$SANDBOX --workspace $(WORKSPACE) --tty -- \ + openshell sandbox exec -n $$SANDBOX --workspace $(WORKSPACE) --tty -- \ /bin/bash -ic 'export HOME=/sandbox OPENCLAW_HOME=/sandbox SQLITE_TMPDIR=/sandbox/.openclaw/state TMPDIR=/sandbox/.openclaw/state OPENCLAW_NIX_MODE=0 TERM=xterm-256color; exec openclaw tui' openshell-saw-tui: .check-saw-name ## Launch TUI (alias for nemoclaw-tui) @@ -420,7 +433,7 @@ nemoclaw-gui: .check-saw-name ## Open NemoClaw web UI (e.g. make nemoclaw-gui SA @if [ -n "$(SANDBOX_NAME)" ]; then \ SANDBOX="$(SANDBOX_NAME)"; \ else \ - SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) --gateway-insecure sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ + SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ fi; \ if [ -z "$$SANDBOX" ]; then echo "Error: No sandboxes found."; exit 1; fi; \ SANDBOX_NAME=$$SANDBOX GATEWAY_NAME=$(OPENSHELL_SAW_NAME) \ @@ -430,13 +443,12 @@ openclaw-gui: .check-saw-name ## Open OpenClaw web UI (e.g. make openclaw-gui SA @if [ -n "$(SANDBOX_NAME)" ]; then \ SANDBOX="$(SANDBOX_NAME)"; \ else \ - SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) --gateway-insecure sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ + SANDBOX=$$(openshell --gateway $(OPENSHELL_SAW_NAME) sandbox list --workspace $(WORKSPACE) --names 2>/dev/null | grep -E '^[a-zA-Z0-9_-]+$$' | head -1); \ fi; \ if [ -z "$$SANDBOX" ]; then echo "Error: No sandboxes found."; exit 1; fi; \ echo "Fetching openclaw gateway token..."; \ - TOKEN=$$(openshell --gateway-insecure sandbox exec -n $$SANDBOX --workspace $(WORKSPACE) --no-tty -- \ + TOKEN=$$(openshell sandbox exec -n $$SANDBOX --workspace $(WORKSPACE) --no-tty -- \ sh -c 'OPENCLAW_HOME=/sandbox cat /sandbox/.openclaw/openclaw.json' 2>/dev/null \ - | grep -v 'TLS certificate' \ | python3 -c "import sys,json; c=json.load(sys.stdin); print(c.get('gateway',{}).get('auth',{}).get('token',''))" 2>/dev/null || true); \ if [ -z "$$TOKEN" ]; then echo "Error: Could not extract token."; exit 1; fi; \ GUI_PORT=$${GUI_PORT:-18789}; \ @@ -446,7 +458,7 @@ openclaw-gui: .check-saw-name ## Open OpenClaw web UI (e.g. make openclaw-gui SA echo "OpenClaw UI: http://localhost:$$GUI_PORT/#token=$$TOKEN"; \ echo "Press Ctrl-C to stop."; \ echo ""; \ - ssh -o "ProxyCommand=openshell --gateway-insecure ssh-proxy --gateway-name $(OPENSHELL_SAW_NAME) --name $$SANDBOX" \ + ssh -o "ProxyCommand=openshell ssh-proxy --gateway-name $(OPENSHELL_SAW_NAME) --name $$SANDBOX" \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ diff --git a/README.md b/README.md index 4da1894f..e95b5474 100644 --- a/README.md +++ b/README.md @@ -194,10 +194,10 @@ make copy-images make login # Opens browser → login with alice / alice export OPENSHELL_SAW_NAME=openshell-saw make openshell-saw-configure-gateway -openshell gateway login $OPENSHELL_SAW_NAME --gateway-insecure # Authenticate CLI with gateway +openshell gateway login $OPENSHELL_SAW_NAME # Authenticate CLI with gateway # 8. Verify -openshell --gateway-insecure sandbox list +openshell sandbox list ``` #### Option B: Quickstart (manual, step-by-step) @@ -258,7 +258,7 @@ openshell gateway add https://$(oc get route openshell-saw-gateway -n openshell- # Log in as alice / alice in the browser # 15. Verify sandboxes -openshell --gateway-insecure sandbox list +openshell sandbox list # 16. Launch TUI (pick one) OPENSHELL_SAW_NAME=openshell-saw SANDBOX_NAME=cuda-sandbox make nemoclaw-tui # NemoClaw @@ -269,8 +269,6 @@ OPENSHELL_SAW_NAME=openshell-saw SANDBOX_NAME=cuda-sandbox GUI_PORT=18789 make n OPENSHELL_SAW_NAME=openshell-saw SANDBOX_NAME=notebook GUI_PORT=18790 make openclaw-gui ``` -> **Note:** The gateway VM uses a self-signed TLS certificate. Pass `--gateway-insecure` to `openshell` commands, or set `export OPENSHELL_GATEWAY_INSECURE=true`. - > **Token expiry:** The OIDC access token lasts 10 hours. If it expires, run `make login` to re-authenticate, then `make openshell-saw-configure-gateway` to copy the fresh token. Alternatively, run `openshell gateway login` directly to re-authenticate with the gateway. You can set `OPENSHELL_SAW_NAME` once via `export` and all `openshell-saw-*` targets will use it automatically. @@ -293,7 +291,7 @@ You can set `OPENSHELL_SAW_NAME` once via `export` and all `openshell-saw-*` tar ```bash # List sandboxes -openshell --gateway-insecure sandbox list +openshell sandbox list # NemoClaw sandbox (TUI and GUI) OPENSHELL_SAW_NAME=openshell-saw SANDBOX_NAME=cuda-sandbox make nemoclaw-tui diff --git a/charts/openshell-saw/files/export-ca-cert.sh b/charts/openshell-saw/files/export-ca-cert.sh new file mode 100644 index 00000000..c26ed271 --- /dev/null +++ b/charts/openshell-saw/files/export-ca-cert.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Phase: export gateway CA certificate to a Kubernetes Secret. +# The CLI reads this CA to verify the gateway's TLS certificate, +# eliminating the need for --gateway-insecure. +# Expects: VM_NAME, NS, guest_ssh (function) + +echo "Exporting gateway CA certificate..." +CA_CERT="$(guest_ssh 'cat $HOME/.local/state/openshell/tls/ca.crt 2>/dev/null' || true)" + +if [[ -z "${CA_CERT}" ]]; then + echo "ERROR: CA certificate not found on VM — gateway certs were not generated" >&2 + return 1 +fi + +kubectl create secret generic "${VM_NAME}-gateway-ca" -n "${NS}" \ + --from-literal=ca.crt="${CA_CERT}" \ + --dry-run=client -o yaml | kubectl apply -f - + +echo "CA certificate exported to secret/${VM_NAME}-gateway-ca" diff --git a/charts/openshell-saw/files/run-setup.sh b/charts/openshell-saw/files/run-setup.sh index f0340f5a..10a75cb1 100644 --- a/charts/openshell-saw/files/run-setup.sh +++ b/charts/openshell-saw/files/run-setup.sh @@ -81,13 +81,16 @@ source "${SCRIPTS_DIR}/wait-for-vm.sh" # --- Phase 4: Upgrade OpenShell binaries --- source "${SCRIPTS_DIR}/upgrade-openshell.sh" -# --- Phase 5: Governance check + SSH key fallback --- +# --- Phase 5: Export CA certificate --- +source "${SCRIPTS_DIR}/export-ca-cert.sh" + +# --- Phase 6: Governance check + SSH key fallback --- source "${SCRIPTS_DIR}/check-governance.sh" -# --- Phase 6: BOM profile setup --- +# --- Phase 7: BOM profile setup --- source "${SCRIPTS_DIR}/setup-bom-profiles.sh" -# --- Phase 7: Dashboard setup --- +# --- Phase 8: Dashboard setup --- if [[ "${DASHBOARD_ENABLED}" == "true" ]]; then source "${SCRIPTS_DIR}/setup-keycloak-redirect.sh" fi diff --git a/charts/openshell-saw/files/upgrade-openshell.sh b/charts/openshell-saw/files/upgrade-openshell.sh index cc1a582e..cf35865b 100644 --- a/charts/openshell-saw/files/upgrade-openshell.sh +++ b/charts/openshell-saw/files/upgrade-openshell.sh @@ -97,7 +97,7 @@ fi # --- Restart gateway with new binaries --- echo "Restarting gateway service..." -guest_ssh "systemctl --user restart openshell-gateway.service" || true +guest_ssh "systemctl --user daemon-reload && systemctl --user restart openshell-gateway.service" || true GW_READY=0 for i in $(seq 1 10); do if guest_ssh "systemctl --user is-active openshell-gateway.service" 2>/dev/null; then diff --git a/charts/openshell-saw/templates/_helpers.tpl b/charts/openshell-saw/templates/_helpers.tpl index 2bef8be5..786e024c 100644 --- a/charts/openshell-saw/templates/_helpers.tpl +++ b/charts/openshell-saw/templates/_helpers.tpl @@ -65,6 +65,21 @@ OpenShell rejects names longer than 19 chars with "name exceeds maximum length". {{- end -}} {{- end }} +{{/* +Resolve the external Route hostname for the gateway. +Priority: explicit route.host > computed from global.clusterDomain. +Used to add the route FQDN to the gateway TLS certificate SANs. +*/}} +{{- define "openshell-sandbox.routeHost" -}} +{{- if .Values.route.host -}} + {{- .Values.route.host -}} +{{- else if .Values.global -}} + {{- if .Values.global.clusterDomain -}} + {{- printf "%s-gateway-%s.apps.%s" (include "openshell-sandbox.fullname" .) .Release.Namespace .Values.global.clusterDomain -}} + {{- end -}} +{{- end -}} +{{- end }} + {{/* Resolve the golden image DataSource name. Priority: explicit source.dataSource > derived from containerRuntime. diff --git a/charts/openshell-saw/templates/cloudinit-sandbox.yaml b/charts/openshell-saw/templates/cloudinit-sandbox.yaml index 1d2722af..10d34c0a 100644 --- a/charts/openshell-saw/templates/cloudinit-sandbox.yaml +++ b/charts/openshell-saw/templates/cloudinit-sandbox.yaml @@ -43,6 +43,10 @@ data: {{- if or .Values.oidc.token $oidcIssuer }} OPENSHELL_ENABLE_MTLS_AUTH=true {{- end }} + {{- $routeHost := include "openshell-sandbox.routeHost" . }} + {{- if $routeHost }} + OPENSHELL_ROUTE_FQDN={{ $routeHost }} + {{- end }} {{- $oidcIssuer := include "openshell-sandbox.oidcIssuerUrl" . }} {{- if or .Values.oidc.token $oidcIssuer .Values.governance.enabled }} @@ -106,6 +110,18 @@ data: cp /etc/openshell/gateway.toml "${USER_HOME}/.config/openshell/gateway.toml" fi chown -R "${USER_NAME}:${USER_NAME}" "${USER_HOME}/.config/openshell" + ROUTE_FQDN=$(grep -oP 'OPENSHELL_ROUTE_FQDN=\K.*' "${USER_HOME}/.config/openshell/gateway.env" || true) + if [[ -n "${ROUTE_FQDN}" ]]; then + echo "Adding route FQDN to gateway cert SANs: ${ROUTE_FQDN}" + OVERRIDE_DIR="${USER_HOME}/.config/systemd/user/openshell-gateway.service.d" + install -d -m 0755 -o "${USER_NAME}" -g "${USER_NAME}" "${OVERRIDE_DIR}" + cat > "${OVERRIDE_DIR}/route-san.conf" <&1 \ - | grep -v 'TLS certificate' + openshell --gateway "${SAW_NAME}" "$@" 2>&1 } run_on_vm() { diff --git a/scripts/governance-profile.sh b/scripts/governance-profile.sh index 8efd8f9e..2912252d 100755 --- a/scripts/governance-profile.sh +++ b/scripts/governance-profile.sh @@ -51,7 +51,7 @@ wait_for_sync() { for i in $(seq 1 36); do sleep 5 local profiles - profiles=$(openshell --gateway "${SAW_NAME}" --gateway-insecure provider list-profiles 2>&1 || true) + profiles=$(openshell --gateway "${SAW_NAME}" provider list-profiles 2>&1 || true) if [[ -n "${expected_action}" && -n "${profile_name}" ]]; then if [[ "${expected_action}" == "appear" ]] && echo "${profiles}" | grep -q "${profile_name}"; then echo " Profile '${profile_name}' is now active. (${i} polls)" @@ -68,8 +68,7 @@ wait_for_sync() { cmd_list() { echo "Active profiles (enforced on gateway):" echo "" - openshell --gateway "${SAW_NAME}" --gateway-insecure provider list-profiles 2>&1 \ - | grep -v 'TLS certificate' + openshell --gateway "${SAW_NAME}" provider list-profiles } cmd_add() { diff --git a/scripts/openshell-saw-create.sh b/scripts/openshell-saw-create.sh index 7d094792..39443f73 100755 --- a/scripts/openshell-saw-create.sh +++ b/scripts/openshell-saw-create.sh @@ -101,6 +101,14 @@ if [[ "${NAMESPACE_MODE}" == "perUser" ]]; then oc create namespace "${DEPLOY_NS}" --dry-run=client -o yaml | oc apply -f - 2>/dev/null fi +# --- Compute route hostname --- +ROUTE_HOST="" +APPS_DOMAIN=$(oc get ingress.config.openshift.io cluster \ + -o jsonpath='{.spec.domain}' 2>/dev/null || true) +if [[ -n "${APPS_DOMAIN}" ]]; then + ROUTE_HOST="${OPENSHELL_SAW_NAME}-gateway-${DEPLOY_NS}.${APPS_DOMAIN}" +fi + # --- Deploy --- echo "Provisioning sandbox '${OPENSHELL_SAW_NAME}' for owner '${OWNER}' in namespace '${DEPLOY_NS}'..." @@ -122,7 +130,8 @@ helm upgrade --install "${OPENSHELL_SAW_NAME}" "${SAW_CHART}" \ --set namespaceMode="${NAMESPACE_MODE}" \ --set containerRuntime="${CONTAINER_RUNTIME}" \ --set governance.enabled="${GOVERNANCE_ENABLED}" \ - --set route.enabled=true --set route.dashboard=true + --set route.enabled=true --set route.dashboard=true \ + ${ROUTE_HOST:+--set route.host="${ROUTE_HOST}"} echo "" echo "Sandbox '${OPENSHELL_SAW_NAME}' deployed." @@ -143,4 +152,4 @@ echo "" echo "Next steps:" echo " 1. make openshell-saw-configure-gateway OPENSHELL_SAW_NAME=${OPENSHELL_SAW_NAME} NS=${DEPLOY_NS}" echo " 2. openshell gateway login" -echo " 3. openshell --gateway-insecure sandbox list" +echo " 3. openshell sandbox list" diff --git a/scripts/openshell-saw-gui.sh b/scripts/openshell-saw-gui.sh index a4e59cee..2507db15 100755 --- a/scripts/openshell-saw-gui.sh +++ b/scripts/openshell-saw-gui.sh @@ -23,13 +23,12 @@ fi # Fetch dashboard token via openshell sandbox exec echo "Fetching dashboard token..." -TOKEN=$(openshell --gateway-insecure sandbox exec -n "${SANDBOX_NAME}" --no-tty -- \ +TOKEN=$(openshell sandbox exec -n "${SANDBOX_NAME}" --no-tty -- \ cat /sandbox/.openclaw/openclaw.json 2>/dev/null \ - | grep -v 'TLS certificate verification is disabled' \ | python3 -c "import sys,json; c=json.load(sys.stdin); print((c.get('gateway',{}).get('auth',{}).get('token','')))" 2>/dev/null | grep -oE '^[a-f0-9]+$' || true) if [[ -z "${TOKEN}" ]]; then - TOKEN=$(openshell --gateway-insecure sandbox exec -n "${SANDBOX_NAME}" --no-tty -- \ + TOKEN=$(openshell sandbox exec -n "${SANDBOX_NAME}" --no-tty -- \ cat /tmp/auth-token 2>/dev/null | grep -oE '[a-f0-9]{32,}' || true) fi @@ -37,7 +36,7 @@ if [[ -z "${TOKEN}" ]]; then echo "Error: Could not extract dashboard token." echo " Make sure the sandbox setup has completed and openclaw is configured." echo "" - echo " Try: openshell --gateway-insecure sandbox list" + echo " Try: openshell sandbox list" exit 1 fi @@ -47,7 +46,7 @@ echo "Press Ctrl-C to stop." echo "" # Port-forward via openshell ssh-proxy — uses local OIDC token -ssh -o "ProxyCommand=openshell --gateway-insecure ssh-proxy --gateway-name ${GATEWAY_NAME} --name ${SANDBOX_NAME}" \ +ssh -o "ProxyCommand=openshell ssh-proxy --gateway-name ${GATEWAY_NAME} --name ${SANDBOX_NAME}" \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \