#!/usr/bin/env bash set -eo pipefail [[ $SSHCOMMAND_TRACE ]] && set -x shopt -s nocasematch # For case insensitive string matching, for the first parameter if [[ -f /etc/defaults/sshcommand ]]; then # shellcheck disable=SC1091 source /etc/defaults/sshcommand fi declare SSHCOMMAND_VERSION="" declare SSHCOMMAND_CHECK_DUPLICATE_FINGERPRINT=${SSHCOMMAND_CHECK_DUPLICATE_FINGERPRINT:="true"} declare SSHCOMMAND_CHECK_DUPLICATE_NAME=${SSHCOMMAND_CHECK_DUPLICATE_NAME:="true"} cmd-help() { declare desc="Shows help information for a command" declare args="$*" if [[ "$args" ]]; then for cmd; do true; done # last arg local fn="sshcommand-$cmd" fn-info "$fn" 1 fi } fn-args() { declare desc="Inspect a function's arguments" local argline argline=$(type "$1" | grep declare | grep -v "declare desc" | head -1) echo -e "${argline// /"\\n"}" | awk -F= '/=/{print "<"$1">"}' | tr "\\n" " " } fn-desc() { declare desc="Inspect a function's description" desc="" eval "$(type "$1" | grep desc | head -1)" echo "$desc" } fn-info() { declare desc="Inspects a function" declare fn="$1" showsource="$2" local fn_name="${1//sshcommand-/}" echo "$fn_name $(fn-args "$fn")" echo " $(fn-desc "$fn")" echo if [[ "$showsource" ]]; then type "$fn" | tail -n +2 echo fi } fn-print-os-id() { declare desc="Returns the release id of the operating system" local OSRELEASE="${SSHCOMMAND_OSRELEASE:="/etc/os-release"}" if [[ -f $OSRELEASE ]]; then sed -n 's#^ID=\(.*\)#\1#p' "$OSRELEASE" | tr -d '"' else echo unknown fi return 0 } fn-adduser() { declare desc="Add a user to the system" local l_user l_platform l_user=$1 l_platform="$(fn-print-os-id)" case $l_platform in alpine) adduser -D -g "" -s /bin/bash "$l_user" passwd -u "$l_user" ;; debian* | ubuntu | raspbian*) adduser --disabled-password --gecos "" "$l_user" ;; arch | amzn) useradd -m -s /bin/bash "$l_user" usermod -L -aG "$l_user" "$l_user" ;; *) useradd -m -s /bin/bash "$l_user" groupadd "$l_user" usermod -L -aG "$l_user" "$l_user" ;; esac } log-fail() { declare desc="Log fail formatter" echo "$@" 1>&2 exit 1 } log-verbose() { declare desc="Log verbose formatter" if [[ -n "$SSHCOMMAND_VERBOSE_OUTPUT" ]]; then echo "$@" fi } sshcommand-create() { declare desc="Creates a local system user and installs sshcommand skeleton" declare USER="$1" COMMAND="$2" local USERHOME if [[ -z "$USER" ]] || [[ -z "$COMMAND" ]]; then log-fail "Usage: sshcommand create" "$(fn-args "sshcommand-create")" fi if id -u "$USER" >/dev/null 2>&1; then log-verbose "User '$USER' already exists" else fn-adduser "$USER" fi USERHOME=$(sh -c "echo ~$USER") mkdir -p "$USERHOME/.ssh" touch "$USERHOME/.ssh/authorized_keys" chmod 0700 "$USERHOME/.ssh/authorized_keys" echo "$COMMAND" >"$USERHOME/.sshcommand" chown -R "$USER" "$USERHOME" } sshcommand-acl-add() { declare desc="Adds named SSH key to user from STDIN or argument" declare USER="$1" NAME="$2" KEY_FILE="$3" local ALLOWED_KEYS FINGERPRINT KEY KEY_FILE KEY_PREFIX NEW_KEY USERHOME if [[ -z "$USER" ]] || [[ -z "$NAME" ]]; then log-fail "Usage: sshcommand acl-add" "$(fn-args "sshcommand-acl-add")" fi getent passwd "$USER" >/dev/null || false USERHOME=$(sh -c "echo ~$USER") NEW_KEY=$(grep "NAME=\\\\\"$NAME"\\\\\" "$USERHOME/.ssh/authorized_keys" || true) if [[ "$SSHCOMMAND_CHECK_DUPLICATE_NAME" == "true" ]] && [[ -n "$NEW_KEY" ]]; then log-fail "Duplicate ssh key name" fi if [[ -z "$KEY_FILE" ]]; then KEY=$(cat) else KEY=$(cat "$KEY_FILE") fi local count count="$(wc -l <<<"$KEY")" [[ "$count" -eq 1 ]] || log-fail "Too many keys provided, set one per invocation of sshcommand acl-add " FINGERPRINT=$(ssh-keygen -lf <(echo "command=\"dummy to fail when options already exist\" $KEY") | awk '{print $2}') if [[ ! "$FINGERPRINT" =~ :.* ]]; then log-fail "Invalid ssh public key" fi if [[ "$SSHCOMMAND_CHECK_DUPLICATE_FINGERPRINT" == "true" ]] && grep -qF "$FINGERPRINT" "$USERHOME/.ssh/authorized_keys"; then log-fail "Duplicate ssh public key specified" fi ALLOWED_KEYS="${SSHCOMMAND_ALLOWED_KEYS:="no-agent-forwarding,no-user-rc,no-X11-forwarding,no-port-forwarding"}" KEY_PREFIX="command=\"FINGERPRINT=$FINGERPRINT NAME=\\\"$NAME\\\" \`cat $USERHOME/.sshcommand\` \$SSH_ORIGINAL_COMMAND\",$ALLOWED_KEYS" echo "$KEY_PREFIX $KEY" >>"$USERHOME/.ssh/authorized_keys" chmod 0700 "$USERHOME/.ssh/authorized_keys" echo "$FINGERPRINT" } sshcommand-acl-remove() { declare desc="Removes SSH key by name" declare USER="$1" NAME="$2" local USERHOME if [[ -z "$USER" ]] || [[ -z "$NAME" ]]; then log-fail "Usage: sshcommand acl-remove" "$(fn-args "sshcommand-acl-remove")" fi getent passwd "$USER" >/dev/null || false USERHOME=$(sh -c "echo ~$USER") sed --in-place "/ NAME=\\\\\"$NAME\\\\\" /d" "$USERHOME/.ssh/authorized_keys" chmod 0700 "$USERHOME/.ssh/authorized_keys" } sshcommand-acl-remove-by-fingerprint() { declare desc="Removes SSH key by fingerprint" declare USER="$1" FINGERPRINT="$2" local USERHOME if [[ -z "$USER" ]] || [[ -z "$FINGERPRINT" ]]; then log-fail "Usage: sshcommand acl-remove-by-fingerprint" "$(fn-args "sshcommand-acl-remove-by-fingerprint")" fi getent passwd "$USER" >/dev/null || false USERHOME=$(sh -c "echo ~$USER") # shellcheck disable=SC1117 sed --in-place "\#\"FINGERPRINT=$FINGERPRINT #d" "$USERHOME/.ssh/authorized_keys" chmod 0700 "$USERHOME/.ssh/authorized_keys" } sshcommand-list() { declare desc="Lists SSH keys by user, an optional name and a optional output format (JSON)" declare userhome USER="$1" NAME="$2" OUTPUT_TYPE="${3:-$2}" [[ -z "$USER" ]] && log-fail "Usage: sshcommand list" "$(fn-args "sshcommand-list")" getent passwd "$USER" >/dev/null || log-fail "\"$USER\" is not a user on this system" userhome=$(sh -c "echo ~$USER") [[ -e "$userhome/.ssh/authorized_keys" ]] || log-fail "authorized_keys not found for $USER" [[ -s "$userhome/.ssh/authorized_keys" ]] || log-fail "authorized_keys is empty for $USER" if [[ -n "$OUTPUT_TYPE" ]] && [[ "$OUTPUT_TYPE" == "json" ]]; then data=$(sed --silent --regexp-extended \ 's/^command="FINGERPRINT=(\S+) NAME=(\\"|)(.*)\2 `.*",(\S+).*/{ "fingerprint": "\1", "name": "\3", "SSHCOMMAND_ALLOWED_KEYS": "\4" }/p' \ "$userhome/.ssh/authorized_keys" | tr '\n' ',' | sed '$s/,$/\n/') if [[ -n "$NAME" ]]; then echo "[${data}]" | jq -cM --arg NAME "$NAME" 'map( select (.name == $NAME) )' else echo "[${data}]" fi else OUTPUT="$(sed --silent --regexp-extended \ 's/^command="FINGERPRINT=(\S+) NAME=(\\"|)(.*)\2 `.*",(\S+).*/\1 NAME="\3" SSHCOMMAND_ALLOWED_KEYS="\4"/p' \ "$userhome/.ssh/authorized_keys")" if [[ -n "$NAME" ]]; then echo "$OUTPUT" | grep "NAME=\"$NAME\"" else echo "$OUTPUT" fi fi } sshcommand-help() { declare desc="Shows help information" declare COMMAND="$1" if [[ -n "$COMMAND" ]]; then cmd-help "$COMMAND" return 0 fi echo "sshcommand ${SSHCOMMAND_VERSION}" echo "" printf " %-25s %-30s %s\\n" "create" "$(fn-args "sshcommand-create")" "$(fn-desc "sshcommand-create")" printf " %-25s %-30s %s\\n" "acl-add" "$(fn-args "sshcommand-acl-add")" "$(fn-desc "sshcommand-acl-add")" printf " %-25s %-30s %s\\n" "acl-remove" "$(fn-args "sshcommand-acl-remove")" "$(fn-desc "sshcommand-acl-remove")" printf " %-25s %-30s %s\\n" "acl-remove-by-fingerprint" "$(fn-args "sshcommand-acl-remove-by-fingerprint")" "$(fn-desc "sshcommand-acl-remove-by-fingerprint")" printf " %-25s %-30s %s\\n" "list" "$(fn-args "sshcommand-list")" "$(fn-desc "sshcommand-list")" printf " %-25s %-30s %s\\n" "help" "$(fn-args "sshcommand-help")" "$(fn-desc "sshcommand-help")" printf " %-25s %-30s %s\\n" "version" "$(fn-args "sshcommand-version")" "$(fn-desc "sshcommand-version")" } sshcommand-version() { declare desc="Shows version" echo "sshcommand ${SSHCOMMAND_VERSION}" } main() { declare COMMAND_SUFFIX="$1" if [[ -z "$COMMAND_SUFFIX" ]]; then sshcommand-help "$@" exit 1 fi if [[ "$COMMAND_SUFFIX" == "-h" ]] || [[ "$COMMAND_SUFFIX" == "--help" ]]; then COMMAND_SUFFIX="help" fi if [[ "$COMMAND_SUFFIX" == "-v" ]] || [[ "$COMMAND_SUFFIX" == "--version" ]]; then COMMAND_SUFFIX="version" fi local cmd="sshcommand-$COMMAND_SUFFIX" shift 1 if declare -f "$cmd" >/dev/null; then $cmd "$@" else log-fail "Invalid command" fi } # shellcheck disable=SC2128 if [[ "$0" == "$BASH_SOURCE" ]]; then main "$@" fi