#!/usr/bin/env bash # Handles indexing and SSH keys of machines I # have access on MACHINES_HOME=$HOME MACHINES_CONFIG=$HOME/.config/machines MACHINES_API=https://machines.frogeye.fr mkdir -p "$MACHINES_HOME" &> /dev/null mkdir -p "$MACHINES_CONFIG" &> /dev/null # COMMON function prompt { # text while true do read -r -p "$1 [yn] " yn case $yn in [Yy]* ) return 1;; [Nn]* ) return 0;; * ) echo "Please answer y or n.";; esac done } # From https://gist.github.com/cdown/1163649 urlencode() { # string old_lc_collate=$LC_COLLATE LC_COLLATE=C local length="${#1}" for (( i = 0; i < length; i++ )); do local c="${1:i:1}" case $c in [a-zA-Z0-9.~_-]) printf "%s" "$c" ;; *) printf '%%%02X' "'$c" ;; esac done LC_COLLATE=$old_lc_collate } urldecode() { # string local url_encoded="${1//+/ }" printf '%b' "${url_encoded//%/\\x}" } # API ACCESS function _machines-api { route=$1 shift wget "$MACHINES_API/$route" --content-on-error --quiet --output-document=- "$@" result=$? if [ $result != 0 ]; then echo "[ERROR] wget returned $result for route $route" 1>&2; exit 2 fi } function _machines-apiToken { read -r -p 'TOTP token: ' token _machines-api "$@" --header="X-TOTP: $token" } function _machines-apiSigned { _machines-ensureAdmin _machines-api "$@" --certificate="$MACHINES_CONFIG/machines.crt" --private-key="$MACHINES_CONFIG/machines.key" } # APPLICATION KEYS & CERTIFICATE function _machines-pubFromCrt { openssl x509 -in "$MACHINES_CONFIG/machines.crt" -pubkey -noout > "$MACHINES_CONFIG/machines.pub" } function _machines-verifyCertificate { return if openssl verify "$MACHINES_CONFIG/machines.crt" | grep -v 'error 18' | grep 'error' --quiet; then echo "[ERROR] Invalid certificate" 1>&2; exit 1 fi } function _machines-ensurePub { if [ ! -f "$MACHINES_CONFIG/machines.crt" ]; then CERT_FILE=$(mktemp) echo "[INFO] Downloading certificate..." _machines-api cert > "$CERT_FILE" openssl x509 -fingerprint -in "$CERT_FILE" | grep Fingerprint --color=never prompt "Is this correct ?" if [ $? == 1 ]; then mv "$CERT_FILE" "$MACHINES_CONFIG/machines.crt" &> /dev/null else echo "[ERROR] Certificate rejected." 1>&2; exit 1 fi fi _machines-verifyCertificate if [ ! -f "$MACHINES_CONFIG/machines.pub" ]; then _machines-pubFromCrt fi return 0 } function _machines-ensureAdmin { if [ ! -f "$MACHINES_CONFIG/machines.key" ]; then echo "[ERROR] You need have to have the private key to do that" 1>&2; exit 1 fi } # SSH ACCESS KEYS function _machines-signAkey { # network KEY_FILE=$(mktemp) SIGN_FILE=$(mktemp) _machines-apiSigned "akey/$1?unsigned" > "$KEY_FILE" openssl dgst -sha256 -sign "$MACHINES_CONFIG/machines.key" -out "$SIGN_FILE" "$KEY_FILE" _machines-apiSigned "akey/$1" --method=PUT --body-file="$SIGN_FILE" rm "$KEY_FILE" "$SIGN_FILE" &> /dev/null } function _machines-getAkey { # network _machines-ensurePub KEY_FILE=$(mktemp) SIGN_FILE=$(mktemp) _machines-api "akey/$1" > "$KEY_FILE" _machines-api "akey/$1?signature" > "$SIGN_FILE" md5sum "$KEY_FILE" 1>&2; md5sum "$SIGN_FILE" 1>&2; md5sum "$MACHINES_CONFIG/machines.pub" 1>&2; if openssl dgst -sha256 -verify "$MACHINES_CONFIG/machines.pub" -signature "$SIGN_FILE" "$KEY_FILE" &> /dev/null then cat "$KEY_FILE" \rm "$KEY_FILE" "$SIGN_FILE" return 0 else \rm "$KEY_FILE" "$SIGN_FILE" exit 1 fi } function _machines-updateAkey { MYKEY_FILE=$(mktemp) network=$(grep '^network=' "$MACHINES_CONFIG/this" | cut -d '=' -f 2) if _machines-getAkey "$network" > "$MYKEY_FILE" then \mv -f "$MYKEY_FILE" "$MACHINES_HOME/.ssh/authorized_keys" return 0 else cat "$MYKEY_FILE" echo "[ERROR] Authorized keys are not properly signed" 1>&2; \rm "$MYKEY_FILE" exit 1 fi } function _machines-postFile { # filename cat $1 | while read -r line; do parameter=$(echo "$line" | cut -d '=' -f 1) value="$(echo "$line" | sed 's/^[a-zA-Z0-9]\+\(\[\]\)\?=//')" echo -n "&$parameter=$(urlencode "$value")" done } function _machines-addElement { # element elementType default FILE=$(mktemp) echo -e "$3" > "$FILE" $EDITOR "$FILE" data=$(_machines-postFile "$FILE") \rm "$FILE" _machines-apiSigned "$2" --post-data "name=$1$data" } function _machines-viewElement { # element elementType _machines-apiSigned "$2/$1" } function _machines-editElement { # element elementType FILE=$(mktemp) _machines-apiSigned "$2/$1" > "$FILE" $EDITOR "$FILE" data=$(_machines-postFile "$FILE") rm "$FILE" &> /dev/null err=$(_machines-apiSigned "$2/$1" --post-data "$data") } function _machines-deleteElement { # element elementType err=$(_machines-apiSigned "$2/$1" --method=DELETE) } # USER ADMIN FUNCTIONS function machines_history { if [ -f "$MACHINES_CONFIG/lastVerifiedLog" ]; then from=$(<"$MACHINES_CONFIG/lastVerifiedLog") else from=0 fi d=$(date +%s) _machines-apiSigned log?from=$from | less if prompt "Is this OK?" then exit 1 else echo "$d" > "$MACHINES_CONFIG/lastVerifiedLog" return 0 fi } function machines_sign { machines_history echo "Signing default network authorized_keys..." _machines-signAkey _machines-apiSigned network | while read -r network; do echo "Signing network $network authorized_keys..." _machines-signAkey $network done } function machines_machine_list { _machines-apiSigned machine } function machines_network_list { _machines-apiSigned network } function machines_machine_add_help { echo "Usage: $0 machine|mac|m add MACHINE" echo echo "Arguments:" echo " MACHINE machine to add" return 0 } function machines_machine_add { # machine if [ -z "$1" ]; then machines_machine_add_help exit 1 fi _machines-addElement "$1" machine "host[]=\nnetwork=\nuserkey=\nhostkey=\nuser=" } function machines_network_add_help { echo "Usage: $0 network|net|n add NETWORK" echo echo "Arguments:" echo " NETWORK Network to add" return 0 } function machines_network_add { # network if [ -z "$1" ]; then machines_network_add_help exit 1 fi _machines-addElement "$1" network "allowed[]=\nsecure=false" } function machines_machine_view_help { echo "Usage: $0 machine|mac|m view MACHINE" echo echo "Arguments:" echo " MACHINE machine to view" return 0 } function machines_machine_view { # machine if [ -z "$1" ]; then machines_machine_view_help exit 1 fi _machines-viewElement "$1" machine } function machines_network_view_help { echo "Usage: $0 network|net|n view NETWORK" echo echo "Arguments:" echo " NETWORK Network to view" return 0 } function machines_network_view { # network if [ -z "$1" ]; then machines_network_view_help exit 1 fi _machines-viewElement "$1" network } function machines_machine_edit_help { echo "Usage: $0 machine|mac|m edit MACHINE" echo echo "Arguments:" echo " MACHINE machine to edit" return 0 } function machines_machine_edit { # machine if [ -z "$1" ]; then machines_machine_edit_help exit 1 fi _machines-editElement "$1" machine } function machines_network_edit_help { echo "Usage: $0 network|net|n edit NETWORK" echo echo "Arguments:" echo " NETWORK Network to edit" return 0 } function machines_network_edit { # network if [ -z "$1" ]; then machines_network_edit_help exit 1 fi _machines-editElement "$1" network } function machines_machine_delete_help { echo "Usage: $0 machine|mac|m delete machine" echo echo "Arguments:" echo " MACHINE machine to remove" return 0 } function machines_machine_delete { # machine if [ -z "$1" ]; then machines_machine_delete_help exit 1 fi _machines-deleteElement "$1" machine } function machines_network_delete_help { echo "Usage: $0 network|net|n delete NETWORK" echo echo "Arguments:" echo " NETWORK Network to remove" return 0 } function machines_network_delete { # network if [ -z "$1" ]; then machines_network_delete_help exit 1 fi _machines-deleteElement "$1" network } function machines_machine_help { echo "Usage: $0 machine|mac|m COMMAND" echo echo "Commands:" echo " list List all machines" echo " add Interactively add a machine" echo " view Display a machine" echo " edit Interactively edit a specified machine" echo " delete Remove a specified machine" echo " help Get help with commands" return 0 } function machines_machine { command="$1" shift if type "machines_machine_$command" &> /dev/null; then "machines_machine_$command" "$@" else machines_machine_help fi } function machines_network_help { echo "Usage: $0 network|net|n COMMAND" echo echo "Commands:" echo " list List all networks" echo " add Interactively add a network" echo " view Display a network" echo " edit Interactively edit a specified network" echo " delete Remove a specified network" echo " help Get help with commands" return 0 } function machines_network { command="$1" shift if type "machines_network_$command" &> /dev/null; then "machines_network_$command" "$@" else machines_network_help fi } machines_mac() { machines_machine "$@"; } machines_m() { machines_machine "$@"; } machines_net() { machines_network "$@"; } machines_n() { machines_network "$@"; } machines_mac_help() { machines_machine_help "$@"; } machines_m_help() { machines_machine_help "$@"; } machines_net_help() { machines_network_help "$@"; } machines_n_help() { machines_network_help "$@"; } function machines_update-all { machines_machine_list | while read -r machine; do echo "Updating $machine..." ssh "$machine" 'cd .dotfiles && git pull; machines update' & done } function machines_regen-keys { if [[ -e $MACHINES_CONFIG/machines.key || -e $MACHINES_CONFIG/machines.pub || -e $MACHINES_CONFIG/machines.crt ]]; then echo "[ERROR] Please delete the pem files manually to prove you know what you're doing." 1>&2; exit 1 else openssl genrsa -out "$MACHINES_CONFIG/machines.key" 4096 chmod 600 "$MACHINES_CONFIG/machines.key" openssl req -key "$MACHINES_CONFIG/machines.key" -new -out "$MACHINES_CONFIG/machines.csr" openssl x509 -req -days 1826 -in "$MACHINES_CONFIG/machines.csr" -signkey "$MACHINES_CONFIG/machines.key" -out "$MACHINES_CONFIG/machines.crt" _machines-pubFromCrt fi } # USER FUNCTIONS function machines_setup { if [ -e "$MACHINES_CONFIG/this.name" ]; then echo "[ERROR] This machine is already set up" 1>&2; exit 1 fi _machines-ensurePub # Variables read -r -p 'Machine name? ' name read -r -p 'Hosts (separated by spaces)? ' hosts # User key mkdir -p "$MACHINES_HOME/.ssh" &> /dev/null if [[ ! -f $MACHINES_HOME/.ssh/id_rsa || ! -f $MACHINES_HOME/.ssh/id_rsa.pub ]]; then ssh-keygen -b 4096 -C "$name@machines.frogeye.fr" -f "$MACHINES_HOME/.ssh/id_rsa" -t rsa fi userkey=$(<"$MACHINES_HOME/.ssh/id_rsa.pub") # Host key for type in ecdsa ed25519 rsa dsa; do if [ -f "/etc/ssh/ssh_host_${type}_key.pub" ]; then hostkey=$(<"/etc/ssh/ssh_host_${type}_key.pub") break fi done # Subscription data="name=$(urlencode "$name")&userkey=$(urlencode "$userkey")&hostkey=$(urlencode "$hostkey")&user=$(urlencode "$USER")" for host in $hosts; do data="$data&host[]=$(urlencode "$host")" done _machines-apiToken machine --post-data "$data" echo "$name" > "$MACHINES_CONFIG/this.name" machines_update } function machines_update { _machines-api "machine/$(cat "$MACHINES_CONFIG/this.name")" > "$MACHINES_CONFIG/this" _machines-updateAkey } function machines_totp { url=$(_machines-apiSigned totp) echo "URL : $url" echo "$url" | qrencode -o - | feh - } function machines_help { command="$1" if [ -n "$command" ]; then if type "machines_${command}_help" &> /dev/null; then shift "machines_${command}_help" "$@" return $? fi fi echo "Usage: $0 COMMAND" echo echo "User commands:" echo " setup Interactive initial setup for new machine" echo " update Update this machine" echo " help Get help with commands" echo echo "Admin commands:" echo " machine|mac|m Modify machines" echo " network|net|n Modify networks" echo " update-all Update all machines available via SSH" echo " regen-keys Regenerate system keys" echo " sign Sign recent transactions for propagation" echo " totp Get TOTP generating QR code / URL" return 0 } # MAIN command="$1" shift if type "machines_$command" &> /dev/null; then "machines_$command" "$@" else machines_help "$@" fi