Files
piawg/piawg.sh
2026-03-03 00:49:53 -05:00

177 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env sh
# SPDX-License-Identifier: 0BSD
# SPDX-FileCopyrightText: 2026 Kris Lamoureux <kris@lamoureux.io>
# Allow local variable scoping, therefore not strictly POSIX
# shellcheck disable=SC3043
err() {
printf '[ERROR]: %s\n' "$1" >&2
exit 1
}
check_http() {
case $1 in
2[0-9][0-9]) return 0 ;;
*) return 1 ;;
esac
}
# Check for plausible looking PIA token
check_token() {
printf '%s\n' "$1" | grep -q '^[0-9A-Fa-f]\{128\}$'
}
bao_curl() {
curl -sS --connect-timeout 5 --max-time 20 --retry 5 --retry-delay 2 \
-H 'Content-Type: application/json' \
-H "X-Vault-Token: $bao_token" \
-w '\n%{http_code}' \
"$@"
}
# Fetch latest PIA token in OpenBao
get_token() {
local pia_token_reply
if ! pia_token_reply=$(bao_curl \
"$BAO_ADDR/v1/$BAO_KV_MOUNT/data/$BAO_PATH_TOKEN"); then
err "Failed to fetch PIA token from '$BAO_ADDR'"
fi
printf '%s' "$pia_token_reply"
}
# Get a new PIA token and store
renew_token() {
local login_response
local update_response
local token_response
local http_code
local pia_user
local pia_pass
local pia_token
login_response="$(bao_curl \
"$BAO_ADDR/v1/$BAO_KV_MOUNT/data/$BAO_PATH_LOGIN"
)"
http_code="$(printf '%s' "$login_response" | tail -1)"
if ! check_http "$http_code"; then
err "Failed to get PIA login details (HTTP $http_code)"
fi
login_response="$(printf '%s' "$login_response" | sed '$d')"
pia_user="$(printf '%s' "$login_response" | jq -r '.data.data.username')"
pia_pass="$(printf '%s' "$login_response" | jq -r '.data.data.password')"
unset login_response
if ! token_response="$(curl -s -X POST "$PIA_API" \
-F "username=$pia_user" \
-F "password=$pia_pass")"; then
err "Failed to get a new PIA token"
fi
unset pia_pass
unset pia_user
pia_token="$(echo "$token_response" | jq -r .token)"
unset token_response
check_token "$pia_token" || err "Invalid token found during renewal attempt"
if ! update_response="$(bao_curl -X POST -d \
"$(jq -n --arg t "$pia_token" '{data:{token:$t}}')" \
"$BAO_ADDR/v1/$BAO_KV_MOUNT/data/$BAO_PATH_TOKEN")"; then
err "Failed to save PIA token to '$BAO_ADDR'"
fi
unset pia_token
http_code="$(printf '%s' "$update_response" | tail -1)"
update_response="$(printf '%s' "$update_response" | sed '$d')"
check_http "$http_code" ||
err "Failed to write PIA token to OpenBao (HTTP $http_code)"
}
# Check for required external commands
for rbin in curl jq openssl; do
command -v "$rbin" >/dev/null 2>&1 ||
err "Required binary '$rbin' not found"
done
# Setup config structure
: "${PIAWG_CONF_DIR:=$HOME/.config/piawg}"
: "${PIAWG_CONF:=$PIAWG_CONF_DIR/config}"
[ -d "$HOME/.config" ] || install -d -m 0755 "$HOME/.config"
[ -d "$PIAWG_CONF_DIR" ] || install -d -m 0700 "$PIAWG_CONF_DIR"
if [ ! -f "$PIAWG_CONF" ]; then
install -m 0600 /dev/null "$PIAWG_CONF"
cat <<-'EOF' >"$PIAWG_CONF"
# piawg configuration
#BAO_ADDR=
#BAO_ROLE=
#BAO_SECRET=
EOF
fi
# Source config
if [ -r "$PIAWG_CONF" ]; then
# shellcheck source=/dev/null
. "$PIAWG_CONF"
else
err "Can't find config at '$PIAWG_CONF'"
fi
# Overridable defaults
: "${PIA_API:=https://www.privateinternetaccess.com/api/client/v2/token}"
: "${PIA_CRT:=https://www.privateinternetaccess.com/openvpn/ca.rsa.4096.crt}"
: "${PIA_HASH:=1fd25658456eab3041fba77ccd398ab8\
124edcc1b8b2fc1d55fdf6b1bbfc9d70}"
: "${BAO_AUTH_PATH:=approle}"
: "${BAO_KV_MOUNT:=kv}"
: "${BAO_PATH_LOGIN:=piawg/creds/login}"
: "${BAO_PATH_TOKEN:=piawg/session/token}"
# Must set these in PIAWG_CONF
: "${BAO_ADDR:?[ERROR]: BAO_ADDR is not set}"
: "${BAO_ROLE:?[ERROR]: BAO_ROLE is not set}"
: "${BAO_SECRET:?[ERROR]: BAO_SECRET is not set}"
# Get ephemeral session token from AppRole login
if ! bao_token_reply=$(curl -sS \
--connect-timeout 5 \
--max-time 20 \
--retry 5 \
--retry-delay 2 \
-H 'Content-Type: application/json' \
-d "{\"role_id\":\"$BAO_ROLE\",\"secret_id\":\"$BAO_SECRET\"}" \
"$BAO_ADDR/v1/auth/$BAO_AUTH_PATH/login"); then
err "Failed to login to '$BAO_ADDR'"
fi
bao_token=$(printf '%s' "$bao_token_reply" | jq -er '.auth.client_token')
unset bao_token_reply
[ -n "$bao_token" ] || err "Failed to get token from '$BAO_ADDR'"
# Get latest PIA token
get_token_reply="$(get_token)"
http_code="$(printf '%s' "$get_token_reply" | tail -1)"
get_token_reply="$(printf '%s' "$get_token_reply" | sed '$d')"
# Renew token if path doesn't exist yet
if [ "$http_code" -eq 404 ]; then
renew_token
get_token_reply="$(get_token)"
http_code="$(printf '%s' "$get_token_reply" | tail -1)"
get_token_reply="$(printf '%s' "$get_token_reply" | sed '$d')"
if ! check_http "$http_code"; then
err "Failed to get PIA token after renewal"
fi
elif ! check_http "$http_code"; then
err "Failed to get PIA token from '$BAO_ADDR' (HTTP $http_code)"
fi
printf '%s\n' "$get_token_reply"
# Download PIA RSA CA certificate
if [ ! -f ./ca.rsa.4096.crt ]; then
[ -f ./.ca.rsa.4096.crt ] && rm ./.ca.rsa.4096.crt
curl -sS --connect-timeout 5 --max-time 20 --retry 5 --retry-delay 2 \
-o ./.ca.rsa.4096.crt "$PIA_CRT"
pia_file_hash="$(openssl x509 -in ./.ca.rsa.4096.crt -outform DER |
openssl dgst -sha256 -r | awk '{print $1}')"
[ "$pia_file_hash" != "$PIA_HASH" ] && err "PIA CA fingerprint mismatch"
mv ./.ca.rsa.4096.crt ./ca.rsa.4096.crt
fi