scripts+manifests: S1 felhom-peersync.sh v1.0.0 + hub wg-endpoint-ssh deploy surface
Peersync script: validate-first (jq contract check before ANY state change), head-file + generated-peers conf model, syncconf-from-tmp then atomic mv (live conf never diverges in the failure direction), zero-peer payload valid (wipe). hub.yaml: 0.32.0 image + WG_ENDPOINT_SSH_* env + optional Secret mount so the pod starts before the runbook's step-6 Secret exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
+29
-1
@@ -126,7 +126,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: hub
|
- name: hub
|
||||||
image: gitea.dooplex.hu/admin/felhom-hub:0.31.0
|
image: gitea.dooplex.hu/admin/felhom-hub:0.32.0
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
@@ -154,6 +154,23 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: gitea-creds
|
name: gitea-creds
|
||||||
key: password
|
key: password
|
||||||
|
# S1 offsite connectivity: the WG peer-sync push channel (doc 06 §5 + runbook
|
||||||
|
# offsite-endpoint.md). Addr is dev-phase literal (the throwaway endpoint); the SSH
|
||||||
|
# private key + (non-secret) pinned host key come from Secret/wg-endpoint-ssh,
|
||||||
|
# created out-of-band in runbook step 6 — optional so the pod starts before it
|
||||||
|
# exists (the hub logs peer-sync disabled until then).
|
||||||
|
- name: WG_ENDPOINT_SSH_ADDR
|
||||||
|
value: "167.233.158.164:22"
|
||||||
|
- name: WG_ENDPOINT_SSH_USER
|
||||||
|
value: "felhom-peersync"
|
||||||
|
- name: WG_ENDPOINT_SSH_KEY_FILE
|
||||||
|
value: "/etc/hub-secrets/wg-endpoint-ssh/key"
|
||||||
|
- name: WG_ENDPOINT_SSH_HOSTKEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: wg-endpoint-ssh
|
||||||
|
key: hostkey
|
||||||
|
optional: true
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "64Mi"
|
memory: "64Mi"
|
||||||
@@ -166,6 +183,9 @@ spec:
|
|||||||
mountPath: /data
|
mountPath: /data
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/felhom-hub
|
mountPath: /etc/felhom-hub
|
||||||
|
- name: wg-endpoint-ssh
|
||||||
|
mountPath: /etc/hub-secrets/wg-endpoint-ssh
|
||||||
|
readOnly: true
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
@@ -187,6 +207,14 @@ spec:
|
|||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: hub-config
|
name: hub-config
|
||||||
|
- name: wg-endpoint-ssh
|
||||||
|
secret:
|
||||||
|
secretName: wg-endpoint-ssh
|
||||||
|
optional: true
|
||||||
|
items:
|
||||||
|
- key: key
|
||||||
|
path: key
|
||||||
|
mode: 0400
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# SERVICE
|
# SERVICE
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# felhom-peersync v1.0.0 — the offsite endpoint's WG peer-list reconcile script (S1, doc 06 §5).
|
||||||
|
#
|
||||||
|
# Runs as the SSH forced command for the hub's `felhom-peersync` user (via sudo — see
|
||||||
|
# documentation/runbooks/offsite-endpoint.md step 5). Reads the hub's declarative payload on
|
||||||
|
# stdin, VALIDATES FIRST (any failure exits 1 before touching anything), then applies the FULL
|
||||||
|
# peer list with `wg syncconf` (exact-match: adds missing peers, removes absent ones, never
|
||||||
|
# bounces the interface) and only after a successful apply persists the conf atomically.
|
||||||
|
#
|
||||||
|
# Payload contract (version 1):
|
||||||
|
# {"version":1,"interface":"wg0","peers":[{"pubkey":"<44b64>","allowed_ip":"10.77.0.x/32"}]}
|
||||||
|
# Response on stdout: {"status":"ok","applied":<N>}
|
||||||
|
#
|
||||||
|
# One script, one job: there is NO second mode. It never reads or prints the WG private key —
|
||||||
|
# /etc/wireguard/wg0.conf.head (the [Interface] section, including PrivateKey) is only ever
|
||||||
|
# concatenated. Do NOT replace the head-file model with `wg-quick save` (nondeterministic; would
|
||||||
|
# rewrite the whole conf from runtime state).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONF_DIR=/etc/wireguard
|
||||||
|
HEAD_FILE="$CONF_DIR/wg0.conf.head"
|
||||||
|
LIVE_CONF="$CONF_DIR/wg0.conf"
|
||||||
|
IFACE=wg0
|
||||||
|
|
||||||
|
err() {
|
||||||
|
echo "felhom-peersync: ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v jq >/dev/null || err "jq is required"
|
||||||
|
command -v wg >/dev/null || err "wireguard-tools is required"
|
||||||
|
[ -r "$HEAD_FILE" ] || err "missing $HEAD_FILE"
|
||||||
|
|
||||||
|
# 1. Read stdin capped at 1 MiB. A truncated (oversized) payload fails JSON validation below.
|
||||||
|
payload=$(head -c 1048576)
|
||||||
|
[ -n "$payload" ] || err "empty payload"
|
||||||
|
|
||||||
|
# 2. Validate EVERYTHING before touching any state. `all` is true on an empty peers array, so a
|
||||||
|
# zero-peer payload (wipe the list) is valid by design. The endpoint's own 10.77.0.1 must
|
||||||
|
# never appear as a peer allowed_ip.
|
||||||
|
jq -e '
|
||||||
|
(.version == 1)
|
||||||
|
and (.interface == "wg0")
|
||||||
|
and ((.peers | type) == "array")
|
||||||
|
and ([.peers[] | (.pubkey | type) == "string" and (.pubkey | test("^[A-Za-z0-9+/]{43}=$"))] | all)
|
||||||
|
and ([.peers[] | (.allowed_ip | type) == "string"
|
||||||
|
and (.allowed_ip | test("^10\\.77\\.0\\.[0-9]{1,3}/32$"))
|
||||||
|
and (.allowed_ip != "10.77.0.1/32")] | all)
|
||||||
|
' >/dev/null <<<"$payload" || err "payload failed validation (version/interface/pubkey/allowed_ip)"
|
||||||
|
|
||||||
|
# 3. Generate the candidate conf in a tmp dir ON THE SAME FILESYSTEM (atomic mv later). Values
|
||||||
|
# are written into the file by jq/cat only — never interpolated into a command line.
|
||||||
|
tmpdir=$(mktemp -d "$CONF_DIR/.peersync.XXXXXX")
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
tmp="$tmpdir/wg0.conf"
|
||||||
|
(umask 077; cat "$HEAD_FILE" > "$tmp")
|
||||||
|
jq -r '.peers[] | "\n[Peer]\nPublicKey = \(.pubkey)\nAllowedIPs = \(.allowed_ip)"' \
|
||||||
|
<<<"$payload" >> "$tmp"
|
||||||
|
chmod 600 "$tmp"
|
||||||
|
|
||||||
|
# 4. Apply from the TMP file first. On failure we exit here (set -e): the previous good
|
||||||
|
# LIVE_CONF is still in place — runtime and boot config never diverge in the bad direction.
|
||||||
|
wg syncconf "$IFACE" <(wg-quick strip "$tmp") || err "wg syncconf failed; live conf untouched"
|
||||||
|
|
||||||
|
# 5. Persist only after a successful apply (same-fs mv = atomic).
|
||||||
|
mv "$tmp" "$LIVE_CONF"
|
||||||
|
|
||||||
|
# 6. Report.
|
||||||
|
applied=$(jq '.peers | length' <<<"$payload")
|
||||||
|
printf '{"status":"ok","applied":%d}\n' "$applied"
|
||||||
Reference in New Issue
Block a user