#!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: Joerg Heinemann (heinemannj) # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # Source: https://github.com/smallstep/certificates source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" color verb_ip6 catch_errors setting_up_container network_check update_os setup_deb822_repo \ "smallstep" \ "https://packages.smallstep.com/keys/apt/repo-signing-key.gpg" \ "https://packages.smallstep.com/stable/debian" \ "debs" \ "main" msg_info "Installing step-ca and step-cli" $STD apt install -y step-ca step-cli STEPHOME="/root/.step" export STEPPATH=/etc/step-ca export STEPHOME=$STEPHOME sed -i '1i export STEPPATH=/etc/step-ca' /etc/profile sed -i '1i export STEPHOME=/root/.step' /etc/profile setcap CAP_NET_BIND_SERVICE=+eip $(which step-ca) $STD useradd --user-group --system --home $(step path) --shell /bin/false step msg_ok "Installed step-ca and step-cli" DomainName="$(hostname -d)" PKIName="$(prompt_input "Enter PKIName" "MyHomePKI" 30)" PKIProvisioner="$(prompt_input "Enter PKIProvisioner" "pki@$DomainName" 30)" AcmeProvisioner="$(prompt_input "Enter AcmeProvisioner" "acme@$DomainName" 30)" X509MinDur="$(prompt_input "Enter X509MinDur" "48h" 30)" X509MaxDur="$(prompt_input "Enter X509MaxDur" "87600h" 30)" X509DefaultDur="$(prompt_input "Enter X509DefaultDur" "168h" 30)" msg_info "Initializing step-ca" DeploymentType="standalone" FQDN="$(hostname -f)" IP="${LOCAL_IP}" LISTENER=":443" EncryptionPwdDir="$(step path)/encryption" PwdFile="$EncryptionPwdDir/ca.pwd" ProvisionerPwdFile="$EncryptionPwdDir/provisioner.pwd" mkdir -p "$EncryptionPwdDir" gpg -q --gen-random --armor 2 32 >"$PwdFile" gpg -q --gen-random --armor 2 32 >"$ProvisionerPwdFile" $STD step ca init --deployment-type="$DeploymentType" --ssh --name="$PKIName" --dns="$FQDN" --dns="$IP" --address="$LISTENER" --provisioner="$PKIProvisioner" --password-file="$PwdFile" --provisioner-password-file="$ProvisionerPwdFile" ln -s "$PwdFile" "$(step path)/password.txt" chown -R step:step $(step path) chmod -R 700 $(step path) $STD step ca provisioner add "$AcmeProvisioner" --type ACME --admin-name "$AcmeProvisioner" $STD step ca provisioner update "$PKIProvisioner" --x509-min-dur="$X509MinDur" --x509-max-dur="$X509MaxDur" --x509-default-dur="$X509DefaultDur" --allow-renewal-after-expiry $STD step ca provisioner update "$AcmeProvisioner" --x509-min-dur="$X509MinDur" --x509-max-dur="$X509MaxDur" --x509-default-dur="$X509DefaultDur" --allow-renewal-after-expiry $STD step certificate install --all $(step path)/certs/root_ca.crt $STD update-ca-certificates msg_ok "Initialized step-ca" msg_info "Start step-ca as a Daemon" cat <<'EOF' >/etc/systemd/system/step-ca.service [Unit] Description=step-ca service Documentation=https://smallstep.com/docs/step-ca Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production After=network-online.target Wants=network-online.target StartLimitIntervalSec=30 StartLimitBurst=3 ConditionFileNotEmpty=/etc/step-ca/config/ca.json ConditionFileNotEmpty=/etc/step-ca/password.txt [Service] Type=simple User=step Group=step Environment=STEPPATH=/etc/step-ca WorkingDirectory=/etc/step-ca ExecStart=/usr/bin/step-ca config/ca.json --password-file password.txt ExecReload=/bin/kill -USR1 $MAINPID Restart=on-failure RestartSec=5 TimeoutStopSec=30 StartLimitAction=reboot ; Process capabilities & privileges AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE SecureBits=keep-caps NoNewPrivileges=yes ; Sandboxing SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@resources @privileged RestrictNamespaces=yes LockPersonality=yes MemoryDenyWriteExecute=yes RestrictRealtime=yes RestrictSUIDSGID=yes PrivateMounts=yes ProtectControlGroups=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/etc/step-ca/db ; Read only paths ReadOnlyPaths=/etc/step-ca [Install] WantedBy=multi-user.target EOF $STD systemctl enable -q --now step-ca msg_ok "Started step-ca as a Daemon" fetch_and_deploy_gh_release "step-badger" "lukasz-lobocki/step-badger" "prebuild" "latest" "/opt/step-badger" "step-badger_Linux_x86_64.tar.gz" ln -s /opt/step-badger/step-badger /usr/local/bin/step-badger msg_info "Install step-ca Admin script" mkdir -p "$STEPHOME" cat <<'ADDON_EOF' >"$STEPHOME/step-ca-admin.sh" #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: Joerg Heinemann (heinemannj) # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE function header_info() { clear cat <<"EOF" __ ___ __ _ _____/ /____ ____ _________ _ / | ____/ /___ ___ (_)___ / ___/ __/ _ \/ __ \______/ ___/ __ `/ / /| |/ __ / __ `__ \/ / __ \ (__ ) /_/ __/ /_/ /_____/ /__/ /_/ / / ___ / /_/ / / / / / / / / / / /____/\__/\___/ .___/ \___/\__,_/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/ /_/ EOF } function die() { echo -e "\n${BL}[ERROR]${GN} ${RD}${1}${CL}\n" exit } function success() { echo -e "${BL}[SUCCESS]${GN} ${1}${CL}\n" exit } function whiptail_menu() { MENU_ARRAY=() MSG_MAX_LENGTH=0 while read -r TAG ITEM; do OFFSET=2 ((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=${#ITEM}+OFFSET MENU_ARRAY+=("$TAG" "$ITEM " "OFF") done < <(echo "$1") } function x509_list() { CERT_LIST="" cp --recursive --force "$(step path)/db/"* "$STEPHOME/db-copy/" cp --recursive --force "$(step path)/certs/"* "$STEPHOME/certs/ca/" if [[ $(step-badger x509Certs "${STEPHOME}/db-copy" 2>/dev/null) ]]; then CERT_LIST=$(step-badger x509Certs ${STEPHOME}/db-copy 2>/dev/null) fi } function ssh_list() { CERT_LIST="" cp --recursive --force "$(step path)/db/"* "$STEPHOME/db-copy/" cp --recursive --force "$(step path)/certs/"* "$STEPHOME/certs/ca/" if [[ $(step-badger sshCerts "${STEPHOME}/db-copy" 2>/dev/null) ]]; then CERT_LIST=$(step-badgersshCerts ${STEPHOME}/db-copy 2>/dev/null) fi } function x509_serial_to_cn() { x509_list CN="$(echo "${CERT_LIST}" | grep "${SERIAL_NUMBER}" | awk '{print $2}' | sed 's/CN=//g')" CRT="$STEPHOME/certs/x509/$CN.crt" KEY="$STEPHOME/certs/x509/$CN.key" if ! [[ -f ${CRT} ]]; then die "Certificate ${CRT} not found!" elif ! [[ -f ${KEY} ]]; then die "Private Key ${KEY} not found!" fi } function x509_revoke() { # shellcheck disable=SC2206 SERIAL_NUMBER_ARRAY=(${CERT_SERIAL_NUMBERS}) for SERIAL_NUMBER in "${SERIAL_NUMBER_ARRAY[@]}"; do echo -e "${BL}[Info]${GN} Revoke x509 Certificate with Serial Number ${BL}${SERIAL_NUMBER}${GN}:${CL}" echo TOKEN=$(step ca token --provisioner="$PROVISIONER" --provisioner-password-file="$PROVISIONER_PASSWORD" --revoke "${SERIAL_NUMBER}") step ca revoke --token "$TOKEN" "${SERIAL_NUMBER}" || die "Failed to revoke certificate!" echo done success "Finished." } function x509_renew() { # shellcheck disable=SC2206 SERIAL_NUMBER_ARRAY=(${CERT_SERIAL_NUMBERS}) for SERIAL_NUMBER in "${SERIAL_NUMBER_ARRAY[@]}"; do echo -e "${BL}[Info]${GN} Renew x509 Certificate with Serial Number ${BL}${SERIAL_NUMBER}${GN}:${CL}" echo x509_serial_to_cn step ca renew "${CRT}" "${KEY}" --force || die "Failed to renew certificate!" echo done success "Finished." } function x509_inspect() { # shellcheck disable=SC2206 SERIAL_NUMBER_ARRAY=(${CERT_SERIAL_NUMBERS}) for SERIAL_NUMBER in "${SERIAL_NUMBER_ARRAY[@]}"; do echo -e "${BL}[Info]${GN} Inspect x509 Certificate with Serial Number ${BL}${SERIAL_NUMBER}${GN}:${CL}\n" x509_serial_to_cn step certificate inspect "${CRT}" || die "Failed to inspect certificate!" if ! [[ $(step certificate inspect "${CRT}" | grep "${SERIAL_NUMBER}") ]]; then die "Serial Number ${SERIAL_NUMBER} mismatch!" fi echo -e "\n${BL}[Info]${GN} Public Key:${CL}\n" cat "${CRT}" echo -e "\n${BL}[Info]${GN} Private Key:${CL}\n" cat "${KEY}" echo done success "Finished." } function x509_request() { FQDN="" SAN="" while true; do FQDN=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Certificate Signing Request (CSR)" --inputbox '\nFQDN (e.g. MyLXC.example.com)' 10 50 "$FQDN" 3>&1 1>&2 2>&3) IP=$(dig +short "$FQDN") if [[ -z "$IP" ]]; then die "Resolution failed for $FQDN!" fi HOST=$(echo "$FQDN" | awk -F'.' '{print $1}') IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Certificate Signing Request (CSR)" --inputbox '\nIP Address (e.g. x.x.x.x)' 10 50 "$IP" 3>&1 1>&2 2>&3) HOST=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Certificate Signing Request (CSR)" --inputbox '\nHostname (e.g. MyHostName)' 10 50 "$HOST" 3>&1 1>&2 2>&3) SAN=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Certificate Signing Request (CSR)" --inputbox '\nSubject Alternative Name(s) (SAN) (e.g. myapp-1.example.com, myapp-2.example.com)' 10 50 "$SAN" 3>&1 1>&2 2>&3) VALID_TO=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Certificate Signing Request (CSR)" --inputbox '\nValidity (e.g. 2034-01-31T00:00:00Z)' 10 50 "2034-01-31T00:00:00Z" 3>&1 1>&2 2>&3) # shellcheck disable=SC2034 if whiptail_yesno=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Certificate Signing Request (CSR)" --yesno "Continue with below?\n FQDN: $FQDN Hostname: $HOST IP Address: $IP Subject Alternative Name(s) (SAN): $SAN Validity: $VALID_TO" --no-button "Change" --yes-button "Continue" 15 70 3>&1 1>&2 2>&3); then break fi done echo -e "${BL}[Info]${GN} Request x509 Certificate with subject ${BL}${FQDN}${GN}:${CL}" echo CRT="$STEPHOME/certs/x509/$FQDN.crt" KEY="$STEPHOME/certs/x509/$FQDN.key" SAN="$FQDN, $HOST, $IP, $SAN" IFS=', ' read -r -a array <<< "$SAN" for element in "${array[@]}" do SAN_ARRAY+=(--san "$element") done step ca certificate "$FQDN" "$CRT" "$KEY" \ --provisioner="$PROVISIONER" \ --provisioner-password-file="$PROVISIONER_PASSWORD" \ --not-after="$VALID_TO" \ "${SAN_ARRAY[@]}" \ || die "Failed to request certificate!" echo -e "\n${BL}[Info]${GN} Inspect Certificate:${CL}\n" step certificate inspect "${CRT}" || die "Failed to inspect certificate!" echo -e "\n${BL}[Info]${GN} Public Key:${CL}\n" cat "${CRT}" echo -e "\n${BL}[Info]${GN} Private Key:${CL}\n" cat "${KEY}" echo success "Finished." } set -eEuo pipefail # shellcheck disable=SC2034 # shellcheck disable=SC2116 # shellcheck disable=SC2028 YW=$(echo "\033[33m") # shellcheck disable=SC2116 # shellcheck disable=SC2028 BL=$(echo "\033[36m") # shellcheck disable=SC2116 # shellcheck disable=SC2028 RD=$(echo "\033[01;31m") # shellcheck disable=SC2034 CM='\xE2\x9C\x94\033' # shellcheck disable=SC2116 # shellcheck disable=SC2028 GN=$(echo "\033[1;92m") # shellcheck disable=SC2116 # shellcheck disable=SC2028 CL=$(echo "\033[m") # Telemetry # shellcheck disable=SC1090 source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) 2>/dev/null || true declare -f init_tool_telemetry &>/dev/null && init_tool_telemetry "step-ca-admin" "step-ca" header_info mkdir --parents "$STEPHOME/db-copy/" mkdir --parents "$STEPHOME/certs/ca/_archive/" mkdir --parents "$STEPHOME/certs/ssh/_archive/" mkdir --parents "$STEPHOME/certs/x509/_archive/" PROVISIONER=$(jq '.authority.provisioners.[] | select(.type=="JWK") | .name' "$(step path)"/config/ca.json) PROVISIONER="${PROVISIONER#\"}" PROVISIONER="${PROVISIONER%\"}" PROVISIONER_PASSWORD=$(step path)/encryption/provisioner.pwd whiptail --backtitle "Proxmox VE Helper Scripts" --title "step-ca Admin" --yesno "This will maintain step-ca issued x509 and ssh Certificates. Proceed?" 10 58 MENU_ARRAY=("x509" "Maintain x509 Certificates." "ON") MENU_ARRAY+=("ssh" "Maintain ssh Certificates." "OFF") CERT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "step-ca Admin" --radiolist "\nSelect Certificate Type:" 16 48 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"') [[ -z ${CERT_TYPE} ]] && die "No Certificate Type selected!" case ${CERT_TYPE} in ("x509") x509_list CERT_LIST=$(echo "$CERT_LIST" | awk 'NR>1 {print $1 " " $2 "|" $3 "|" $4 "|" $5}') if [[ $CERT_LIST ]]; then whiptail_menu "$CERT_LIST" else MENU_ARRAY=() MSG_MAX_LENGTH=2 fi MENU_ARRAY+=("" "Create a new Certificate" "OFF") CERT_SERIAL_NUMBERS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Certificates on $(hostname)" --checklist "\nSelect Certificate(s) to maintain:\n" 16 $((MSG_MAX_LENGTH + 55)) 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"') [[ -z ${CERT_SERIAL_NUMBERS} ]] && x509_request MENU_ARRAY=("Renew" "Renew x509 Certificates." "ON") MENU_ARRAY+=("Revoke" "Revoke x509 Certificates." "OFF") MENU_ARRAY+=("Inspect" "Inspect x509 Certificates." "OFF") CERT_MAINTENANCE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "step-ca Admin" --radiolist "\nSelect Maintenance Type:" 16 48 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"') case ${CERT_MAINTENANCE} in ("Renew") x509_renew "${CERT_SERIAL_NUMBERS[@]}" ;; ("Revoke") x509_revoke "${CERT_SERIAL_NUMBERS[@]}" ;; ("Inspect") x509_inspect "${CERT_SERIAL_NUMBERS[@]}" ;; *) die "Unsupported CERT_MAINTENANCE Option!" ;; esac ;; ("ssh") die "Maintain ssh Certificates - To be implemented in future" ;; *) die "Unsupported CERT_TYPE Option!" ;; esac ADDON_EOF chmod 700 "$STEPHOME/step-ca-admin.sh" msg_ok "Installed step-ca Admin script" motd_ssh customize cleanup_lxc