#!/usr/bin/env bash set -euo pipefail # Regenerate a FRESH PIA WireGuard peer and write wg0.conf for gluetun (custom mode), # using PIA's official CA-pinned curl flow (token via authv3/generateToken + addKey). # # WHY NOT pia-wg-config: the pinned Go binary (Go 1.23) TLS-rejects PIA's legacy # CommonName (no-SAN) server certs. OpenSSL/curl still honor CN fallback, so the same # endpoints validate fine here. See memory: pia-wg-regen-broken-go-legacy-cn. # # CA-AWARE region auto-pick: PIA has migrated some regions (e.g. us_chicago) onto # Kape/ExpressVPN-CA infra whose certs do NOT chain to PIA's ca.rsa.4096.crt. Because # every request below is --cacert-pinned to the PIA CA, such a region fails the token # step (curl exit 60) and is transparently skipped. We try $PIA_REGION first, then a # US-centric PIA-native fallback list; the first region that validates + registers wins. # # Creds read from a read-only-mounted .env so the password never lands in this # container's Config.Env, the compose file, or any log. ENV_FILE="${ENV_FILE:-/pia/.env}" OUT="${OUT:-/config/wg0.conf}" CA="${CA:-/etc/pia/ca.rsa.4096.crt}" SERVERLIST="${SERVERLIST:-https://serverlist.piaservers.net/vpninfo/servers/v6}" PREFERRED="${PIA_REGION:-us_denver}" # fallback order if the preferred region isn't PIA-CA / is unreachable FALLBACKS="us_denver us_atlanta us_california us_east us_seattle ca_toronto" log(){ echo "[regen] $*" >&2; } [ -f "$CA" ] || { echo "ERROR: PIA CA not found at $CA" >&2; exit 1; } user="$(grep -E '^OPENVPN_USER=' "$ENV_FILE" | head -1 | cut -d= -f2-)" pass="$(grep -E '^OPENVPN_PASSWORD=' "$ENV_FILE" | head -1 | cut -d= -f2-)" if [ -z "${user:-}" ] || [ -z "${pass:-}" ]; then echo "ERROR: OPENVPN_USER/OPENVPN_PASSWORD not found in $ENV_FILE" >&2; exit 1 fi log "fetching PIA server list" servers="$(curl -fsS --max-time 20 "$SERVERLIST" | sed -n '1p')" [ -n "$servers" ] || { echo "ERROR: empty server list" >&2; exit 1; } # build ordered, de-duplicated candidate list: preferred first, then fallbacks candidates="" for r in "$PREFERRED" $FALLBACKS; do case " $candidates " in *" $r "*) : ;; *) candidates="$candidates $r" ;; esac done # try_region -> writes $OUT and returns 0 on success; non-zero to try next try_region() { local region="$1" WG_IP WG_CN META_IP META_CN read -r WG_IP WG_CN META_IP META_CN < <( printf '%s' "$servers" | jq -r --arg r "$region" ' .regions[] | select(.id==$r) as $reg | ($reg.servers.wg[0]) as $wg | ($reg.servers.meta[0]) as $meta | "\($wg.ip) \($wg.cn) \($meta.ip) \($meta.cn)"' 2>/dev/null) [ -n "${WG_IP:-}" ] && [ -n "${META_CN:-}" ] || { log " $region: no wg/meta server, skip"; return 1; } # token: CA-pinned generateToken on the META server (this is also the PIA-CA gate) local tok_json rc=0 token tok_status tok_json="$(curl -fsS --max-time 20 -u "$user:$pass" \ --connect-to "${META_CN}::${META_IP}:" --cacert "$CA" \ "https://${META_CN}/authv3/generateToken" 2>/dev/null)" || rc=$? if [ "$rc" -ne 0 ]; then [ "$rc" -eq 60 ] && log " $region: cert not PIA-CA (curl 60), skip" || log " $region: token curl failed (rc=$rc), skip" return 1 fi token="$(printf '%s' "$tok_json" | jq -r '.token // empty')" tok_status="$(printf '%s' "$tok_json" | jq -r '.status // empty')" [ -n "$token" ] || { log " $region: no token (status=$tok_status), skip"; return 1; } # fresh keypair; private key never leaves this container local priv pub add_json add_status server_key peer_ip server_ip server_port dns priv="$(wg genkey)"; pub="$(printf '%s' "$priv" | wg pubkey)" add_json="$(curl -fsS --max-time 20 -G \ --connect-to "${WG_CN}::${WG_IP}:" --cacert "$CA" \ --data-urlencode "pt=${token}" --data-urlencode "pubkey=${pub}" \ "https://${WG_CN}:1337/addKey" 2>/dev/null)" || { log " $region: addKey curl failed, skip"; return 1; } add_status="$(printf '%s' "$add_json" | jq -r '.status // empty')" [ "$add_status" = "OK" ] || { log " $region: addKey status=$add_status, skip"; return 1; } server_key="$(printf '%s' "$add_json" | jq -r '.server_key')" peer_ip="$(printf '%s' "$add_json" | jq -r '.peer_ip')" server_ip="$(printf '%s' "$add_json" | jq -r '.server_ip // empty')"; server_ip="${server_ip:-$WG_IP}" server_port="$(printf '%s' "$add_json" | jq -r '.server_port // 1337')" dns="$(printf '%s' "$add_json" | jq -r '.dns_servers[0] // empty')" for v in server_key peer_ip dns; do [ -n "${!v}" ] && [ "${!v}" != "null" ] || { log " $region: addKey missing $v, skip"; return 1; } done local TMP; TMP="$(mktemp "${OUT}.XXXXXX")" cat > "$TMP" <&2; rm -f "$TMP"; return 1; } done chmod 600 "$TMP"; mv "$TMP" "$OUT" log "regen OK: region=$region endpoint=${server_ip}:${server_port} address=${peer_ip} (PIA-CA verified)" return 0 } for region in $candidates; do log "trying region: $region" if try_region "$region"; then exit 0; fi done echo "ERROR: no PIA-CA region succeeded (tried:$candidates)" >&2 exit 1