Add the connect-wireguard composite action for Gitea (not yet verified on a runner)
actions/connect-wireguard brings a WireGuard tunnel up for a deployment job and takes it down again, with a guide and offline tests. The runs on a real runner (MIL-012 task 3) are still to do. Refs #184 Task: MIL-012#3
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
name: Connect WireGuard
|
||||
description: >-
|
||||
Brings up a WireGuard tunnel on a Gitea Actions runner, so the next steps of
|
||||
the job (a deployment) can reach hosts on a private network. Run it again
|
||||
with action "down" in a last step with if always().
|
||||
|
||||
inputs:
|
||||
action:
|
||||
description: "up brings the tunnel up; down takes it down and removes the configuration"
|
||||
required: false
|
||||
default: "up"
|
||||
config:
|
||||
description: >-
|
||||
A complete wg-quick configuration, stored as a Gitea secret. When set, the
|
||||
individual inputs below are ignored.
|
||||
required: false
|
||||
default: ""
|
||||
private-key:
|
||||
description: "Private key of this runner (a secret)"
|
||||
required: false
|
||||
default: ""
|
||||
address:
|
||||
description: "Address of this runner inside the tunnel, e.g. 10.8.0.2/32"
|
||||
required: false
|
||||
default: ""
|
||||
peer-public-key:
|
||||
description: "Public key of the WireGuard server"
|
||||
required: false
|
||||
default: ""
|
||||
endpoint:
|
||||
description: "Address of the WireGuard server as host:port"
|
||||
required: false
|
||||
default: ""
|
||||
allowed-ips:
|
||||
description: "Networks routed through the tunnel, comma separated. Never 0.0.0.0/0 unless the deployment needs it"
|
||||
required: false
|
||||
default: ""
|
||||
preshared-key:
|
||||
description: "Optional preshared key (a secret)"
|
||||
required: false
|
||||
default: ""
|
||||
dns:
|
||||
description: "Optional DNS server for the tunnel"
|
||||
required: false
|
||||
default: ""
|
||||
persistent-keepalive:
|
||||
description: "Seconds between keepalive packets, 0 turns them off"
|
||||
required: false
|
||||
default: "25"
|
||||
interface:
|
||||
description: "Name of the interface"
|
||||
required: false
|
||||
default: "wg0"
|
||||
health-check-host:
|
||||
description: "Optional host behind the tunnel that must answer a ping, else the step fails"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Mask the secrets
|
||||
shell: bash
|
||||
env:
|
||||
WG_CONFIG: ${{ inputs.config }}
|
||||
WG_PRIVATE_KEY: ${{ inputs.private-key }}
|
||||
WG_PRESHARED_KEY: ${{ inputs.preshared-key }}
|
||||
run: bash "${{ gitea.action_path }}/connect-wireguard.sh" mask
|
||||
|
||||
- name: Run the WireGuard step
|
||||
shell: bash
|
||||
env:
|
||||
WG_ACTION: ${{ inputs.action }}
|
||||
WG_CONFIG: ${{ inputs.config }}
|
||||
WG_PRIVATE_KEY: ${{ inputs.private-key }}
|
||||
WG_ADDRESS: ${{ inputs.address }}
|
||||
WG_PEER_PUBLIC_KEY: ${{ inputs.peer-public-key }}
|
||||
WG_ENDPOINT: ${{ inputs.endpoint }}
|
||||
WG_ALLOWED_IPS: ${{ inputs.allowed-ips }}
|
||||
WG_PRESHARED_KEY: ${{ inputs.preshared-key }}
|
||||
WG_DNS: ${{ inputs.dns }}
|
||||
WG_KEEPALIVE: ${{ inputs.persistent-keepalive }}
|
||||
WG_INTERFACE: ${{ inputs.interface }}
|
||||
WG_HEALTH_CHECK_HOST: ${{ inputs.health-check-host }}
|
||||
run: bash "${{ gitea.action_path }}/connect-wireguard.sh" "$WG_ACTION"
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# Steps of the composite action actions/connect-wireguard/action.yml.
|
||||
#
|
||||
# connect-wireguard.sh mask print ::add-mask:: for every secret input
|
||||
# connect-wireguard.sh up write the configuration and bring the tunnel up
|
||||
# connect-wireguard.sh down take the tunnel down and delete the configuration
|
||||
#
|
||||
# Settings come from the environment, never from interpolated expressions:
|
||||
# WG_CONFIG, WG_PRIVATE_KEY, WG_ADDRESS, WG_PEER_PUBLIC_KEY, WG_ENDPOINT,
|
||||
# WG_ALLOWED_IPS, WG_PRESHARED_KEY, WG_DNS, WG_KEEPALIVE, WG_INTERFACE,
|
||||
# WG_HEALTH_CHECK_HOST. WG_DIR is the configuration directory (/etc/wireguard
|
||||
# by default); WG_QUICK, WG_SUDO and WG_PING name the commands, for the tests.
|
||||
# Every failure is printed as ::error::[category] message. A key is never printed.
|
||||
|
||||
WG_DIR="${WG_DIR:-/etc/wireguard}"
|
||||
WG_QUICK="${WG_QUICK:-wg-quick}"
|
||||
WG_PING="${WG_PING:-ping}"
|
||||
SUDO="${WG_SUDO-sudo}"
|
||||
|
||||
fail() {
|
||||
echo "::error::[$1] $2"
|
||||
exit 1
|
||||
}
|
||||
|
||||
mask_secrets() {
|
||||
local value line
|
||||
for value in "${WG_PRIVATE_KEY:-}" "${WG_PRESHARED_KEY:-}"; do
|
||||
[ -n "$value" ] && echo "::add-mask::$value"
|
||||
done
|
||||
# every line of a full configuration is masked, so a key cannot leak in pieces
|
||||
while IFS= read -r line; do
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
[ -n "$line" ] && echo "::add-mask::$line"
|
||||
done <<<"${WG_CONFIG:-}"
|
||||
return 0
|
||||
}
|
||||
|
||||
check_interface() {
|
||||
[[ "$WG_INTERFACE" =~ ^[a-zA-Z0-9_=+.-]{1,15}$ ]] ||
|
||||
fail configuration "interface '$WG_INTERFACE' is not a valid interface name."
|
||||
}
|
||||
|
||||
# render_config: the wg-quick configuration on stdout
|
||||
render_config() {
|
||||
if [ -n "${WG_CONFIG:-}" ]; then
|
||||
printf '%s\n' "$WG_CONFIG"
|
||||
return 0
|
||||
fi
|
||||
local name
|
||||
for name in WG_PRIVATE_KEY WG_ADDRESS WG_PEER_PUBLIC_KEY WG_ENDPOINT WG_ALLOWED_IPS; do
|
||||
[ -n "${!name:-}" ] ||
|
||||
fail configuration "set config, or all of private-key, address, peer-public-key, endpoint and allowed-ips ($name is empty)."
|
||||
done
|
||||
[[ "$WG_KEEPALIVE" =~ ^[0-9]+$ ]] ||
|
||||
fail configuration "persistent-keepalive must be a number of seconds."
|
||||
printf '[Interface]\nPrivateKey = %s\nAddress = %s\n' "$WG_PRIVATE_KEY" "$WG_ADDRESS"
|
||||
[ -z "${WG_DNS:-}" ] || printf 'DNS = %s\n' "$WG_DNS"
|
||||
printf '\n[Peer]\nPublicKey = %s\nEndpoint = %s\nAllowedIPs = %s\nPersistentKeepalive = %s\n' \
|
||||
"$WG_PEER_PUBLIC_KEY" "$WG_ENDPOINT" "$WG_ALLOWED_IPS" "$WG_KEEPALIVE"
|
||||
[ -z "${WG_PRESHARED_KEY:-}" ] || printf 'PresharedKey = %s\n' "$WG_PRESHARED_KEY"
|
||||
}
|
||||
|
||||
install_tools() {
|
||||
command -v "$WG_QUICK" >/dev/null 2>&1 && return 0
|
||||
echo "Installing wireguard-tools"
|
||||
if ! { $SUDO apt-get update -qq &&
|
||||
$SUDO apt-get install -y -qq wireguard-tools resolvconf iproute2 iputils-ping; }; then
|
||||
fail environment "wireguard-tools is not installed and could not be installed (a Debian or Ubuntu runner with root is needed)."
|
||||
fi
|
||||
}
|
||||
|
||||
# Use the user-space implementation when the kernel has no WireGuard module,
|
||||
# which is the usual case in a Gitea runner container.
|
||||
choose_implementation() {
|
||||
if $SUDO ip link add dev "wgprobe$$" type wireguard 2>/dev/null; then
|
||||
$SUDO ip link delete dev "wgprobe$$" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
if command -v wireguard-go >/dev/null 2>&1 || $SUDO apt-get install -y -qq wireguard-go 2>/dev/null; then
|
||||
export WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard-go
|
||||
echo "No kernel WireGuard module; using wireguard-go."
|
||||
else
|
||||
fail environment "the runner has no WireGuard kernel module and wireguard-go could not be installed; the runner container needs NET_ADMIN and /dev/net/tun (see guides/vpn-wireguard.md)."
|
||||
fi
|
||||
}
|
||||
|
||||
tunnel_up() {
|
||||
check_interface
|
||||
local conf="$WG_DIR/$WG_INTERFACE.conf" rendered
|
||||
# on failure the captured output is the ::error:: line, which must be shown
|
||||
rendered="$(render_config)" || {
|
||||
echo "$rendered"
|
||||
exit 1
|
||||
}
|
||||
install_tools
|
||||
choose_implementation
|
||||
$SUDO install -d -m 700 "$WG_DIR"
|
||||
(umask 077 && printf '%s\n' "$rendered" | $SUDO tee "$conf" >/dev/null)
|
||||
$SUDO chmod 600 "$conf"
|
||||
$SUDO "$WG_QUICK" up "$WG_INTERFACE" ||
|
||||
fail connection "wg-quick could not bring $WG_INTERFACE up."
|
||||
if [ -n "${WG_HEALTH_CHECK_HOST:-}" ]; then
|
||||
"$WG_PING" -c 3 -W 5 "$WG_HEALTH_CHECK_HOST" >/dev/null 2>&1 ||
|
||||
fail connection "health-check-host '$WG_HEALTH_CHECK_HOST' does not answer through the tunnel."
|
||||
fi
|
||||
echo "WireGuard interface $WG_INTERFACE is up."
|
||||
}
|
||||
|
||||
tunnel_down() {
|
||||
check_interface
|
||||
local conf="$WG_DIR/$WG_INTERFACE.conf"
|
||||
$SUDO "$WG_QUICK" down "$WG_INTERFACE" 2>/dev/null || echo "Interface $WG_INTERFACE was not up."
|
||||
$SUDO rm -f "$conf"
|
||||
echo "WireGuard interface $WG_INTERFACE is down."
|
||||
}
|
||||
|
||||
main() {
|
||||
set -uo pipefail
|
||||
WG_INTERFACE="${WG_INTERFACE:-wg0}"
|
||||
WG_KEEPALIVE="${WG_KEEPALIVE:-25}"
|
||||
case "${1:-}" in
|
||||
mask) mask_secrets ;;
|
||||
up) tunnel_up ;;
|
||||
down) tunnel_down ;;
|
||||
*) fail configuration "unknown command '${1:-}'; use mask, up or down." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
main "$@"
|
||||
fi
|
||||
Reference in New Issue
Block a user