wgtunnel: S3 Part 3 — FELHOM_WG sudoers + capabilities + config (DEFAULT OFF) + main wiring + escrow join

Sudoers: fixed-path conf install, enable/restart/disable, latest-handshakes-only
wg read (dump FORBIDDEN — the S1 incident). 6 capability-manifest entries
(Critical=false until S4 makes the tunnel load-bearing). WGTunnelConfig with
enabled=false DEFAULT (the safety gate: a v0.64.0 rollout without explicit
config is a no-op). Daemon wiring mirrors lanresolver + AddConsumer +
SetWireguardReporter; --selftest=wgtunnel single-shot. IdentityBundle
+wg_private_key (omitempty; pre-S3 blobs cannot be retrofitted — documented)
with escrow-create auto-inject (field name only in logs). Red-proof (e) run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-04 07:14:33 +02:00
parent fb248961c6
commit e2b6c63ea2
6 changed files with 230 additions and 9 deletions
+28
View File
@@ -2,10 +2,12 @@ package escrow
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
// Slice 10D.1 — IDENTITY escrow. The K-escrow (above) wraps the PBS *encryption key* via the
@@ -24,6 +26,32 @@ var ageBinary = "/usr/bin/age"
type IdentityBundle struct {
TunnelToken string `json:"tunnel_token"` // the Cloudflare tunnel connector token
PBSToken string `json:"pbs_token"` // the PBS access token (steady-state; rotated on re-establish)
// WGPrivateKey is the offsite WG tunnel private key (S3; base64, 32 bytes). OPTIONAL: escrow
// blobs created before S3 lack it and CANNOT be retro-fitted (R is never retained) — S5 DR
// falls back to fresh-key re-registration, which keeps the box's /32 (hub S2 re-key-in-place).
WGPrivateKey string `json:"wg_private_key,omitempty"`
}
// AttachWGKey injects the offsite WG private key into the bundle when the key file exists (S3
// escrow-create auto-inject). Returns whether it attached. The VALUE is validated (base64, 32
// bytes) but never logged by callers — log the field NAME only. A missing key file is a clean
// no-attach (pre-S3 behavior, byte-compatible bundle); a corrupt one is an error (the operator
// should know their escrow would silently lack a live identity).
func AttachWGKey(b *IdentityBundle, keyPath string) (bool, error) {
raw, err := os.ReadFile(keyPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("escrow: reading wg key file: %w", err)
}
s := strings.TrimSpace(string(raw))
dec, err := base64.StdEncoding.DecodeString(s)
if err != nil || len(dec) != 32 {
return false, fmt.Errorf("escrow: wg key file %s is corrupt (not 32-byte base64)", keyPath)
}
b.WGPrivateKey = s
return true, nil
}
// WrapIdentity wraps arbitrary bundle bytes under `R` via `age -p` (scrypt + ChaCha20-Poly1305) and