dotfiles/config/scripts/machines

519 lines
14 KiB
Bash
Executable file

#!/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"
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"
if [ -f "$MACHINES_HOME/.ssh/authorized_keys.tail" ]
then
cat "$MACHINES_HOME/.ssh/authorized_keys.tail" >> "$MACHINES_HOME/.ssh/authorized_keys"
fi
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