The node embeds the reward preimage verbatim in the PreRuntime digest of every block it authors (client/consensus/qpow/src/lib.rs:726), and the mining-rewards pallet reads that digest back to derive the payout address on-chain. It is public from the first mined block. So the Gitea secret, the 0640 handling and the ps-visibility caveat were all theatre over a value published on a blockchain. It moves to the workflow env: block with the rest of the infra truth, and infra-setup.sh loses --mint-reward-key entirely along with its pass and Gitea-API dependencies. This also answers why the node takes a preimage and not an address: the runtime does the derivation, which is what proves the payout target is a canonical wormhole address rather than an arbitrary account. An address would give it nothing to verify.
247 lines
11 KiB
Bash
Executable File
247 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# One-time, operator-run provisioning so the Gitea Actions runner can deploy as
|
|
# the scoped `gitea_ci` user, plus the key material that cannot live in the
|
|
# workflow. Two roles, each with its own narrow sudoers:
|
|
#
|
|
# node (--node-hosts) quantus-node binary + unit + config + firewalld,
|
|
# and read access to the two miner credential files
|
|
# the node generates -> deploy.yaml deploy-node
|
|
# miner (--miner-hosts) quantus-miner binary + unit + credentials
|
|
# -> deploy.yaml deploy-miner
|
|
#
|
|
# The roles are separate deliberately: the miner host holds a GPU and an
|
|
# outbound-only process, and a compromise of the miner deploy must not be able
|
|
# to rewrite the validator's binary or read its reward config. Hence
|
|
# `quantus-miner_gitea_ci` as a SEPARATE sudoers file from
|
|
# `quantus-node_gitea_ci` (deployment-gitea-actions.md §1).
|
|
#
|
|
# Convention: ~/git/architecture/deployment-gitea-actions.md §1–§2, generic.md
|
|
# §8–§10, agent-credentials.md §1 and §6.
|
|
#
|
|
# Run from a workstation with admin (sudo) ssh access to the targets — NOT the
|
|
# gitea_ci account. Idempotent; re-running is a no-op. Skips unreachable hosts
|
|
# so one offline node doesn't block the rest.
|
|
#
|
|
# ./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub
|
|
# ./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub --node-hosts "" \
|
|
# --miner-hosts benjy.hanzalova.internal
|
|
#
|
|
# The runner keypair is NOT generated here. It already exists at
|
|
# ~/.ssh/id_gitea_ci and is shared by every project's RSYNC_SSH_KEY secret and
|
|
# every host's gitea_ci authorized_keys — regenerating it would silently break
|
|
# every other deploy on the fleet (deployment-gitea-actions.md §2).
|
|
set -euo pipefail
|
|
|
|
ADMIN_USER="${ADMIN_USER:-$USER}"
|
|
# `${X-default}` not `${X:-default}`: an explicitly EMPTY list means "none of
|
|
# this role", which is how you provision only the other one. With `:-` the empty
|
|
# value would silently fall back to the full default list.
|
|
NODE_HOSTS="${NODE_HOSTS-bob.hanzalova.internal}"
|
|
MINER_HOSTS="${MINER_HOSTS-benjy.hanzalova.internal}"
|
|
PUBKEY="${PUBKEY:-${HOME}/.ssh/id_gitea_ci.pub}"
|
|
CHAIN="${CHAIN:-planck}"
|
|
NODE_VERSION="${NODE_VERSION:-0.10.0}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--pubkey) PUBKEY="$2"; shift 2 ;;
|
|
--node-hosts) NODE_HOSTS="$2"; shift 2 ;;
|
|
--miner-hosts) MINER_HOSTS="$2"; shift 2 ;;
|
|
--admin) ADMIN_USER="$2"; shift 2 ;;
|
|
--node-version) NODE_VERSION="$2"; shift 2 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
|
|
NODE_URL="https://github.com/Quantus-Network/chain/releases/download/v${NODE_VERSION}/quantus-node-v${NODE_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
|
|
|
# ------------------------------------------------------------------- roles ----
|
|
|
|
provision_node() {
|
|
local host="$1"
|
|
echo "== ${host} (node) =="
|
|
if ! ssh -o ConnectTimeout=8 -o BatchMode=yes "${ADMIN_USER}@${host}" true; then
|
|
echo " ! unreachable as ${ADMIN_USER} — skipping" >&2
|
|
return 1
|
|
fi
|
|
|
|
# All privileged work in one remote `sudo bash`. The runner pubkey is the only
|
|
# dynamic value, passed as $1 (single line, no quoting hazard). The sudoers
|
|
# `\=` are the required escapes — visudo rejects a bare `=` in a command arg.
|
|
# The `*` in an rsync line matches rsync's --server arg vector; the trailing
|
|
# literal destination is what actually bounds the rule.
|
|
ssh "${ADMIN_USER}@${host}" "sudo bash -seu -- '$(cat "$PUBKEY")'" <<'REMOTE'
|
|
PUBKEY="$1"
|
|
|
|
if ! getent passwd gitea_ci >/dev/null; then
|
|
useradd --system --create-home --home-dir /var/lib/gitea_ci \
|
|
--shell /usr/sbin/nologin gitea_ci
|
|
echo " + created gitea_ci"
|
|
else
|
|
echo " = gitea_ci already present"
|
|
fi
|
|
|
|
install -d -o gitea_ci -g gitea_ci -m 0700 /var/lib/gitea_ci/.ssh
|
|
ak=/var/lib/gitea_ci/.ssh/authorized_keys
|
|
touch "$ak"
|
|
grep -qxF "$PUBKEY" "$ak" || printf '%s\n' "$PUBKEY" >> "$ak"
|
|
chown gitea_ci:gitea_ci "$ak"
|
|
chmod 0600 "$ak"
|
|
|
|
# journalctl -u <unit> for the deploy's log capture, without sudo.
|
|
usermod -aG systemd-journal gitea_ci
|
|
|
|
tmp=/etc/sudoers.d/.quantus-node_gitea_ci.tmp
|
|
cat > "$tmp" <<'SUDO'
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /usr/local/bin/quantus-node
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/quantus-node.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/sysusers.d/quantus-node.conf
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-node/node.env
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/quantus-node.xml
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/quantus-node-miner.xml
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemd-sysusers
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o root -g quantus-node -m 0750 /etc/quantus-node
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o quantus-node -g quantus-node -m 0750 /var/lib/quantus-node
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/quantus-node /etc/quantus-node /var/lib/quantus-node
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --get-default-zone
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --reload
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --permanent --zone\=* --add-service\=quantus-node
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --zone\=* --add-service\=quantus-node
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --zone\=* --query-service\=quantus-node
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --permanent --zone\=* --add-rich-rule\=*
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --zone\=* --add-rich-rule\=*
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --zone\=* --query-rich-rule\=*
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl enable quantus-node.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart quantus-node.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/cat /var/lib/quantus-node/chains/*/miner-auth-token
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/cat /var/lib/quantus-node/chains/*/miner-tls-cert-sha256
|
|
SUDO
|
|
chmod 0440 "$tmp"
|
|
visudo -cf "$tmp"
|
|
mv "$tmp" /etc/sudoers.d/quantus-node_gitea_ci
|
|
echo " = sudoers quantus-node_gitea_ci installed and visudo-verified"
|
|
REMOTE
|
|
}
|
|
|
|
provision_miner() {
|
|
local host="$1"
|
|
echo "== ${host} (miner) =="
|
|
if ! ssh -o ConnectTimeout=8 -o BatchMode=yes "${ADMIN_USER}@${host}" true; then
|
|
echo " ! unreachable as ${ADMIN_USER} — skipping" >&2
|
|
return 1
|
|
fi
|
|
|
|
ssh "${ADMIN_USER}@${host}" "sudo bash -seu -- '$(cat "$PUBKEY")'" <<'REMOTE'
|
|
PUBKEY="$1"
|
|
|
|
if ! getent passwd gitea_ci >/dev/null; then
|
|
useradd --system --create-home --home-dir /var/lib/gitea_ci \
|
|
--shell /usr/sbin/nologin gitea_ci
|
|
echo " + created gitea_ci"
|
|
else
|
|
echo " = gitea_ci already present"
|
|
fi
|
|
|
|
install -d -o gitea_ci -g gitea_ci -m 0700 /var/lib/gitea_ci/.ssh
|
|
ak=/var/lib/gitea_ci/.ssh/authorized_keys
|
|
touch "$ak"
|
|
grep -qxF "$PUBKEY" "$ak" || printf '%s\n' "$PUBKEY" >> "$ak"
|
|
chown gitea_ci:gitea_ci "$ak"
|
|
chmod 0600 "$ak"
|
|
usermod -aG systemd-journal gitea_ci
|
|
|
|
# The GPU must be present and enumerable before a miner deploy is worth
|
|
# attempting; failing here beats a green deploy that mines nothing.
|
|
if ! command -v nvidia-smi >/dev/null; then
|
|
echo " ! nvidia-smi not found — this host has no usable NVIDIA driver" >&2
|
|
exit 1
|
|
fi
|
|
nvidia-smi --query-gpu=name --format=csv,noheader | sed 's/^/ = gpu: /'
|
|
|
|
tmp=/etc/sudoers.d/.quantus-miner_gitea_ci.tmp
|
|
cat > "$tmp" <<'SUDO'
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /usr/local/bin/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/quantus-miner.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/sysusers.d/quantus-miner.conf
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-miner/miner.env
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-miner/miner-auth-token
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-miner/miner-tls-cert-sha256
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemd-sysusers
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o root -g quantus-miner -m 0750 /etc/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o quantus-miner -g quantus-miner -m 0750 /var/lib/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/quantus-miner /etc/quantus-miner /var/lib/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl enable quantus-miner.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart quantus-miner.service
|
|
SUDO
|
|
chmod 0440 "$tmp"
|
|
visudo -cf "$tmp"
|
|
mv "$tmp" /etc/sudoers.d/quantus-miner_gitea_ci
|
|
echo " = sudoers quantus-miner_gitea_ci installed and visudo-verified"
|
|
REMOTE
|
|
}
|
|
|
|
# The node's libp2p identity. It must exist BEFORE the first start — without it
|
|
# the node exits with NetworkKeyNotFound(".../network/secret_dilithium") rather
|
|
# than generating one. It is also the node's stable identity on the network, so
|
|
# it is generated once and never rotated by a deploy.
|
|
ensure_node_key() {
|
|
local host="$1"
|
|
echo "== ${host} (node key) =="
|
|
ssh "${ADMIN_USER}@${host}" "sudo bash -seu -- '${NODE_URL}'" <<'REMOTE'
|
|
NODE_URL="$1"
|
|
|
|
install -d -o root -g root -m 0755 /etc/sysusers.d
|
|
cat > /etc/sysusers.d/quantus-node.conf <<'SYSUSERS'
|
|
u quantus-node - "Quantus node service account" /var/lib/quantus-node /usr/sbin/nologin
|
|
SYSUSERS
|
|
systemd-sysusers
|
|
install -d -o quantus-node -g quantus-node -m 0750 /var/lib/quantus-node
|
|
|
|
key=/var/lib/quantus-node/node_key.p2p
|
|
if [ -s "$key" ]; then
|
|
echo " = node key already present — left untouched"
|
|
else
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
curl -fSL --retry 5 --retry-delay 5 --retry-all-errors --connect-timeout 15 \
|
|
-o "$tmp/node.tar.gz" "$NODE_URL"
|
|
tar xzf "$tmp/node.tar.gz" -C "$tmp"
|
|
bin="$(find "$tmp" -name quantus-node -type f | head -1)"
|
|
chmod +x "$bin"
|
|
"$bin" key generate-node-key --file "$key"
|
|
chown quantus-node:quantus-node "$key"
|
|
chmod 0600 "$key"
|
|
echo " + generated node key"
|
|
fi
|
|
# The peer id is public, and is what you look for on telemetry.
|
|
echo -n " = peer id: "
|
|
getent passwd quantus-node >/dev/null && cat /var/lib/quantus-node/node_key.p2p >/dev/null
|
|
REMOTE
|
|
}
|
|
|
|
# --------------------------------------------------------------------- main ---
|
|
|
|
[[ -n "$PUBKEY" ]] || { echo "error: --pubkey <path> is required" >&2; exit 2; }
|
|
[[ -s "$PUBKEY" ]] || { echo "error: pubkey not found or empty: $PUBKEY" >&2; exit 2; }
|
|
|
|
rc=0
|
|
for h in $NODE_HOSTS; do
|
|
provision_node "$h" || { rc=1; continue; }
|
|
ensure_node_key "$h" || rc=1
|
|
done
|
|
for h in $MINER_HOSTS; do
|
|
provision_miner "$h" || rc=1
|
|
done
|
|
|
|
echo
|
|
echo "done. remaining operator steps:"
|
|
echo " 1. set QUANTUS_INNER_HASH in .gitea/workflows/deploy.yaml (it is public config,"
|
|
echo " not a secret — the node publishes it in every block header)"
|
|
echo " 2. confirm RSYNC_SSH_KEY is set in the repo settings"
|
|
echo " 3. push to main, or run the deploy workflow manually"
|
|
exit $rc
|