diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index ddc52c9..b5e843c 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -54,6 +54,7 @@ jobs: WG_PRIVATE_KEY: ${{ secrets.WG_PRIVATE_KEY }} WG_PEER_PUBLIC_KEY: ${{ secrets.WG_PEER_PUBLIC_KEY }} WG_ENDPOINT: ${{ secrets.WG_ENDPOINT }} + WG_LOCAL_IP: 10.8.0.3/32 run: bash scripts/wg-tunnel-up.sh - name: Setup kubeconfig @@ -65,9 +66,11 @@ jobs: - name: Dry-run kubectl apply (server-side) run: | set -euo pipefail - # Exclude 01-ci-rbac.yaml — operator-bootstrapped, not CI-managed - find k8s/staging -maxdepth 1 -name '*.yaml' ! -name '01-ci-rbac.yaml' | sort | \ - xargs kubectl apply -n "$K8S_NAMESPACE" --dry-run=server -f + # Exclude operator-bootstrapped, non-CI-managed manifests: + # 00-namespace.yaml (cluster-scoped Namespace) and 01-ci-rbac.yaml + # (the namespace-scoped ci-deployer Role cannot get/create either). + find k8s/staging -maxdepth 1 -name '*.yaml' ! -name '00-namespace.yaml' ! -name '01-ci-rbac.yaml' | sort | sed 's/^/-f /' | \ + xargs kubectl apply -n "$K8S_NAMESPACE" --dry-run=server deploy: name: Deploy to k3s @@ -88,6 +91,7 @@ jobs: WG_PRIVATE_KEY: ${{ secrets.WG_PRIVATE_KEY }} WG_PEER_PUBLIC_KEY: ${{ secrets.WG_PEER_PUBLIC_KEY }} WG_ENDPOINT: ${{ secrets.WG_ENDPOINT }} + WG_LOCAL_IP: 10.8.0.3/32 run: bash scripts/wg-tunnel-up.sh - name: Setup kubeconfig @@ -120,9 +124,11 @@ jobs: - name: Apply staging manifests run: | set -euo pipefail - # Exclude 01-ci-rbac.yaml — operator-bootstrapped, not CI-managed - find k8s/staging -maxdepth 1 -name '*.yaml' ! -name '01-ci-rbac.yaml' | sort | \ - xargs kubectl apply -n "$K8S_NAMESPACE" -f + # Exclude operator-bootstrapped, non-CI-managed manifests: + # 00-namespace.yaml (cluster-scoped Namespace) and 01-ci-rbac.yaml + # (the namespace-scoped ci-deployer Role cannot get/create either). + find k8s/staging -maxdepth 1 -name '*.yaml' ! -name '00-namespace.yaml' ! -name '01-ci-rbac.yaml' | sort | sed 's/^/-f /' | \ + xargs kubectl apply -n "$K8S_NAMESPACE" - name: Verify rollouts run: | diff --git a/scripts/kubeconfig-setup.sh b/scripts/kubeconfig-setup.sh index 8b48bf6..5f7bafe 100755 --- a/scripts/kubeconfig-setup.sh +++ b/scripts/kubeconfig-setup.sh @@ -46,6 +46,7 @@ printf '%s' "$K8S_CA_CERT" > "$ca_file" echo "Configuring cluster $K8S_CLUSTER_NAME -> $K8S_API_SERVER..." kubectl config set-cluster "$K8S_CLUSTER_NAME" \ --certificate-authority="$ca_file" \ + --embed-certs=true \ --server="$K8S_API_SERVER" \ --kubeconfig="$KUBECONFIG" diff --git a/scripts/wg-tunnel-up.sh b/scripts/wg-tunnel-up.sh index 373b118..cc610b8 100755 --- a/scripts/wg-tunnel-up.sh +++ b/scripts/wg-tunnel-up.sh @@ -53,18 +53,34 @@ sudo ip link add dev "$WG_INTERFACE" type wireguard echo "Assigning local IP $WG_LOCAL_IP..." sudo ip address add "$WG_LOCAL_IP" dev "$WG_INTERFACE" -# --- 3. Configure peer (private key via process substitution — never on disk) +# --- 3. Configure peer (private key via /dev/stdin — never on disk) --------- +# NB: process substitution <(...) does NOT survive `sudo` — sudo closes +# inherited FDs (closefrom=3), so the FD-backed /dev/fd/NN path disappears in +# the wg process ("fopen: No such file or directory"). /dev/stdin (FD 0) IS +# preserved by sudo, so the key still never touches disk. echo "Configuring peer $WG_PEER_PUBLIC_KEY..." -sudo wg set "$WG_INTERFACE" \ - private-key <(printf '%s' "$WG_PRIVATE_KEY") \ +printf '%s' "$WG_PRIVATE_KEY" | sudo wg set "$WG_INTERFACE" \ + private-key /dev/stdin \ peer "$WG_PEER_PUBLIC_KEY" \ endpoint "$WG_ENDPOINT" \ + persistent-keepalive 25 \ allowed-ips "$WG_ALLOWED_IPS" # --- 4. Bring interface up -------------------------------------------------- echo "Bringing up $WG_INTERFACE..." sudo ip link set up dev "$WG_INTERFACE" +# --- 4b. Route allowed-ips through the tunnel ------------------------------- +# `wg set ... allowed-ips` only configures WireGuard's cryptokey routing — it +# does NOT add a kernel route (that is wg-quick's job, and we don't use it). +# With a /32 local address there is no subnet route either, so the API server +# IP would be unreachable at the IP layer even though the endpoint-to-endpoint +# handshake succeeds. Add an explicit route for each allowed IP. +for cidr in ${WG_ALLOWED_IPS//,/ }; do + echo "Routing $cidr via $WG_INTERFACE..." + sudo ip route replace "$cidr" dev "$WG_INTERFACE" +done + # --- 5. Handshake polling loop (fail-closed) -------------------------------- # latest-handshakes output: "\t" # Handshake is complete when timestamp_epoch is non-zero (> 0). @@ -72,6 +88,12 @@ sudo ip link set up dev "$WG_INTERFACE" echo "Waiting for handshake (timeout: ${HANDSHAKE_TIMEOUT_SECS}s)..." start_epoch=$(date +%s) while true; do + # Prime: WireGuard initiates a handshake only when an outbound packet needs + # to reach the peer. Poke the API server through the tunnel to force that + # initiation (the connect may fail until the tunnel is up — we only need it + # to queue a packet so the handshake starts). + timeout 1 bash -c "echo > /dev/tcp/$K8S_API_HOST/$K8S_API_PORT" 2>/dev/null || true + if sudo wg show "$WG_INTERFACE" latest-handshakes | grep -qP '\t[1-9][0-9]*$'; then echo "Handshake complete." break