#!/usr/bin/env sh # Script to deploy a certificate to a JetKVM (https://jetkvm.com) KVM-over-IP # device over SSH. See also: # https://github.com/acmesh-official/acme.sh/wiki/deployhooks # # JetKVM only supports key-based SSH authentication (root@, password # logins are disabled) once "Developer Mode" is enabled and a public key is # pasted into its web UI (Settings > Advanced). SSH keys must already be # exchanged and a passwordless login confirmed working (e.g. `ssh # root@jetkvm.example.com true`) before using this hook. # # JetKVM's minimal userspace does not ship an scp binary or SFTP server, so # unlike deploy/ssh.sh this hook has no "use scp" option: it always writes # the certificate and key by piping a small POSIX shell script to the # remote "sh" over stdin (only depends on "sh", "cat", "chmod", "mkdir", # "mv" and "rm" on the device side). The remote path, filenames and file # permissions are firmware constants on this single-purpose, single-root # appliance, so they are not configurable here. # # JetKVM's "Custom" TLS mode (device web UI: Settings > Network > HTTPS # Mode, must already be set to "Custom" before this hook's uploads take # effect) reads the certificate/key from that fixed location and does not # hot-reload: a device reboot is required to pick up a new certificate. # This hook's restart command therefore defaults to "reboot" -- a blank # DEPLOY_JETKVM_RESTART_CMD is treated the same as unset (falls back to # "reboot") rather than silently skipping it, since a renewed certificate # that's never actually applied defeats the point of automating this; set # it to the literal value "none" to opt out and apply/verify manually. # The restart command is run detached on the device (nohup ... &) so this # hook's ssh call can return before the reboot itself lands, rather than # racing the connection teardown. # # The certificate and key are staged under fixed temporary names on the # device and only renamed into their final names (an atomic "mv", on the # same filesystem) once both have been fully written and chmod'ed. This # keeps a dropped connection or a failed write from ever leaving the # device with a truncated or mismatched certificate/key pair for its own # HTTPS listener, and a "trap ... EXIT" in the generated script removes # any leftover staged file however that script exits. # # Before writing anything, this hook also checks that the device's HTTPS # Mode is already "Custom" -- uploading a certificate that mode won't # even serve would otherwise be a silent no-op. There is currently no # documented/headless way to read this back (JetKVM's own JSON-RPC # getTLSState/setTLSState calls require an authenticated WebRTC session, # see https://github.com/jetkvm/kvm/issues/1240 and the still-open # https://github.com/jetkvm/kvm/pull/1515), so this greps the device's # own config file instead: JetKVM's firmware (see web_tls.go / config.go # in https://github.com/jetkvm/kvm) persists the mode as the plain-JSON # field "tls_mode" (values "", "self-signed", or "custom") in # /userdata/kvm_config.json. # # None of the above (storage path, filenames, config file, reboot-to-apply # behavior) is part of JetKVM's stable/documented API; it was confirmed # against real JetKVM hardware, but is worth a spot-check after a JetKVM # firmware upgrade -- set DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE=no to skip the # HTTPS-mode check entirely if a future firmware version changes that # file's format out from under it. # # The following variables exported from environment will be used. If not # set then values previously saved in the domain.conf file are used. All # of them are optional. # # export DEPLOY_JETKVM_USER="root" # defaults to "root" # export DEPLOY_JETKVM_HOST="jetkvm.example.com" # defaults to the cert's domain # export DEPLOY_JETKVM_PORT="22" # defaults to 22 # export DEPLOY_JETKVM_SSH_CMD="ssh -T" # defaults to "ssh -T" # export DEPLOY_JETKVM_RESTART_CMD="reboot" # defaults to "reboot"; set to "none" to skip it # export DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE="yes" # defaults to "yes" (verify tls_mode=custom before upload); set to "no" to skip # # Example: # ```sh # export DEPLOY_JETKVM_HOST="192.168.1.50" # acme.sh --deploy -d jetkvm.example.com --deploy-hook jetkvm # ``` # # returns 0 means success, otherwise error. ######## Public functions ##################### #domain keyfile certfile cafile fullchain jetkvm_deploy() { _cdomain="$1" _ckey="$2" _ccert="$3" _cca="$4" _cfullchain="$5" _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" _debug _cca "$_cca" _debug _cfullchain "$_cfullchain" if [ ! -s "$_ckey" ] || [ ! -s "$_cfullchain" ]; then _err "JetKVM deploy needs both a private key and a fullchain certificate (not available, e.g., after --signcsr)." return 1 fi _getdeployconf DEPLOY_JETKVM_USER if [ -z "$DEPLOY_JETKVM_USER" ]; then DEPLOY_JETKVM_USER="root" fi _savedeployconf DEPLOY_JETKVM_USER "$DEPLOY_JETKVM_USER" _getdeployconf DEPLOY_JETKVM_HOST if [ -z "$DEPLOY_JETKVM_HOST" ]; then _debug "Using _cdomain as DEPLOY_JETKVM_HOST, please set if not correct." DEPLOY_JETKVM_HOST="$_cdomain" fi _savedeployconf DEPLOY_JETKVM_HOST "$DEPLOY_JETKVM_HOST" _getdeployconf DEPLOY_JETKVM_PORT if [ -z "$DEPLOY_JETKVM_PORT" ]; then DEPLOY_JETKVM_PORT="22" fi _savedeployconf DEPLOY_JETKVM_PORT "$DEPLOY_JETKVM_PORT" _getdeployconf DEPLOY_JETKVM_SSH_CMD if [ -z "$DEPLOY_JETKVM_SSH_CMD" ]; then DEPLOY_JETKVM_SSH_CMD="ssh -T" fi _savedeployconf DEPLOY_JETKVM_SSH_CMD "$DEPLOY_JETKVM_SSH_CMD" "base64" _getdeployconf DEPLOY_JETKVM_RESTART_CMD if [ -z "$DEPLOY_JETKVM_RESTART_CMD" ]; then DEPLOY_JETKVM_RESTART_CMD="reboot" fi _savedeployconf DEPLOY_JETKVM_RESTART_CMD "$DEPLOY_JETKVM_RESTART_CMD" "base64" _getdeployconf DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE if [ -z "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE" ]; then DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE="yes" fi _savedeployconf DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE" _info "Deploying certificate to JetKVM device $DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST:$DEPLOY_JETKVM_PORT" # Firmware constants on a single-purpose, single-root appliance -- not # user configuration. If JetKVM ever moves these, that's a hook update, # not a setting (a saved-per-domain override would just as easily hide # the fix from anyone already using this hook). _jetkvm_remote_path="/userdata/jetkvm/tls" _jetkvm_cert_name="user-defined.crt" _jetkvm_key_name="user-defined.key" _jetkvm_config_file="/userdata/kvm_config.json" _jetkvm_mode_exitcode=3 _jetkvm_config_missing_exitcode=4 _jetkvm_run_id="$$.$(_time)" _jetkvm_cert_marker="ACME_JETKVM_CERT_$_jetkvm_run_id" _jetkvm_key_marker="ACME_JETKVM_KEY_$_jetkvm_run_id" _jetkvm_cert_tmp="$_jetkvm_remote_path/.$_jetkvm_cert_name.tmp" _jetkvm_key_tmp="$_jetkvm_remote_path/.$_jetkvm_key_name.tmp" _jetkvm_cert_target="$_jetkvm_remote_path/$_jetkvm_cert_name" _jetkvm_key_target="$_jetkvm_remote_path/$_jetkvm_key_name" # Command substitution strips all trailing newlines, so the printf below # always emits the content with exactly one trailing newline before the # heredoc terminator -- regardless of whether the source file already # ended with one -- so the terminator is guaranteed to start its own line. _jetkvm_cert_content="$(cat "$_cfullchain")" _jetkvm_key_content="$(cat "$_ckey")" _jetkvm_upload_script="$( echo "#!/bin/sh" echo "set -e" echo "umask 077" printf "trap \"rm -f '%s' '%s'\" EXIT\n" "$_jetkvm_cert_tmp" "$_jetkvm_key_tmp" if [ "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE" != "no" ]; then # Uploading a certificate that HTTPS Mode won't even serve would # otherwise fail silently -- see the header comment for why this # greps the device's own config file rather than querying it # through a documented API (there isn't one for reading this # headlessly yet). The config file is checked for readability # separately so a missing/renamed file isn't misreported as # HTTPS Mode being wrong. printf "if [ ! -r '%s' ]; then exit %s; fi\n" "$_jetkvm_config_file" "$_jetkvm_config_missing_exitcode" printf 'if ! grep -q '\''"tls_mode" *: *"custom"'\'' '\''%s'\''; then exit %s; fi\n' "$_jetkvm_config_file" "$_jetkvm_mode_exitcode" fi printf "mkdir -p '%s'\n" "$_jetkvm_remote_path" printf "cat > '%s' <<'%s'\n" "$_jetkvm_cert_tmp" "$_jetkvm_cert_marker" printf '%s\n' "$_jetkvm_cert_content" echo "$_jetkvm_cert_marker" printf "chmod 0644 '%s'\n" "$_jetkvm_cert_tmp" printf "cat > '%s' <<'%s'\n" "$_jetkvm_key_tmp" "$_jetkvm_key_marker" printf '%s\n' "$_jetkvm_key_content" echo "$_jetkvm_key_marker" printf "chmod 0600 '%s'\n" "$_jetkvm_key_tmp" printf "mv '%s' '%s'\n" "$_jetkvm_cert_tmp" "$_jetkvm_cert_target" printf "mv '%s' '%s'\n" "$_jetkvm_key_tmp" "$_jetkvm_key_target" )" _secure_debug "Generated upload script" "$_jetkvm_upload_script" _info "Connecting to JetKVM device $DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST:$DEPLOY_JETKVM_PORT to deploy certificate" # shellcheck disable=SC2086 printf '%s\n' "$_jetkvm_upload_script" | $DEPLOY_JETKVM_SSH_CMD -p "$DEPLOY_JETKVM_PORT" "$DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST" sh _ret=$? if [ "$_ret" = "$_jetkvm_config_missing_exitcode" ]; then _err "JetKVM config file ($_jetkvm_config_file) was not found or not readable on the device -- this hook's assumptions may be out of date after a firmware upgrade. Certificate was NOT uploaded." return "$_ret" fi if [ "$_ret" = "$_jetkvm_mode_exitcode" ]; then _err "JetKVM HTTPS Mode is not set to \"Custom\" (checked \"tls_mode\" in $_jetkvm_config_file on the device). Set it in the device's web UI (Settings > Network > HTTPS Mode) before this hook can take effect. Certificate was NOT uploaded." return "$_ret" fi if [ "$_ret" != "0" ]; then _err "Error code $_ret returned uploading certificate to JetKVM device" return "$_ret" fi _info "Certificate and key uploaded to $_jetkvm_remote_path on the device" if [ "$DEPLOY_JETKVM_RESTART_CMD" = "none" ]; then _info "Certificate successfully deployed to JetKVM device. DEPLOY_JETKVM_RESTART_CMD=none, skipping restart command." return 0 fi # Run the restart command detached (nohup ... &) so this ssh call # returns as soon as it's launched, before the device actually reboots, # rather than racing the connection teardown -- observed, against real # hardware, that a reboot racing the SSH session's own exit can make # ssh itself exit anywhere from a clean 0 to a connection-reset 255. # Since the restart command then runs as an unwaited background job on # the device, this ssh call reports success as soon as that job is # launched -- it does NOT confirm nohup, sh, or the restart command # itself actually exist or succeed (measured: a nonexistent restart # command, and even a missing nohup binary, both still return 0 here). # Only an outright SSH connection failure (unreachable host, auth # failure, etc.) is caught below. "sleep" runs on the device's own # shell, not acme.sh's, so acme.sh's _sleep wrapper does not apply. _info "Running post-upload command on JetKVM device: $DEPLOY_JETKVM_RESTART_CMD" # Escape any single quotes in the (user-configurable, free-text) # restart command before nesting it inside the outer 'sleep N; ...' # single-quoted string -- otherwise a value like "sh -c 'sync; reboot'" # breaks the quoting and only part of it ends up inside the detached # background job. _jetkvm_restart_cmd_escaped=$(printf '%s' "$DEPLOY_JETKVM_RESTART_CMD" | sed "s/'/'\\\\''/g") _jetkvm_detached_cmd="nohup sh -c 'sleep 2; $_jetkvm_restart_cmd_escaped' >/dev/null 2>&1 &" # shellcheck disable=SC2086 if ! $DEPLOY_JETKVM_SSH_CMD -p "$DEPLOY_JETKVM_PORT" "$DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST" "$_jetkvm_detached_cmd"; then _err "Certificate was uploaded, but connecting to the JetKVM device to launch the restart command failed." return 1 fi _info "Certificate deployed to JetKVM device; it will restart shortly to apply it." return 0 }