2023-03-27 16:22:12 -07:00
#!/usr/bin/env bash
2023-04-15 19:06:43 -07:00
# @file CloudFlare SSO SSH Tunnel Setup
2023-03-27 16:22:12 -07:00
# @brief Installs and configures cloudflared for short-lived SSH certificates authenticated via SSO
# @description
# This script ensures Homebrew is installed and then uses Homebrew to ensure `cloudflared` is installed.
# After that, it connects `cloudflared` to CloudFlare Teams and sets up short-lived SSH certificates so
# that you do not have to manage SSH keys and instead use SSO (Single Sign-On) via CloudFlare Teams.
#
# **Note**: `https://install.doctor/cloudflared` points to this file.
#
# ## Variables
#
# The `SSH_DOMAIN` variable should be set to the endpoint you want to be able to SSH into. The SSH endpoint(s)
# that are created depend on what type of system is being configured. Some device types include multiple
# properties that need multiple unique SSH endpoints. The `SSH_DOMAIN` must be passed to this script or else
# it will default to `ssh.megabyte.space`.
#
# * For most installations, the configured domain will be `$(hostname).${SSH_DOMAIN}`
# * If Qubes is being configured, then the configured domain will be `$(hostname)-qube.${SSH_DOMAIN}`
# * If [EasyEngine](https://easyengine.io/) is installed, then each domain setup with EasyEngine is configured to have an `ssh` subdomain (i.e. `ssh.example.com` for `example.com`)
#
# There are other optional variables that can be customized as well:
#
# * `CF_TUNNEL_NAME` - The ID to assign to the tunnel that `cloudflared` creates (`default-ssh-tunnel` by default)
#
# ## Notes
#
# Since the certificates are "short-lived", you will have to periodically re-authenticate against the
# SSO authentication endpoint that is hosted by CloudFlare Teams (or an identity provider of your choosing).
# This script will likely only work on AMD x64 devices.
#
# Some of the commands are conditionally run based on whether or not the `CRONTAB_JOB` environment variable is set.
# This is to accomodate EasyEngine installations where the list of SSH endpoints is variable. Both the initial
# setup and updates are applied using this script (via a cronjob that does not need to run initialization tasks during
# the cronjobs).
#
2023-11-12 20:53:44 -08:00
# ## TODO
#
# * Automatically add UFW exceptions using [this script](https://github.com/Paul-Reed/cloudflare-ufw)
#
2023-03-27 16:22:12 -07:00
# ## Links
#
# [SSH with short-lived certificates](https://developers.cloudflare.com/cloudflare-one/tutorials/ssh-cert-bastion/)
2023-11-27 14:53:29 -08:00
# @description This function logs with style using Gum if it is installed, otherwise it uses `echo`. It is also capable of leveraging Glow to render markdown.
# When Glow is not installed, it uses `cat`. The following sub-commands are available:
#
# | Sub-Command | Description |
# |-------------|-----------------------------------------------------------------------------------------------------|
# | `error` | Logs a bright red error message |
# | `info` | Logs a regular informational message |
# | `md` | Tries to render the specified file using `glow` if it is installed and uses `cat` as a fallback |
# | `prompt` | Alternative that logs a message intended to describe an upcoming user input prompt |
# | `star` | Alternative that logs a message that starts with a star icon |
# | `start` | Same as `success` |
# | `success` | Logs a success message that starts with green checkmark |
# | `warn` | Logs a bright yellow warning message |
2023-11-12 20:53:44 -08:00
# @example
# logger info "An informative log"
2023-11-27 14:53:29 -08:00
# @example
# logger md ~/README.md
2023-11-12 20:53:44 -08:00
logg( ) {
TYPE = " $1 "
MSG = " $2 "
if [ " $TYPE " = = 'error' ] ; then
if command -v gum > /dev/null; then
gum style --border= "thick" " $( gum style --foreground= "#ff0000" "✖" ) $( gum style --bold --background= "#ff0000" --foreground= "#ffffff" " ERROR " ) $( gum style --bold " $MSG " ) "
else
echo " ERROR: $MSG "
2023-03-27 16:23:46 -07:00
fi
2023-11-12 20:53:44 -08:00
elif [ " $TYPE " = = 'info' ] ; then
if command -v gum > /dev/null; then
gum style " $( gum style --foreground= "#00ffff" "○" ) $( gum style --faint " $MSG " ) "
else
echo " INFO: $MSG "
fi
elif [ " $TYPE " = = 'md' ] ; then
if command -v glow > /dev/null; then
glow " $MSG "
else
cat " $MSG "
fi
elif [ " $TYPE " = = 'prompt' ] ; then
if command -v gum > /dev/null; then
gum style " $( gum style --foreground= "#00008b" "▶" ) $( gum style --bold " $MSG " ) "
else
echo " PROMPT: $MSG "
fi
elif [ " $TYPE " = = 'star' ] ; then
if command -v gum > /dev/null; then
gum style " $( gum style --foreground= "#d1d100" "◆" ) $( gum style --bold " $MSG " ) "
else
echo " STAR: $MSG "
fi
elif [ " $TYPE " = = 'start' ] ; then
if command -v gum > /dev/null; then
gum style " $( gum style --foreground= "#00ff00" "▶" ) $( gum style --bold " $MSG " ) "
else
echo " START: $MSG "
fi
elif [ " $TYPE " = = 'success' ] ; then
if command -v gum > /dev/null; then
2023-11-27 15:12:15 -08:00
gum style " $( gum style --foreground= "#00ff00" "✔" ) $( gum style --bold " $MSG " ) "
2023-11-12 20:53:44 -08:00
else
echo " SUCCESS: $MSG "
fi
elif [ " $TYPE " = = 'warn' ] ; then
if command -v gum > /dev/null; then
gum style " $( gum style --foreground= "#d1d100" "◆" ) $( gum style --bold --background= "#ffff00" --foreground= "#000000" " WARNING " ) $( gum style --bold " $MSG " ) "
else
echo " WARNING: $MSG "
fi
else
if command -v gum > /dev/null; then
gum style " $( gum style --foreground= "#00ff00" "▶" ) $( gum style --bold " $TYPE " ) "
else
echo " $MSG "
fi
fi
}
2023-11-12 22:32:18 -08:00
2023-11-27 14:53:29 -08:00
# @description This function ensures dependencies like `git` and `curl` are installed. More specifically, this function will:
#
# 1. Check if `curl`, `git`, `expect`, `rsync`, and `unbuffer` are on the system
# 2. If any of the above are missing, it will then use the appropriate system package manager to satisfy the requirements. *Note that some of the requirements are not scanned for in order to keep it simple and fast.*
# 3. On macOS, the official Xcode Command Line Tools are installed.
ensureBasicDeps( ) {
2023-11-29 23:18:43 -08:00
if ! command -v curl > /dev/null || ! command -v git > /dev/null || ! command -v expect > /dev/null || ! command -v rsync > /dev/null || ! command -v unbuffer > /dev/null; then
2023-11-27 14:53:29 -08:00
if command -v apt-get > /dev/null; then
### Debian / Ubuntu
logg info 'Running sudo apt-get update' && sudo apt-get update
logg info 'Running sudo apt-get install -y build-essential curl expect git rsync procps file' && sudo apt-get install -y build-essential curl expect git rsync procps file
elif command -v dnf > /dev/null; then
### Fedora
logg info 'Running sudo dnf groupinstall -y "Development Tools"' && sudo dnf groupinstall -y 'Development Tools'
logg info 'Running sudo dnf install -y curl expect git rsync procps-ng file' && sudo dnf install -y curl expect git rsync procps-ng file
elif command -v yum > /dev/null; then
### CentOS
logg info 'Running sudo yum groupinstall -y "Development Tools"' && sudo yum groupinstall -y 'Development Tools'
logg info 'Running sudo yum install -y curl expect git rsync procps-ng file' && sudo yum install -y curl expect git rsync procps-ng file
elif command -v pacman > /dev/null; then
### Archlinux
logg info 'Running sudo pacman update' && sudo pacman update
logg info 'Running sudo pacman -Syu base-devel curl expect git rsync procps-ng file' && sudo pacman -Syu base-devel curl expect git rsync procps-ng file
elif command -v zypper > /dev/null; then
### OpenSUSE
logg info 'Running sudo zypper install -yt pattern devel_basis' && sudo zypper install -yt pattern devel_basis
logg info 'Running sudo zypper install -y curl expect git rsync procps file' && sudo zypper install -y curl expect git rsync procps file
elif command -v apk > /dev/null; then
### Alpine
logg info 'Running sudo apk add build-base curl expect git rsync ruby procps file' && sudo apk add build-base curl expect git rsync ruby procps file
elif [ -d /Applications ] && [ -d /Library ] ; then
### macOS
logg info "Ensuring Xcode Command Line Tools are installed.."
if ! xcode-select -p >/dev/null 2>& 1; then
logg info "Command Line Tools for Xcode not found"
### This temporary file prompts the 'softwareupdate' utility to list the Command Line Tools
touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress;
XCODE_PKG = " $( softwareupdate -l | grep "\*.*Command Line" | tail -n 1 | sed 's/^[^C]* //' ) "
logg info "Installing from softwareupdate" && softwareupdate -i " $XCODE_PKG " && logg success " Successfully installed $XCODE_PKG "
fi
elif [ [ " $OSTYPE " = = 'cygwin' ] ] || [ [ " $OSTYPE " = = 'msys' ] ] || [ [ " $OSTYPE " = = 'win32' ] ] ; then
### Windows
logg info 'Running choco install -y curl expect git rsync' && choco install -y curl expect git rsync
elif command -v nix-env > /dev/null; then
### NixOS
logg warn "TODO - Add support for NixOS"
elif [ [ " $OSTYPE " = = 'freebsd' * ] ] ; then
### FreeBSD
logg warn "TODO - Add support for FreeBSD"
elif command -v pkg > /dev/null; then
### Termux
logg warn "TODO - Add support for Termux"
elif command -v xbps-install > /dev/null; then
### Void
logg warn "TODO - Add support for Void"
2023-11-12 21:20:10 -08:00
fi
2023-11-12 20:53:44 -08:00
fi
2023-11-27 14:53:29 -08:00
}
ensureBasicDeps
2023-12-09 04:57:12 -08:00
### Ensure Homebrew is loaded
loadHomebrew( ) {
2023-11-27 14:53:29 -08:00
if ! command -v brew > /dev/null; then
2023-12-09 04:57:12 -08:00
if [ -f /usr/local/bin/brew ] ; then
logg info "Using /usr/local/bin/brew" && eval " $( /usr/local/bin/brew shellenv) "
if [ -f " ${ HOMEBREW_PREFIX :- /opt/homebrew } /bin/brew " ] ; then
logg info " Using ${ HOMEBREW_PREFIX :- /opt/homebrew } /bin/brew " && eval " $( " ${ HOMEBREW_PREFIX :- /opt/homebrew } /bin/brew " shellenv) "
if [ -d " $HOME /.linuxbrew " ] ; then
logg info " Using $HOME /.linuxbrew/bin/brew " && eval " $( " $HOME /.linuxbrew/bin/brew " shellenv) "
elif [ -d "/home/linuxbrew/.linuxbrew" ] ; then
logg info 'Using /home/linuxbrew/.linuxbrew/bin/brew' && eval "(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
2023-03-27 16:23:46 -07:00
else
2023-12-09 04:57:12 -08:00
logg info 'Could not find Homebrew installation'
fi
fi
}
2023-03-27 16:23:46 -07:00
2023-12-09 04:57:12 -08:00
### Ensures Homebrew folders have proper owners / permissions
fixHomebrewPermissions( ) {
if command -v brew > /dev/null; then
logg info 'Applying proper permissions on Homebrew folders'
sudo chmod -R go-w " $( brew --prefix) /share "
BREW_DIRS = "share etc/bash_completion.d"
for BREW_DIR in $BREW_DIRS ; do
if [ -d " $( brew --prefix) / $BREW_DIR " ] ; then
sudo chown -Rf " $( whoami) " " $( brew --prefix) / $BREW_DIR "
2023-11-27 14:53:29 -08:00
fi
2023-12-09 04:57:12 -08:00
done
logg info 'Running brew update --force --quiet' && brew update --force --quiet
fi
}
### Installs Homebrew
ensurePackageManagerHomebrew( ) {
if ! command -v brew > /dev/null; then
### Select install type based off of whether or not sudo privileges are available
if command -v sudo > /dev/null && sudo -n true; then
logg info 'Installing Homebrew. Sudo privileges available.'
echo | bash -c " $( curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh) " || BREW_EXIT_CODE = " $? "
else
logg info 'Installing Homebrew. Sudo privileges not available. Password may be required.'
bash -c " $( curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh) " || BREW_EXIT_CODE = " $? "
2023-03-27 16:23:46 -07:00
fi
2023-12-09 04:57:12 -08:00
### Attempt to fix problematic installs
if [ -n " $BREW_EXIT_CODE " ] ; then
logg warn 'Homebrew was installed but part of the installation failed to complete successfully.'
fixHomebrewPermissions
fi
2023-11-12 20:53:44 -08:00
fi
2023-12-09 04:57:12 -08:00
}
2023-03-27 16:22:12 -07:00
2023-12-09 04:57:12 -08:00
### Ensures gcc is installed
ensureGcc( ) {
2023-11-27 14:53:29 -08:00
if command -v brew > /dev/null; then
if ! brew list | grep gcc > /dev/null; then
2023-12-09 04:57:12 -08:00
logg info 'Installing Homebrew gcc' && brew install --quiet gcc
else
logg info 'Homebrew gcc is available'
2023-11-27 14:53:29 -08:00
fi
else
2023-12-09 04:57:12 -08:00
logg error 'Failed to initialize Homebrew' && exit 1
2023-11-12 20:53:44 -08:00
fi
2023-11-27 14:53:29 -08:00
}
2023-12-09 04:57:12 -08:00
# @description This function ensures Homebrew is installed and available in the `PATH`. It handles the installation of Homebrew on both **Linux and macOS**.
# It will attempt to bypass sudo password entry if it detects that it can do so. The function also has some error handling in regards to various
# directories falling out of the correct ownership and permission states. Finally, it loads Homebrew into the active profile (allowing other parts of the script
# to use the `brew` command).
#
# With Homebrew installed and available, the script finishes by installing the `gcc` Homebrew package which is a very common dependency.
ensureHomebrew( ) {
loadHomebrew
ensurePackageManagerHomebrew
loadHomebrew
fixHomebrewPermissions
ensureGcc
}
2023-11-27 14:53:29 -08:00
ensureHomebrew
2023-03-27 16:22:12 -07:00
# @description Ensures `cloudflared` is installed via Homebrew
if ! command -v cloudflared > /dev/null; then
2023-12-05 11:40:25 -08:00
brew install --quiet cloudflared
2023-03-27 16:22:12 -07:00
fi
# @description Detect the SSH port being used
SSH_PORT = "22"
if sudo cat /etc/ssh/sshd_config | grep '^Port ' > /dev/null; then
SSH_PORT = " $( sudo cat /etc/ssh/sshd_config | grep '^Port ' | sed 's/Port //' ) "
fi
# @description Login to the CloudFlare network (if running outside a cronjob) and acquire the tunnel ID
if [ -z " $CRONTAB_JOB " ] ; then
sudo cloudflared tunnel login
sudo cloudflared tunnel create " ${ CF_TUNNEL_NAME : =default-ssh-tunnel } "
fi
TUNNEL_ID = " $( sudo cloudflared tunnel list | grep " ${ CF_TUNNEL_NAME } " | sed 's/^\([^ ]*\).*$/\1/' ) "
# @description Ensure CloudFlare tunnel ID was acquired, then add tunnel route, and create
# tunnel configuration based on what type of machine is being configured (i.e. regular, Qubes, or EasyEngine)
if [ -n " $TUNNEL_ID " ] ; then
if [ -z " $CRONTAB_JOB " ] ; then
sudo cloudflared tunnel route ip add 100.64.0.0/10 " ${ TUNNEL_ID } "
fi
# @description Create the `/root/.cloudflared/config.yml`
CONFIG_TMP = " $( mktemp) "
echo " tunnel: ${ TUNNEL_ID } " > " $CONFIG_TMP "
echo " credentials-file: /root/.cloudflared/ ${ TUNNEL_ID } .json " >> " $CONFIG_TMP "
echo "" >> " $CONFIG_TMP "
echo "ingress:" >> " $CONFIG_TMP "
if [ -d '/opt/easyengine/sites' ] ; then
DOMAINS_HOSTED = " $( find /opt/easyengine/sites -maxdepth 1 -mindepth 1 -type d | sed 's/.*sites\///' | xargs) "
else
if [ -d /etc/qubes ] ; then
2023-04-15 16:23:24 -07:00
DOMAINS_HOSTED = " $( hostname) -qube. ${ SSH_DOMAIN :- ssh .megabyte.space } "
2023-03-27 16:22:12 -07:00
else
2023-04-15 16:23:24 -07:00
DOMAINS_HOSTED = " $( hostname) . ${ SSH_DOMAIN :- ssh .megabyte.space } "
2023-03-27 16:22:12 -07:00
fi
fi
for DOMAIN in $DOMAINS_HOSTED ; do
echo " - hostname: ${ DOMAIN } " >> " $CONFIG_TMP "
echo " service: ssh://localhost: $SSH_PORT " >> " $CONFIG_TMP "
done
echo " - service: http_status:404" >> " $CONFIG_TMP "
echo "" >> " $CONFIG_TMP "
sudo mkdir -p /root/.cloudflared
sudo mv " $CONFIG_TMP " /root/.cloudflared/config.yml
# @description Install the `cloudflared` system service (if it is not a cronjob)
if [ -z " $CRONTAB_JOB " ] ; then
sudo cloudflared service install
fi
else
echo "ERROR: Unable to acquire CloudFlare tunnel ID" && exit 1
fi
# @description Restart if the config file changed
if [ ! -f /root/.cloudflared/config-previous.yml ] || [ " $( cat /root/.cloudflared/config-previous.yml) " != " $( cat /root/.cloudflared/config.yml) " ] ; then
# @todo Add restart command for non-systemctl devices (i.e. macOS)
if command -v systemctl > /dev/null; then
sudo systemctl restart cloudflared
fi
sudo cp /root/.cloudflared/config.yml /root/.cloudflared/config-previous.yml
fi