#!/usr/bin/env bash # Test locally, then rsync validated trees to ns1. Does not enable PFC_REQUIRE_NPE. # Never skip tests. Does not replace pfc-py-admin with Go pfc-admin. set -euo pipefail ROOT="${RESEARCH:-$HOME/research}" HOST="${DEPLOY_HOST:-marchon@70.88.205.138}" export PATH="/opt/homebrew/bin:$PATH" echo "=== review ===" bash "$ROOT/system-git-sync/cicd/run-review.sh" echo "=== linux sm-leaf ===" mkdir -p "$ROOT/secure-messaging/go/bin" ( cd "$ROOT/secure-messaging/go" && GOOS=linux GOARCH=amd64 go build -o bin/sm-leaf-linux ./cmd/sm-leaf ) echo "=== linux pfc-repl ===" ( cd "$ROOT/peergos-compliance-go" && GOOS=linux GOARCH=amd64 go build -o /tmp/pfc-repl-linux ./cmd/pfc-repl ) echo "=== rsync ===" ssh "$HOST" 'sudo mkdir -p /opt/pfc/bin /opt/pfc/python/secure_messaging /opt/pfc/python/admin /opt/pfc/python/lib /opt/pfc/etc /opt/pfc/data/admin/admin-history /opt/pfc/docs/html/sync /opt/pfc/docs/html/nats-service-endpoints' rsync -az "$ROOT/secure-messaging/go/bin/sm-leaf-linux" "$HOST:/tmp/sm-leaf-linux" rsync -az /tmp/pfc-repl-linux "$HOST:/tmp/pfc-repl-linux" rsync -az --exclude '__pycache__' "$ROOT/secure-messaging/python/secure_messaging/" "$HOST:/tmp/secure_messaging/" rsync -az "$ROOT/secure-messaging/deploy/pfc-sm-leaf.service" "$HOST:/tmp/pfc-sm-leaf.service" rsync -az "$ROOT/system-git-sync/" --exclude '.git' --exclude '.forgejo' "$HOST:/tmp/system-git-sync/" rsync -az --exclude '__pycache__' "$ROOT/peergos-for-compliance-admin/pfc_admin/" "$HOST:/tmp/pfc_admin/" rsync -az --exclude '__pycache__' "$ROOT/peergos-for-compliance/lib/" "$HOST:/tmp/pfc_lib/" if [[ -d "$ROOT/nats-service-endpoints/build/html" ]]; then rsync -az "$ROOT/nats-service-endpoints/build/html/" "$HOST:/tmp/nse-html/" fi if [[ -f "$ROOT/nats-service-endpoints/build/latex/nats-service-endpoints.pdf" ]]; then rsync -az "$ROOT/nats-service-endpoints/build/latex/nats-service-endpoints.pdf" "$HOST:/tmp/nse.pdf" fi if [[ -d "$ROOT/peergos-compliance-docs/build/html" ]]; then rsync -az "$ROOT/peergos-compliance-docs/build/html/" "$HOST:/tmp/pfc-docs-html/" fi if [[ -f "$ROOT/peergos-compliance-docs/build/latex/peergos-for-compliance.pdf" ]]; then rsync -az "$ROOT/peergos-compliance-docs/build/latex/peergos-for-compliance.pdf" "$HOST:/tmp/pfc-docs.pdf" fi if [[ -d "$ROOT/peergos-compliance-docs/build/pdf-bundle" ]]; then rsync -az "$ROOT/peergos-compliance-docs/build/pdf-bundle/" "$HOST:/tmp/pdf-bundle/" fi if [[ -d "$ROOT/secure-messaging/build/html" ]]; then rsync -az "$ROOT/secure-messaging/build/html/" "$HOST:/tmp/sm-html/" fi if [[ -d "$ROOT/peergos-compliance-config/web" ]]; then rsync -az "$ROOT/peergos-compliance-config/web/" "$HOST:/tmp/config-web/" fi rsync -az "$ROOT/peergos-compliance-go/deploy/pfc.georgelambert.org.caddy" "$HOST:/tmp/pfc.georgelambert.org.caddy" rsync -az "$ROOT/system-git-sync/docs/pfc-site/" "$HOST:/tmp/pfc-site/" rsync -az "$ROOT/system-git-sync/docs/" --exclude pfc-site "$HOST:/tmp/sync-docs-extra/" echo "=== install on host ===" ssh "$HOST" 'bash -s' << "EOF" set -e sudo systemctl stop pfc-sm-leaf pfc-repl pfc-repl-a pfc-repl-b || true sudo cp /tmp/sm-leaf-linux /opt/pfc/bin/sm-leaf sudo chmod +x /opt/pfc/bin/sm-leaf if [ -f /tmp/pfc-repl-linux ]; then sudo cp /tmp/pfc-repl-linux /opt/pfc/bin/pfc-repl sudo chmod +x /opt/pfc/bin/pfc-repl fi # bus PSK: never plaintext PHI on NATS (broker remains untrusted) if [ ! -f /opt/pfc/etc/nats.env ]; then k=$(python3 -c 'import secrets; print(secrets.token_hex(32))') printf 'PFC_NATS_KEY=%s\nPFC_FORBID_PLAIN=1\n' "$k" | sudo tee /opt/pfc/etc/nats.env >/dev/null sudo chmod 600 /opt/pfc/etc/nats.env fi sudo mkdir -p /etc/systemd/system/pfc-py-admin.service.d /etc/systemd/system/pfc-repl.service.d /etc/systemd/system/pfc-repl-a.service.d /etc/systemd/system/pfc-repl-b.service.d sudo tee /etc/systemd/system/pfc-py-admin.service.d/nats-psk.conf >/dev/null << 'UNIT' [Service] EnvironmentFile=-/opt/pfc/etc/nats.env UNIT sudo tee /etc/systemd/system/pfc-repl.service.d/nats-psk.conf >/dev/null << 'UNIT' [Service] EnvironmentFile=-/opt/pfc/etc/nats.env UNIT sudo tee /etc/systemd/system/pfc-repl-a.service.d/nats-psk.conf >/dev/null << 'UNIT' [Service] EnvironmentFile=-/opt/pfc/etc/nats.env UNIT sudo tee /etc/systemd/system/pfc-repl-b.service.d/nats-psk.conf >/dev/null << 'UNIT' [Service] EnvironmentFile=-/opt/pfc/etc/nats.env UNIT # Public-key directory + per-endpoint HPKE keys (private 0600, public listed) sudo python3 - << 'PY' import sys sys.path.insert(0, "/opt/pfc/python") from pathlib import Path from secure_messaging.pubkey_dir import PubKeyDir d = PubKeyDir(Path("/opt/pfc/etc/npe-dir")) for h in ("pfc-admin", "pfc-repl", "pfc-repl-a", "pfc-repl-b", "pfc-ipfs", "sm-leaf"): if d.get(h) is None: d.create_endpoint(h) print("created", h) else: print("exists", h) print("handles", d.available()["handles"]) PY sudo chmod 700 /opt/pfc/etc/npe-dir/private sudo tee /etc/systemd/system/pfc-py-admin.service.d/npe-dir.conf >/dev/null << 'UNIT' [Service] Environment=PFC_KEYDIR=/opt/pfc/etc/npe-dir Environment=PFC_NPE_HANDLE=pfc-admin UNIT sudo tee /etc/systemd/system/pfc-repl.service.d/npe-dir.conf >/dev/null << 'UNIT' [Service] Environment=PFC_KEYDIR=/opt/pfc/etc/npe-dir Environment=PFC_NPE_HANDLE=pfc-repl UNIT sudo tee /etc/systemd/system/pfc-repl-a.service.d/npe-dir.conf >/dev/null << 'UNIT' [Service] Environment=PFC_KEYDIR=/opt/pfc/etc/npe-dir Environment=PFC_NPE_HANDLE=pfc-repl-a UNIT sudo tee /etc/systemd/system/pfc-repl-b.service.d/npe-dir.conf >/dev/null << 'UNIT' [Service] Environment=PFC_KEYDIR=/opt/pfc/etc/npe-dir Environment=PFC_NPE_HANDLE=pfc-repl-b UNIT sudo mkdir -p /opt/pfc/python/secure_messaging /opt/pfc/python/admin/pfc_admin /opt/pfc/python/lib sudo rsync -a /tmp/secure_messaging/ /opt/pfc/python/secure_messaging/ sudo rsync -a /tmp/pfc_admin/ /opt/pfc/python/admin/pfc_admin/ sudo rsync -a /tmp/pfc_lib/ /opt/pfc/python/lib/ sudo cp /tmp/pfc-sm-leaf.service /etc/systemd/system/pfc-sm-leaf.service sudo mkdir -p /etc/systemd/system/pfc-py-admin.service.d sudo tee /etc/systemd/system/pfc-py-admin.service.d/sm.conf >/dev/null << 'UNIT' [Service] Environment=PYTHONPATH=/opt/pfc/python/lib:/opt/pfc/python/admin:/opt/pfc/python Environment=PFC_SIGNED_CONFIG=/opt/pfc/etc/secure-messaging.signed.json Environment=PFC_CONFIG_KEY_PEM=/opt/pfc/etc/sm-keys/config.ed25519.pem Environment=PFC_REPL_URL=http://127.0.0.1:18784 Environment=NPE_BIN=/opt/pfc/bin/npe UNIT # lab keys on host only — never overwrite an existing private key sudo mkdir -p /opt/pfc/etc/sm-keys if [ ! -f /opt/pfc/etc/sm-keys/config.ed25519.pem ]; then sudo python3 - << 'PY' from pathlib import Path import sys sys.path.insert(0, "/opt/pfc/python") from secure_messaging.signed_config import generate_signing_key, pem_private, pem_public, save_signed, sign priv = generate_signing_key() d = Path("/opt/pfc/etc/sm-keys") d.mkdir(parents=True, exist_ok=True) (d / "config.ed25519.pem").write_bytes(pem_private(priv)) (d / "config.ed25519.pem").chmod(0o600) (d / "config.ed25519.pub.pem").write_bytes(pem_public(priv.public_key())) (d / "config.ed25519.pub.pem").chmod(0o644) payload = { "crypto": {"mode": "lab-xor", "system_key_id": "lab-system"}, "routing": {"mode": "passthrough"}, "admin": {"history_cube": "admin-history"}, "logging": {"mode": "summary"}, } save_signed(Path("/opt/pfc/etc/secure-messaging.signed.json"), sign(payload, priv)) print("signed config written") PY fi sudo chmod 600 /opt/pfc/etc/sm-keys/config.ed25519.pem sudo chmod 644 /opt/pfc/etc/sm-keys/config.ed25519.pub.pem /opt/pfc/etc/secure-messaging.signed.json || true # seed admin-history if empty (prev empty, new = current signed file) sudo python3 - << 'PY' from pathlib import Path import sys sys.path.insert(0, "/opt/pfc/python") from secure_messaging.admin_history import AdminHistory hist = Path("/opt/pfc/data/admin/admin-history") chain = hist / "blockchain" / "chain.jsonl" new = Path("/opt/pfc/etc/secure-messaging.signed.json").read_text() if not chain.exists() or not chain.read_text().strip(): row = AdminHistory(hist).append_change(actor="deploy-ns1", prev_text="", new_text=new) print("admin-history seeded seq", row.get("seq")) else: print("admin-history exists lines", sum(1 for _ in chain.open())) PY if [ -d /tmp/system-git-sync ]; then sudo mkdir -p /opt/pfc/docs/html/sync sudo rsync -a /tmp/system-git-sync/*.MD /tmp/system-git-sync/*.md /opt/pfc/docs/html/sync/ 2>/dev/null || true if [ -f /tmp/system-git-sync/docs/index.html ]; then sudo cp /tmp/system-git-sync/docs/index.html /opt/pfc/docs/html/sync/index.html fi if [ -f /tmp/system-git-sync/docs/thesaurus.md ]; then sudo cp /tmp/system-git-sync/docs/thesaurus.md /opt/pfc/docs/html/sync/thesaurus.md fi if [ -d /tmp/sync-docs-extra ]; then sudo cp /tmp/sync-docs-extra/*.md /tmp/sync-docs-extra/*.html /opt/pfc/docs/html/sync/ 2>/dev/null || true fi fi if [ -f /tmp/pfc.georgelambert.org.caddy ]; then sudo cp /tmp/pfc.georgelambert.org.caddy /etc/caddy/sites/pfc.georgelambert.org.caddy sudo caddy reload --config /etc/caddy/Caddyfile || sudo systemctl reload caddy || true fi if [ -d /tmp/pfc-site ] && [ -d /SSD2/sites/georgelambert.org ]; then sudo mkdir -p /SSD2/sites/georgelambert.org/pfc sudo cp /tmp/pfc-site/index.html /SSD2/sites/georgelambert.org/pfc/index.html fi if [ -x /opt/pfc/bin/npe ] && [ ! -f /opt/pfc/etc/npe/lab.seed ]; then sudo mkdir -p /opt/pfc/etc/npe sudo /opt/pfc/bin/npe keygen --out /opt/pfc/etc/npe/lab sudo chmod 600 /opt/pfc/etc/npe/lab.seed sudo chmod 644 /opt/pfc/etc/npe/lab.npeid || true fi if [ -d /tmp/nse-html ]; then sudo rsync -a /tmp/nse-html/ /opt/pfc/docs/html/nats-service-endpoints/ fi if [ -f /tmp/nse.pdf ]; then sudo cp /tmp/nse.pdf /opt/pfc/docs/html/nats-service-endpoints/nats-service-endpoints.pdf fi if [ -d /tmp/pfc-docs-html ]; then sudo rsync -a /tmp/pfc-docs-html/ /opt/pfc/docs/html/ fi if [ -f /tmp/pfc-docs.pdf ]; then sudo cp /tmp/pfc-docs.pdf /opt/pfc/docs/html/peergos-for-compliance.pdf fi if [ -d /tmp/pdf-bundle ]; then sudo cp /tmp/pdf-bundle/*.pdf /opt/pfc/docs/html/ sudo cp /tmp/pdf-bundle/*.pdf /opt/pfc/docs/html/nats-service-endpoints/ 2>/dev/null || true sudo mkdir -p /opt/pfc/docs/html/secure-messaging sudo cp /tmp/pdf-bundle/*.pdf /opt/pfc/docs/html/secure-messaging/ 2>/dev/null || true fi if [ -d /tmp/sm-html ]; then sudo mkdir -p /opt/pfc/docs/html/secure-messaging sudo rsync -a /tmp/sm-html/ /opt/pfc/docs/html/secure-messaging/ fi if [ -d /tmp/config-web ]; then sudo mkdir -p /opt/pfc/config-ui sudo rsync -a /tmp/config-web/ /opt/pfc/config-ui/ fi # lab NPE identity on host only (never git) if [ -x /opt/pfc/bin/npe ] && [ ! -f /opt/pfc/etc/npe/lab.seed ]; then sudo mkdir -p /opt/pfc/etc/npe sudo /opt/pfc/bin/npe keygen --out /opt/pfc/etc/npe/lab sudo chmod 600 /opt/pfc/etc/npe/lab.seed sudo chmod 644 /opt/pfc/etc/npe/lab.npeid fi # Caddy: sync.pfc (DNS already 70.88.205.138) if [ -f /tmp/system-git-sync/../peergos-compliance-go/deploy/pfc.georgelambert.org.caddy ]; then true fi sudo systemctl daemon-reload sudo systemctl enable --now pfc-sm-leaf sudo systemctl start pfc-repl pfc-repl-a pfc-repl-b sudo systemctl restart pfc-py-admin sleep 2 systemctl is-active pfc-sm-leaf pfc-py-admin pfc-connector pfc-peergos-bridge echo "--- health ---" curl -sS -m 5 http://127.0.0.1:18783/health; echo curl -sS -m 5 http://127.0.0.1:18780/health; echo echo "--- config unauth ---" curl -sS -m 5 -o /tmp/cfg.out -w "http=%{http_code}\n" http://127.0.0.1:18780/v1/admin/config cat /tmp/cfg.out; echo echo "--- signed load ---" sudo python3 - << 'PY' from pathlib import Path import sys sys.path.insert(0, "/opt/pfc/python") from cryptography.hazmat.primitives.serialization import load_pem_private_key from secure_messaging.signed_config import load_signed priv = load_pem_private_key(Path("/opt/pfc/etc/sm-keys/config.ed25519.pem").read_bytes(), password=None) payload = load_signed(Path("/opt/pfc/etc/secure-messaging.signed.json"), priv.public_key()) assert payload["crypto"]["mode"] in ("lab-xor", "npe", "plain-lab") assert payload["routing"]["mode"] == "passthrough" print("signed_ok", payload["crypto"], payload["routing"]) PY # never enable NPE on the live console from this script if systemctl show pfc-py-admin -p Environment | grep -q PFC_REQUIRE_NPE=1; then echo "WARNING: PFC_REQUIRE_NPE=1 is set; fail-closed if sidecar unused" fi echo DEPLOY_HOST_OK EOF echo "=== public smoke ===" curl -sS -m 10 -o /tmp/pfc.health -w "pfc_https=%{http_code}\n" https://pfc.georgelambert.org/health curl -sS -m 10 -o /tmp/nse.html -w "docs_nse=%{http_code}\n" https://docs.pfc.georgelambert.org/nats-service-endpoints/index.html curl -sS -m 10 -o /tmp/sync.html -w "docs_sync=%{http_code}\n" https://docs.pfc.georgelambert.org/sync/index.html curl -sS -m 10 -o /tmp/hist.md -w "docs_hist=%{http_code}\n" https://docs.pfc.georgelambert.org/sync/Historical-Information.MD echo "DEPLOY OK"