From 81e728bc1f8270453920f60b779b5b0a4ff6be83 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Wed, 26 Aug 2026 21:24:18 +0800 Subject: [PATCH 1/9] feat: add observer install script and documentation --- README.md | 12 +- install/OBSERVER.md | 653 ++++++++++++++++++ install/README.md | 22 +- install/install-observer.sh | 67 ++ install/install-validator.sh | 2 +- install/lib/binaries.sh | 8 +- install/lib/configuration.sh | 75 +- install/lib/custodian.sh | 111 ++- install/lib/directories.sh | 2 +- install/lib/observer-configuration.sh | 256 +++++++ install/lib/observer-directories.sh | 13 + install/lib/observer-instructions.sh | 106 +++ install/lib/observer-keys.sh | 150 ++++ install/lib/observer-supervisor.sh | 156 +++++ install/lib/packages.sh | 13 +- install/lib/supervisor.sh | 31 +- install/provision-observer-key.sh | 209 ++++++ install/templates/supervisor/custodian.conf | 1 + .../supervisor/observer-custodian.conf | 25 + install/templates/supervisor/observer.conf | 30 + .../supervisor/{validator.conf => reth.conf} | 22 - .../supervisor/summit-validator.conf | 21 + 22 files changed, 1912 insertions(+), 73 deletions(-) create mode 100644 install/OBSERVER.md create mode 100755 install/install-observer.sh create mode 100644 install/lib/observer-configuration.sh create mode 100644 install/lib/observer-directories.sh create mode 100644 install/lib/observer-instructions.sh create mode 100644 install/lib/observer-keys.sh create mode 100644 install/lib/observer-supervisor.sh create mode 100755 install/provision-observer-key.sh create mode 100644 install/templates/supervisor/observer-custodian.conf create mode 100644 install/templates/supervisor/observer.conf rename install/templates/supervisor/{validator.conf => reth.conf} (64%) create mode 100644 install/templates/supervisor/summit-validator.conf diff --git a/README.md b/README.md index c95d27e..5204831 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Seismic Node Operations -Operational tooling and configuration for installing and running Seismic validator nodes. +Operational tooling and configuration for installing and running Seismic validator and observer nodes. ## Validator installation @@ -12,6 +12,16 @@ install/install-validator.sh For requirements, installation steps, persistent storage, MDBX notes, validator registration, service startup, OpenResty, and troubleshooting, see the **[Validator Installer and First-Start Guide](install/README.md)**. +## Observer installation + +The observer installer is located at: + +```text +install/install-observer.sh +``` + +See the **[Observer Installer and First-Start Guide](install/OBSERVER.md)**. + The Summit internal-testnet genesis file is provided at: ```text diff --git a/install/OBSERVER.md b/install/OBSERVER.md new file mode 100644 index 0000000..cf01b83 --- /dev/null +++ b/install/OBSERVER.md @@ -0,0 +1,653 @@ +# Seismic Observer Installer and First-Start Guide + +This directory contains the interactive installer for a Seismic observer node: + +```text +install/install-observer.sh +``` + +The installer prepares the observer software, persistent directories, keys, +Supervisor programs, and optional OpenResty endpoint. It deliberately does not +start the observer services. Follow the first-start procedure in this document +after installation. + +For a validator node, use `install/install-validator.sh` and follow the separate +**[Validator Installer and First-Start Guide](README.md)**. + +## Safety model + +The installer is designed to avoid replacing persistent observer state: + +- Observer services run as an existing non-root service user. +- Persistent directories must be absolute, non-root, and non-overlapping. +- Existing Reth P2P keys are validated and preserved. +- Existing observer consensus and parent node keys are preserved only when the + assignment marker matches the configured parent public key and observer + index. +- Keys without an assignment marker, or a marker for another assignment, cause + the installer to stop rather than reuse or replace them. +- Existing observer Custodian root keys are preserved for verification against + the parent Custodian. +- Service binaries are installed as root-owned, non-writable executables. +- Supervisor programs use `autostart=false` and `autorestart=false`. +- The installer does not start, enable, reread, update, reload, or restart + observer services. + +Review the complete interactive configuration summary before accepting it. + +## Requirements + +- Ubuntu with `apt-get`. Other Linux distributions are not supported. +- Root access through `sudo`. +- An existing non-root Linux user that will run the observer services. +- Python 3 for public Summit socket-address validation. +- The Summit genesis TOML for the selected network; it must remain readable by + the service user. +- The parent validator's Summit node public key and the selected observer index. +- A literal public IPv4 or bracketed IPv6 Summit P2P socket address for the + observer. Summit's default P2P port is `18551`. +- The parent validator's private Summit `node_key.pem`, which must be placed at + the configured observer Summit key path after installation. +- Sufficient persistent storage for Reth, Summit, observer keys, and any enabled + optional components. +- Network access to package and source repositories when installing packages or + building from source. +- When Custodian is enabled, the reachable council endpoint of the Custodian + running on the parent validator, normally `PARENT_IP:7876`. +- If configuring OpenResty, DNS for the selected domain must point to the node. + +The installer validates local input and key consistency. It deliberately does +not check whether the parent key is a genesis validator or a current validator. +Network authorization is outside the installation process. + +The installation log is written to: + +```text +/var/log/seismic-observer-install.log +``` + +### Network and firewall requirements + +The installer does not configure cloud firewall, security-group, or host +firewall rules. Configure the required access before starting the observer. + +| Port | Protocol | Purpose | Required exposure | +| --- | --- | --- | --- | +| `30303` | TCP and UDP | seismic-reth P2P and discovery | Public | +| `18551` or configured port | TCP and UDP | Summit observer P2P | Public | +| `80` | TCP | HTTP redirect and ACME challenge | Public when OpenResty is enabled | +| `443` | TCP | OpenResty HTTPS endpoint | Public when OpenResty is enabled | +| `7876` or configured port | TCP | Observer Custodian council listener | Externally reachable when Custodian is enabled; restrict sources to intended participants | + +When observer Custodian is enabled, the observer also needs outbound TCP access +to the configured parent validator Custodian council endpoint. The parent +firewall must allow the observer's source IP. + +The parent-Custodian protocol transports root-key and plaintext epoch-key +material. Use a private network or protect the path with a TLS tunnel. Do not +send it over an untrusted plaintext network. + +The following application ports must remain loopback-only: + +```text +3000 3030 3031 8545 8546 8552 8999 9001 9090 +``` + +### Supplied internal-testnet genesis + +The repository includes the Summit genesis file for the internal testnet: + +```text +internal_testnet_genesis.toml +``` + +When the installer asks for the Summit genesis TOML path, enter its absolute +path. + +Keep this file in place after installation. The generated Supervisor program +references the selected path directly, and the configured service user must be +able to read it. + +## Run the installer + +Run the installer from the repository root: + +```bash +sudo ./install/install-observer.sh +``` + +The installer asks you to configure: + +- The non-root service user. +- Persistent Reth, Summit, and observer-key directories. +- An optional public HTTPS endpoint through OpenResty. +- The Summit genesis file and an optional Reth bootnode RPC. +- The parent validator Summit node public key. +- An observer derivation index from `0` through `255`. +- The observer's public Summit P2P `IP:port`; the default Summit port is + `18551`. +- An optional path to a Summit bootstrappers TOML file. +- Summit and seismic-reth binary installation methods. +- summit-checkpointer. +- Centralized Custodian, including the parent validator Custodian council + endpoint and Custodian binary installation method. + +Neither the parent nor nodes listed in an optional bootstrappers file are +required to appear in the genesis `[[validators]]` list. + +### Binary installation methods + +Summit, seismic-reth, summit-checkpointer, and Centralized Custodian support: + +1. Install a supplied prebuilt executable. +2. Build from source during installation. +3. Defer installation and provide the executable later. + +The current source-build defaults are: + +```text +Summit: m/metrics +seismic-reth: feat/purpose-key-rotation-reth +Checkpointer: m/dynamic-epochs-and-ckpt-chain +Custodian: d/centralized-custodian +``` + +A prebuilt or already-present deferred observer Custodian is checked for these +required options: + +```text +--summit-key-dir +--observer +--parent-custodian +``` + +Deferred binaries must be installed at their configured target paths before the +corresponding services are started. + +### Optional Summit bootstrappers file + +A bootstrappers file can contain one or more known Summit peers: + +```toml +[[bootstrappers]] +node_public_key = "32-byte Summit node public key" +address = "203.0.113.10:18551" + +[[bootstrappers]] +node_public_key = "another 32-byte Summit node public key" +address = "198.51.100.20:18551" +``` + +The selected source path must be absolute and point to a non-empty, +non-symbolic-link file readable by the service user. The installer copies it to +the configured Summit key directory as `bootstrappers.toml`; Supervisor +references that stable copy. + +This file configures Summit consensus peers. It is separate from the optional +Reth bootnode RPC used to discover an execution-layer enode. + +## Persistent layout + +The default persistent paths are: + +| State | Default path | +| --- | --- | +| Reth data | `/persistence/reth` | +| Summit observer data | `/persistence/summit` | +| Observer and Reth keys | `/persistence/keys` | +| Checkpointer output, when enabled | `/persistence/checkpoints` | +| Custodian data, when enabled | `/persistence/custodian` | + +Important files derived from those paths include: + +```text +/persistence/keys/reth/p2p-key +/persistence/keys/summit/observer-assignment +/persistence/keys/summit/consensus_key.pem +/persistence/keys/summit/node_key.pem +/persistence/keys/summit/bootstrappers.toml +/persistence/custodian/root.key +``` + +Every persistent directory is configurable. Changing a directory on a later +installer run does not migrate existing state; it selects a separate store. + +### Observer assignment and keys + +The observer assignment is represented as: + +```text +: +``` + +and stored in: + +```text +/summit/observer-assignment +``` + +The installer generates the observer's independent `consensus_key.pem`. It does +not generate or copy the parent validator's private `node_key.pem`. + +Before starting Custodian or `summit-observer`, place the parent validator's +private node key at the configured Summit key path. With the default paths, the +required destination is: + +```text +/persistence/keys/summit/node_key.pem +``` + +The file must be owned by the configured service user and have mode `0600`. +The Summit key directory must have mode `0700`. The private key must correspond +to the parent public key entered during installation. + +Summit derives the observer's secondary P2P identity from the parent node key +and observer index. The observer's separate consensus key does not make it a +voting or proposing validator. + +### Reth MDBX database + +seismic-reth stores its MDBX execution database under: + +```text +/db +``` + +Treat this as persistent execution-layer state. Do not delete it during an +installer rerun, and do not manually copy or modify the live database while +Reth is running. + +When summit-checkpointer is enabled, it uses `mdbx_copy` to produce a consistent +Reth database copy. `mdbx_copy` must be built from the same vendored libmdbx +revision as the seismic-reth binary. If Reth is built from source, the installer +builds `mdbx_copy` from that checkout. For a prebuilt or deferred Reth binary, +provide the matching seismic-reth source checkout when prompted. Do not start +the checkpointer until a compatible executable is installed at: + +```text +/usr/local/bin/mdbx_copy +``` + +## Generated services and configuration + +The observer installer writes the Supervisor configuration to: + +```text +/etc/supervisor/conf.d/seismic-observer.conf +``` + +It always defines: + +- `reth` +- `summit-observer` + +It also defines `checkpointer` and `custodian` when those components are +enabled. It does not define `summit-deposit-rpc`; observers do not create +validator deposit signatures. + +The installer also does not create a checkpoint-start Supervisor program. +Starting a validator or observer from a checkpoint is handled by the dedicated +checkpoint-start CLI. + +Supervisor logs are written under: + +```text +/var/log/seismic-observer/ +``` + +When summit-checkpointer is enabled, its generated configuration is written to: + +```text +/etc/seismic/summit-checkpointer.toml +``` + +When OpenResty is enabled, the installer writes: + +```text +/usr/local/openresty/nginx/conf/nginx.conf +/usr/local/openresty/nginx/lua/rate_limit.lua +/usr/local/openresty/nginx/lua/jwt_auth.lua +/etc/seismic/openresty-jwt-secret +/etc/logrotate.d/openresty +``` + +The JWT secret is root-owned and is not printed by the installer. + +Reth HTTP, WebSocket, Ops, and metrics endpoints and Summit RPC, admin RPC, and +metrics endpoints bind to loopback whether or not OpenResty is enabled. + +## Observer Custodian + +The parent validator Custodian council endpoint and the observer's local council +listener are different settings: + +- The parent endpoint is a remote `host:port` that the observer contacts to + fetch or verify the root key and synchronize epoch-key deliveries. +- The local listener is the observer Custodian's own bind address and defaults + to `0.0.0.0:7876`. + +The generated observer Custodian command includes: + +```text +--observer +--parent-custodian +--summit-key-dir +``` + +If the configured Custodian `root.key` is absent, the observer Custodian fetches +it from the parent on first startup. If it already exists, the installer +preserves it and the Custodian verifies it against the parent. Installer reruns +do not replace an existing observer root key. + +The parent validator Custodian must be running with its own `--summit-key-dir` +argument so it can authenticate and serve observer requests. + +## First observer startup + +Do not start Custodian or Summit until the parent validator's private +`node_key.pem` exists at the configured observer Summit key path. + +### 1. Load the Supervisor configuration + +```bash +sudo systemctl enable --now supervisor +sudo supervisorctl reread +sudo supervisorctl update +sudo supervisorctl status +``` + +All observer programs should still be stopped because they use: + +```ini +autostart=false +autorestart=false +``` + +### 2. Start Custodian when enabled + +Custodian must start before Reth because Reth connects to its Unix socket during +startup: + +```bash +sudo supervisorctl start custodian +sudo supervisorctl status custodian +``` + +With default paths, verify its state and socket: + +```bash +sudo stat /persistence/custodian/root.key +sudo test -S /tmp/custodian.sock +``` + +If it does not start, inspect: + +```bash +sudo tail -n 100 /var/log/seismic-observer/custodian.err +sudo tail -n 100 /var/log/seismic-observer/custodian.log +``` + +Skip this step when Custodian was not enabled. + +### 3. Start Reth + +```bash +sudo supervisorctl start reth +sudo supervisorctl status reth +``` + +If it does not start, inspect: + +```bash +sudo tail -n 100 /var/log/seismic-observer/reth.err +sudo tail -n 100 /var/log/seismic-observer/reth.log +``` + +Reth must be running before Summit because Summit connects through the Engine +API IPC socket. + +### 4. Start Summit observer + +```bash +sudo supervisorctl start summit-observer +sudo supervisorctl status summit-observer +``` + +If it does not start, inspect: + +```bash +sudo tail -n 100 /var/log/seismic-observer/summit-observer.err +sudo tail -n 100 /var/log/seismic-observer/summit-observer.log +``` + +The dependency order is: + +```text +custodian -> reth -> summit-observer +``` + +When Custodian is disabled, start only Reth and Summit observer in that order. + +### 5. Start summit-checkpointer when enabled + +```bash +sudo supervisorctl start checkpointer +sudo supervisorctl status checkpointer +``` + +If it does not start, inspect: + +```bash +sudo tail -n 100 /var/log/seismic-observer/checkpointer.err +sudo tail -n 100 /var/log/seismic-observer/checkpointer.log +``` + +summit-checkpointer produces checkpoint artifacts. Starting a validator or +observer from a checkpoint remains the responsibility of the dedicated +checkpoint-start CLI. + +Because `autostart=false` and `autorestart=false`, all enabled programs must be +started manually again after a server or Supervisor restart. + +## OpenResty public endpoint + +When enabled, OpenResty terminates HTTPS, obtains certificates through +`lua-resty-auto-ssl`, applies per-client rate limiting, and proxies local Reth +and Summit endpoints. + +Reth HTTP and WebSocket RPC, Reth Ops RPC, Summit RPC, and metrics listeners +remain bound to loopback whether or not OpenResty is enabled. When OpenResty is +disabled, these endpoints are available only from the node itself or through an +operator-managed tunnel. + +The configured routes are: + +| Public path | Local upstream | Notes | +| --- | --- | --- | +| `/` | `127.0.0.1:3000` | Grafana | +| `/staking` | `/var/www/html/staking` | Static staking UI | +| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | +| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | +| `/summit` | `127.0.0.1:3030` | Summit RPC | +| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | +| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | +| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | + +The Summit admin RPC on loopback port `3031` is deliberately not proxied. + +### Start or reload OpenResty + +```bash +sudo openresty -t +sudo systemctl enable openresty + +if sudo systemctl is-active --quiet openresty; then + sudo systemctl reload openresty +else + sudo systemctl start openresty +fi + +sudo systemctl status openresty --no-pager +``` + +## Verify the observer + +Check service state: + +```bash +sudo supervisorctl status +``` + +Inspect listening sockets: + +```bash +sudo ss -lntup +``` + +Confirm that: + +- Reth P2P is reachable on configured TCP and UDP port `30303`. +- Summit P2P is reachable on its configured public address, normally port + `18551`. +- Reth ports `8545`, `8546`, `8552`, and `9001` are loopback-only. +- Summit ports `3030`, `3031`, and `9090` are loopback-only. +- Custodian listens on the configured council address when enabled. +- OpenResty listens on ports `80` and `443` only when configured and explicitly + started. + +Follow the main logs: + +```bash +sudo tail -f \ + /var/log/seismic-observer/reth.log \ + /var/log/seismic-observer/summit-observer.log +``` + +There is no staking transaction or deposit-signature workflow for an observer. + +## Safe installer reruns + +The installer can be rerun to update binaries or rendered configuration, but it +does not persist previous answers. Re-enter the same service user, persistent +directories, genesis path, observer assignment, and optional component settings +unless you are intentionally changing them. + +On a normal rerun, the installer: + +- Preserves the Reth P2P key. +- Preserves Summit keys when the assignment marker matches. +- Refuses to reuse keys for another parent or observer index. +- Preserves an existing observer Custodian root key. +- Replaces the installer-managed bootstrappers file from the accepted source, + or removes it when no source is configured. +- Replaces generated OpenResty and Supervisor configuration. +- Leaves services stopped and does not reload Supervisor or OpenResty. + +Changing a persistent path does not migrate existing data. It creates or uses a +separate store. + +Before applying a later `supervisorctl update`, inspect running services: + +```bash +sudo supervisorctl status +``` + +Supervisor may restart a running program whose definition changed when the +updated configuration is applied. Plan that operation separately for a live +observer. + +## Troubleshooting + +### Parent node key is missing or does not match + +Confirm that `node_key.pem` exists at the configured Summit key path, is owned +by the service user, and has mode `0600`. Run: + +```bash +sudo -u SERVICE_USER /usr/local/bin/summit keys show \ + --key-store-path /path/to/configured/summit/keys +``` + +The displayed node public key must match the parent public key accepted by the +installer. + +### Observer assignment marker mismatch + +The marker must contain exactly: + +```text +: +``` + +Do not edit it to repurpose existing keys or state. Use separate persistent key +and Summit data paths for a different assignment. + +### Parent Custodian is unreachable + +Verify the configured parent `host:port`, outbound routing, and the parent +firewall. The parent Custodian must be running and configured with its Summit +key directory. + +### Custodian root-key fetch or verification fails + +Check that the observer index, parent node key, chain ID, council address, and +parent Custodian endpoint all match the intended deployment. Do not delete an +existing root key merely to bypass a mismatch. + +### Custodian socket is missing + +Inspect the Custodian logs and confirm that the configured socket parent path is +usable. Reth must not be started in Custodian mode until the Unix socket exists. + +### Reth cannot start + +Check the Reth executable, Reth P2P key, data-directory ownership, Custodian +socket when enabled, and Reth error log. Do not delete the MDBX database as a +first troubleshooting step. + +### Summit cannot start + +Check the Summit executable, genesis readability, assignment marker, both +Summit key files, Engine API IPC availability, public P2P address, and Summit +error log. + +### Observer cannot find Summit peers + +Confirm that the genesis contains reachable initial peers or provide a valid +bootstrappers TOML. The bootstrappers file must contain the expected public keys +and reachable `IP:port` addresses. + +### RPC or metrics port is publicly exposed + +Stop the affected program and inspect: + +```text +/etc/supervisor/conf.d/seismic-observer.conf +``` + +Reth HTTP, WebSocket, Ops, and metrics and Summit RPC, admin RPC, and metrics +must bind to loopback. Only P2P and explicitly configured public services should +bind externally. + +### Deferred binary is missing or unsafe + +Install the executable at the accepted target path as a root-owned mode `0755` +regular file. Parent directories must also be root-owned and not writable by the +service user. + +### OpenResty validation fails + +Run: + +```bash +sudo openresty -t +``` + +Then inspect: + +```text +/var/log/seismic-observer-install.log +/usr/local/openresty/nginx/conf/nginx.conf +``` + +Do not start or reload OpenResty until its configuration test passes. diff --git a/install/README.md b/install/README.md index 0ac2947..3dd9eaa 100644 --- a/install/README.md +++ b/install/README.md @@ -11,6 +11,9 @@ Supervisor programs, and optional OpenResty endpoint. It deliberately does not start the validator services. Follow the first-start procedure in this document after installation. +For an observer node, use `install/install-observer.sh` and follow the separate +**[Observer Installer and First-Start Guide](OBSERVER.md)**. + ## Safety model The installer is designed to avoid replacing persistent validator state: @@ -103,13 +106,17 @@ known shared default root key and does not prompt for a custom key. This makes epoch-0 purpose keys public. The Custodian council listener defaults to `0.0.0.0:7876`. TCP port `7876` -must be reachable from outside the node for key rotation. -Configure the cloud firewall, security group, and host firewall as -needed, and restrict allowed source addresses to the intended council -participants rather than exposing the port more broadly than necessary. - -For Summit, seismic-reth, and summit-checkpointer, supported installation modes -are: +must be reachable from outside the node for key rotation. The generated +validator Custodian program also receives the Summit key directory so it can +authenticate and serve configured observer Custodians. +Configure the cloud firewall, security group, and host firewall as needed, and +restrict allowed source addresses to intended council participants and observer +hosts rather than exposing the port more broadly than necessary. Observer root +keys and plaintext epoch-key material transit the parent-Custodian connection; +use a private network or TLS tunnel. + +For Summit, seismic-reth, summit-checkpointer, and Centralized Custodian, +supported installation modes are: 1. Install a supplied prebuilt executable. 2. Build from source during installation. @@ -120,6 +127,7 @@ The current source-build defaults are: ```text Summit: m/metrics seismic-reth: feat/purpose-key-rotation-reth +Custodian: d/centralized-custodian ``` Deferred binaries must be installed at the configured target paths before the diff --git a/install/install-observer.sh b/install/install-observer.sh new file mode 100755 index 0000000..ce2492a --- /dev/null +++ b/install/install-observer.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# Install and configure a Seismic observer node. +# +# The installer reuses the validator installer's shared node components while +# keeping observer assignment, parent-key provisioning, and Custodian parent +# synchronization explicit. It never starts node services. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATES_DIR="$SCRIPT_DIR/templates" +LOG_FILE="${LOG_FILE:-/var/log/seismic-observer-install.log}" + +# shellcheck source=lib/common.sh +source "$SCRIPT_DIR/lib/common.sh" +# shellcheck source=lib/preflight.sh +source "$SCRIPT_DIR/lib/preflight.sh" +# shellcheck source=lib/packages.sh +source "$SCRIPT_DIR/lib/packages.sh" +# shellcheck source=lib/openresty.sh +source "$SCRIPT_DIR/lib/openresty.sh" +# shellcheck source=lib/directories.sh +source "$SCRIPT_DIR/lib/directories.sh" +# shellcheck source=lib/observer-directories.sh +source "$SCRIPT_DIR/lib/observer-directories.sh" +# shellcheck source=lib/binaries.sh +source "$SCRIPT_DIR/lib/binaries.sh" +# shellcheck source=lib/checkpointer.sh +source "$SCRIPT_DIR/lib/checkpointer.sh" +# shellcheck source=lib/custodian.sh +source "$SCRIPT_DIR/lib/custodian.sh" +# shellcheck source=lib/keys.sh +source "$SCRIPT_DIR/lib/keys.sh" +# shellcheck source=lib/observer-keys.sh +source "$SCRIPT_DIR/lib/observer-keys.sh" +# shellcheck source=lib/supervisor.sh +source "$SCRIPT_DIR/lib/supervisor.sh" +# shellcheck source=lib/observer-supervisor.sh +source "$SCRIPT_DIR/lib/observer-supervisor.sh" +# shellcheck source=lib/configuration.sh +source "$SCRIPT_DIR/lib/configuration.sh" +# shellcheck source=lib/observer-configuration.sh +source "$SCRIPT_DIR/lib/observer-configuration.sh" +# shellcheck source=lib/observer-instructions.sh +source "$SCRIPT_DIR/lib/observer-instructions.sh" + +main() { + info "Seismic observer installer" + preflight + command -v python3 >/dev/null \ + || die "python3 is required for observer socket-address validation." + configure_observer + install_system_packages + install_openresty + setup_observer_runtime_directories + install_node_binaries + install_checkpointer + install_observer_custodian + setup_observer_keys + deploy_openresty_configuration + deploy_observer_supervisor_configuration + print_observer_manual_start_instructions + success "Observer installation complete; services were not started." +} + +main "$@" diff --git a/install/install-validator.sh b/install/install-validator.sh index 14b70d4..41248a3 100755 --- a/install/install-validator.sh +++ b/install/install-validator.sh @@ -44,7 +44,7 @@ main() { install_system_packages install_openresty setup_runtime_directories - install_validator_binaries + install_node_binaries install_checkpointer install_custodian setup_validator_keys diff --git a/install/lib/binaries.sh b/install/lib/binaries.sh index 4b41311..26977bf 100644 --- a/install/lib/binaries.sh +++ b/install/lib/binaries.sh @@ -283,9 +283,13 @@ install_reth_binary() { esac } -install_validator_binaries() { - section "Installing validator binaries" +install_node_binaries() { + section "Installing node binaries" install_summit_binary install_reth_binary success "Summit and seismic-reth installation methods completed." } + +install_validator_binaries() { + install_node_binaries +} diff --git a/install/lib/configuration.sh b/install/lib/configuration.sh index fe3c6ee..d2c4a12 100644 --- a/install/lib/configuration.sh +++ b/install/lib/configuration.sh @@ -42,14 +42,14 @@ configure_service_user() { [[ "$default_user" != "root" ]] || default_user="ubuntu" while true; do - prompt SERVICE_USER "User to run validator services as" "$default_user" + prompt SERVICE_USER "User to run node services as" "$default_user" if [[ ! "$SERVICE_USER" =~ ^[a-z_][a-z0-9_-]*\$?$ ]]; then error "Invalid service username: $SERVICE_USER" continue fi if [[ "$SERVICE_USER" == "root" ]]; then - error "Validator services must not run as root." + error "Node services must not run as root." continue fi if ! id "$SERVICE_USER" >/dev/null 2>&1; then @@ -310,8 +310,8 @@ print_component_installation() { esac } -configure_validator_software() { - section "Validator software configuration" +configure_node_software() { + section "Node software configuration" SUMMIT_TARGET_BIN="/usr/local/bin/summit" RETH_TARGET_BIN="/usr/local/bin/seismic-reth" @@ -327,13 +327,17 @@ configure_validator_software() { configure_component_installation \ RETH_INSTALL_METHOD RETH_BINARY "seismic-reth" RETH_TARGET_BIN - _out "Validator software:" + _out "Node software:" print_component_installation \ "Summit" "$SUMMIT_INSTALL_METHOD" "$SUMMIT_BINARY" "$SUMMIT_TARGET_BIN" print_component_installation \ "Seismic Reth" "$RETH_INSTALL_METHOD" "$RETH_BINARY" "$RETH_TARGET_BIN" } +configure_validator_software() { + configure_node_software +} + configure_public_endpoint() { section "Public endpoint configuration" @@ -588,10 +592,13 @@ configure_custodian() { INSTALL_CUSTODIAN=false CUSTODIAN_DATA_DIR="" CUSTODIAN_TARGET_BIN="/usr/local/bin/seismic-centralized-custodian-service" + CUSTODIAN_INSTALL_METHOD="" + CUSTODIAN_BINARY="" CUSTODIAN_SOCKET="" COUNCIL_LISTEN="" COUNCIL_ADDRESS="" CUSTODIAN_CHAIN_ID="" + PARENT_CUSTODIAN="" CUSTODIAN_REQUIRED_SUMMIT_REF="m/metrics" CUSTODIAN_REQUIRED_RETH_REF="feat/purpose-key-rotation-reth" CUSTODIAN_SOURCE_REF="d/centralized-custodian" @@ -635,8 +642,25 @@ configure_custodian() { done success "Custodian Unix socket selected: $CUSTODIAN_SOCKET" - _out "Custodian will use the publicly known shared default root key." - warn "The shared default makes epoch-0 purpose keys public." + if [[ "${NODE_ROLE:-validator}" == "observer" ]]; then + _out "Enter the Custodian council endpoint running on the parent validator." + _out "The observer uses it to fetch or verify the root key and synchronize epoch-key deliveries." + while true; do + prompt PARENT_CUSTODIAN \ + "Parent validator Custodian council endpoint (host:port; default port 7876)" \ + "" + if validate_host_port "$PARENT_CUSTODIAN"; then + break + fi + error "Parent validator Custodian council endpoint must be host:port with a valid port." + done + _out "The observer Custodian will fetch and verify its root key through $PARENT_CUSTODIAN." + warn "The parent Custodian connection transports root-key and epoch-key material." + warn "Use a private network or protect the connection with a TLS tunnel." + else + _out "Custodian will use the publicly known shared default root key." + warn "The shared default makes epoch-0 purpose keys public." + fi while true; do prompt COUNCIL_LISTEN "Custodian council listen address" "0.0.0.0:7876" @@ -666,17 +690,32 @@ configure_custodian() { error "Custodian chain ID must be a positive integer." done + configure_component_installation \ + CUSTODIAN_INSTALL_METHOD \ + CUSTODIAN_BINARY \ + "Centralized Custodian" \ + CUSTODIAN_TARGET_BIN + warn "Custodian requires Summit compatible with $CUSTODIAN_REQUIRED_SUMMIT_REF." warn "Custodian requires seismic-reth compatible with $CUSTODIAN_REQUIRED_RETH_REF." _out "Centralized Custodian: $INSTALL_CUSTODIAN" _out " Data: $CUSTODIAN_DATA_DIR" _out " Socket: $CUSTODIAN_SOCKET" - _out " Root key: publicly known shared default" + if [[ "${NODE_ROLE:-validator}" == "observer" ]]; then + _out " Root key: fetched and verified through parent Custodian" + _out " Parent: $PARENT_CUSTODIAN" + else + _out " Root key: publicly known shared default" + fi _out " Council: $COUNCIL_LISTEN ($COUNCIL_ADDRESS)" _out " Chain ID: $CUSTODIAN_CHAIN_ID" - _out " Binary: build from source -> $CUSTODIAN_TARGET_BIN" - _out " Source ref: $CUSTODIAN_SOURCE_REF" + print_component_installation \ + "Custodian" "$CUSTODIAN_INSTALL_METHOD" \ + "$CUSTODIAN_BINARY" "$CUSTODIAN_TARGET_BIN" + if [[ "$CUSTODIAN_INSTALL_METHOD" == "source" ]]; then + _out " Source ref: $CUSTODIAN_SOURCE_REF" + fi } configure_directories() { @@ -779,14 +818,23 @@ print_configuration_summary() { _out " Enabled: true" _out " Data: $CUSTODIAN_DATA_DIR" _out " Socket: $CUSTODIAN_SOCKET" - _out " Root key: publicly known shared default" + if [[ "${NODE_ROLE:-validator}" == "observer" ]]; then + _out " Root key: fetched and verified through parent Custodian" + _out " Parent Custodian: $PARENT_CUSTODIAN" + else + _out " Root key: publicly known shared default" + fi _out " Council listen: $COUNCIL_LISTEN" _out " Council address: $COUNCIL_ADDRESS" _out " Chain ID: $CUSTODIAN_CHAIN_ID" - _out " Binary: build from source -> $CUSTODIAN_TARGET_BIN" + print_component_installation \ + "Custodian" "$CUSTODIAN_INSTALL_METHOD" \ + "$CUSTODIAN_BINARY" "$CUSTODIAN_TARGET_BIN" _out " Required Summit compatibility: $CUSTODIAN_REQUIRED_SUMMIT_REF" _out " Required Reth compatibility: $CUSTODIAN_REQUIRED_RETH_REF" - _out " Custodian source ref: $CUSTODIAN_SOURCE_REF" + if [[ "$CUSTODIAN_INSTALL_METHOD" == "source" ]]; then + _out " Custodian source ref: $CUSTODIAN_SOURCE_REF" + fi else _out " Enabled: false" fi @@ -841,6 +889,7 @@ review_configuration() { } configure() { + NODE_ROLE="validator" section "Configuration" configure_service_user configure_directories diff --git a/install/lib/custodian.sh b/install/lib/custodian.sh index 39a2a22..8fd5368 100644 --- a/install/lib/custodian.sh +++ b/install/lib/custodian.sh @@ -9,25 +9,66 @@ install_custodian_binary() { local source_dir="$SERVICE_HOME/src/enclave" local build_output="$source_dir/target/release/seismic-centralized-custodian-service" - install_rust_for_service_user - prepare_source_checkout \ - "Centralized Custodian" \ - "$CUSTODIAN_REPO" \ - "$source_dir" \ - "$CUSTODIAN_SOURCE_REF" - - info "Building Centralized Custodian from $CUSTODIAN_SOURCE_REF..." - # shellcheck disable=SC2016 - if ! run_as_service_user bash -c \ - 'source "$HOME/.cargo/env"; cd "$1"; cargo build --release -p seismic-centralized-custodian-service' \ - _ "$source_dir" >>"$LOG_FILE" 2>&1; then - die "Centralized Custodian build failed; see $LOG_FILE" + case "$CUSTODIAN_INSTALL_METHOD" in + prebuilt) + install_binary_target \ + "Centralized Custodian" \ + "$CUSTODIAN_BINARY" \ + "$CUSTODIAN_TARGET_BIN" + ;; + source) + install_rust_for_service_user + prepare_source_checkout \ + "Centralized Custodian" \ + "$CUSTODIAN_REPO" \ + "$source_dir" \ + "$CUSTODIAN_SOURCE_REF" + + info "Building Centralized Custodian from $CUSTODIAN_SOURCE_REF..." + # shellcheck disable=SC2016 + if ! run_as_service_user bash -c \ + 'source "$HOME/.cargo/env"; cd "$1"; cargo build --release -p seismic-centralized-custodian-service' \ + _ "$source_dir" >>"$LOG_FILE" 2>&1; then + die "Centralized Custodian build failed; see $LOG_FILE" + fi + + install_binary_target \ + "Centralized Custodian" \ + "$build_output" \ + "$CUSTODIAN_TARGET_BIN" + ;; + deferred) + info "Centralized Custodian installation deferred; expected executable: $CUSTODIAN_TARGET_BIN" + ;; + *) + die "Unknown Centralized Custodian installation method: $CUSTODIAN_INSTALL_METHOD" + ;; + esac +} + +validate_custodian_cli_support() { + local help_output + local security_error + + if [[ ! -e "$CUSTODIAN_TARGET_BIN" && ! -L "$CUSTODIAN_TARGET_BIN" ]]; then + return 1 + fi + if ! security_error=$(check_service_executable_security "$CUSTODIAN_TARGET_BIN"); then + die "Custodian executable is not safe for service use: $security_error" + fi + + help_output=$("$CUSTODIAN_TARGET_BIN" --help 2>&1) \ + || die "Could not inspect the installed Custodian command-line interface." + grep -q -- '--summit-key-dir' <<<"$help_output" \ + || die "Installed Custodian does not support --summit-key-dir." + if [[ "${NODE_ROLE:-validator}" == "observer" ]]; then + grep -q -- '--observer' <<<"$help_output" \ + || die "Installed Custodian does not support --observer." + grep -q -- '--parent-custodian' <<<"$help_output" \ + || die "Installed Custodian does not support --parent-custodian." fi - install_binary_target \ - "Centralized Custodian" \ - "$build_output" \ - "$CUSTODIAN_TARGET_BIN" + success "Custodian command-line compatibility validated" } read_binary_file_hex() { @@ -74,6 +115,40 @@ install_custodian() { section "Installing Centralized Custodian" install_custodian_binary + if ! validate_custodian_cli_support; then + warn "Custodian executable is deferred and not present at $CUSTODIAN_TARGET_BIN." + warn "Provide a compatible executable before starting Custodian." + fi install_custodian_root_key - success "Centralized Custodian binary and persistent root-key state are ready." + success "Centralized Custodian installation and persistent root-key preparation are complete." +} + +prepare_observer_custodian_root_key() { + local target="$CUSTODIAN_DATA_DIR/root.key" + + if [[ ! -e "$target" && ! -L "$target" ]]; then + info "No observer Custodian root key is present; it will be fetched from the parent Custodian on first start." + return + fi + + validate_existing_custodian_root_key "$target" + chown "$SERVICE_USER:$SERVICE_GROUP" "$target" + chmod 0600 "$target" + success "Existing observer Custodian root key preserved for parent verification on startup" +} + +install_observer_custodian() { + if [[ "$INSTALL_CUSTODIAN" != true ]]; then + info "Centralized Custodian disabled; skipping observer Custodian installation." + return + fi + + section "Installing observer Centralized Custodian" + install_custodian_binary + if ! validate_custodian_cli_support; then + warn "Observer Custodian executable is deferred and not present at $CUSTODIAN_TARGET_BIN." + warn "Provide a compatible executable before starting Custodian." + fi + prepare_observer_custodian_root_key + success "Observer Custodian installation and persistent root-key preparation are complete." } diff --git a/install/lib/directories.sh b/install/lib/directories.sh index ec2e937..6f8dede 100644 --- a/install/lib/directories.sh +++ b/install/lib/directories.sh @@ -36,7 +36,7 @@ setup_runtime_directories() { prepare_runtime_directory "$RETH_DATA_DIR" 0750 "Reth data directory" prepare_runtime_directory "$SUMMIT_DATA_DIR" 0750 "Summit data directory" - prepare_runtime_directory "$VALIDATOR_KEYS_DIR" 0700 "Validator keys directory" + prepare_runtime_directory "$VALIDATOR_KEYS_DIR" 0700 "Node keys directory" if [[ "$INSTALL_CHECKPOINTER" == true ]]; then prepare_runtime_directory \ diff --git a/install/lib/observer-configuration.sh b/install/lib/observer-configuration.sh new file mode 100644 index 0000000..30f7dec --- /dev/null +++ b/install/lib/observer-configuration.sh @@ -0,0 +1,256 @@ +#!/usr/bin/env bash + +# Observer-specific interactive configuration. + +validate_observer_socket_address() { + local value=$1 + + python3 - "$value" <<'PY' >/dev/null 2>&1 +import ipaddress +import sys + +value = sys.argv[1] +try: + if value.startswith("["): + end = value.index("]") + host = value[1:end] + if value[end + 1 : end + 2] != ":": + raise ValueError + port = int(value[end + 2 :]) + else: + host, raw_port = value.rsplit(":", 1) + port = int(raw_port) + ipaddress.ip_address(host) + if not 1 <= port <= 65535: + raise ValueError +except (ValueError, IndexError): + raise SystemExit(1) +PY +} + +normalize_observer_public_key() { + local value=$1 + + value=${value#0x} + printf '%s\n' "${value,,}" +} + +configure_observer_assignment() { + local selected + + section "Observer assignment configuration" + + while true; do + prompt OBSERVER_PARENT_NODE_PUBLIC_KEY \ + "Parent validator Summit node public key" "" + if [[ "$OBSERVER_PARENT_NODE_PUBLIC_KEY" =~ ^(0x)?[0-9a-fA-F]{64}$ ]]; then + OBSERVER_PARENT_NODE_PUBLIC_KEY=$(normalize_observer_public_key \ + "$OBSERVER_PARENT_NODE_PUBLIC_KEY") + break + fi + error "Parent node public key must be a 32-byte hexadecimal value." + done + + while true; do + prompt OBSERVER_INDEX "Observer derivation index" "0" + if [[ "$OBSERVER_INDEX" =~ ^[0-9]{1,3}$ ]] \ + && ((10#$OBSERVER_INDEX <= 255)); then + OBSERVER_INDEX=$((10#$OBSERVER_INDEX)) + break + fi + error "Observer index must be an integer from 0 through 255." + done + + while true; do + prompt OBSERVER_PUBLIC_ADDRESS \ + "Observer public Summit P2P address (IP:port; default Summit port 18551)" "" + if validate_observer_socket_address "$OBSERVER_PUBLIC_ADDRESS"; then + break + fi + error "Observer public address must be an IPv4:port or [IPv6]:port socket address." + done + OBSERVER_P2P_PORT=${OBSERVER_PUBLIC_ADDRESS##*:} + + OBSERVER_BOOTSTRAPPERS_SOURCE="" + while true; do + prompt selected "Summit bootstrappers TOML path (blank for none)" "" + if [[ -z "$selected" ]]; then + break + fi + if [[ "$selected" != /* ]]; then + error "Summit bootstrappers TOML path must be absolute." + continue + fi + if contains_unsafe_path_characters "$selected"; then + error "Summit bootstrappers TOML path contains unsupported whitespace or shell characters." + continue + fi + if [[ -L "$selected" || ! -s "$selected" ]]; then + error "Summit bootstrappers TOML must be a non-empty regular file and not a symbolic link." + continue + fi + selected=$(realpath -- "$selected") + if ! run_as_service_user test -r "$selected"; then + error "Service user $SERVICE_USER cannot read the Summit bootstrappers TOML: $selected" + continue + fi + OBSERVER_BOOTSTRAPPERS_SOURCE="$selected" + break + done + + _out "Observer assignment:" + _out " Parent key: $OBSERVER_PARENT_NODE_PUBLIC_KEY" + _out " Index: $OBSERVER_INDEX" + _out " Public P2P: $OBSERVER_PUBLIC_ADDRESS" + _out " Bootstrappers: ${OBSERVER_BOOTSTRAPPERS_SOURCE:-none}" + warn "The installer does not verify whether the parent is a genesis or current validator." +} + +configure_observer_directories() { + section "Observer data directory configuration" + + configure_directory \ + RETH_DATA_DIR \ + "Reth data directory" \ + "/persistence/reth" \ + "Stores the Reth execution database, static files, and other execution-layer state." + configure_directory \ + SUMMIT_DATA_DIR \ + "Summit data directory" \ + "/persistence/summit" \ + "Stores the observer's mutable Summit consensus state." + configure_directory \ + VALIDATOR_KEYS_DIR \ + "Observer keys directory" \ + "/persistence/keys" \ + "Stores the Reth P2P identity, observer consensus key, and provisioned parent node key." + + RETH_P2P_KEY_PATH="$VALIDATOR_KEYS_DIR/reth/p2p-key" + SUMMIT_KEYS_DIR="$VALIDATOR_KEYS_DIR/summit" + OBSERVER_ASSIGNMENT_FILE="$SUMMIT_KEYS_DIR/observer-assignment" + OBSERVER_BOOTSTRAPPERS_FILE="$SUMMIT_KEYS_DIR/bootstrappers.toml" + OBSERVER_STORE_DIR="$SUMMIT_DATA_DIR" + OBSERVER_CRITICAL_LOG_DIR="/var/log/seismic-observer/critical" +} + +print_observer_configuration_summary() { + section "Observer configuration summary" + + _out "Service user: $SERVICE_USER ($SERVICE_GROUP)" + _out "Service home: $SERVICE_HOME" + _out "Directories:" + _out " Reth data: $RETH_DATA_DIR" + _out " Summit data: $SUMMIT_DATA_DIR" + _out " Observer keys: $VALIDATOR_KEYS_DIR" + _out " Summit store: $OBSERVER_STORE_DIR" + + _out "Observer assignment:" + _out " Parent key: $OBSERVER_PARENT_NODE_PUBLIC_KEY" + _out " Index: $OBSERVER_INDEX" + _out " Public P2P: $OBSERVER_PUBLIC_ADDRESS" + _out " Bootstrappers: ${OBSERVER_BOOTSTRAPPERS_SOURCE:-none}" + + _out "Network bootstrap:" + _out " Genesis: $GENESIS_PATH" + _out " Reth bootnode RPC: ${BOOTNODE_RPC:-none}" + + _out "Public endpoint:" + if [[ "$CONFIGURE_PUBLIC_ENDPOINT" == true ]]; then + _out " https://$DOMAIN" + _out " Rate limit: $RATE_LIMIT_RPS requests/sec, burst $RATE_LIMIT_BURST" + else + _out " Disabled" + fi + + _out "Node software:" + print_component_installation \ + "Summit" "$SUMMIT_INSTALL_METHOD" "$SUMMIT_BINARY" "$SUMMIT_TARGET_BIN" + print_component_installation \ + "Seismic Reth" "$RETH_INSTALL_METHOD" "$RETH_BINARY" "$RETH_TARGET_BIN" + + _out "Summit-checkpointer: $INSTALL_CHECKPOINTER" + if [[ "$INSTALL_CHECKPOINTER" == true ]]; then + print_component_installation \ + "Checkpointer" "$CHECKPOINTER_INSTALL_METHOD" \ + "$CHECKPOINTER_BINARY" "$CHECKPOINTER_TARGET_BIN" + print_mdbx_copy_plan + fi + + _out "Centralized Custodian: $INSTALL_CUSTODIAN" + if [[ "$INSTALL_CUSTODIAN" == true ]]; then + _out " Parent Custodian: $PARENT_CUSTODIAN" + _out " Root key: fetched and verified through the parent" + _out " Council: $COUNCIL_LISTEN ($COUNCIL_ADDRESS)" + _out " Chain ID: $CUSTODIAN_CHAIN_ID" + print_component_installation \ + "Custodian" "$CUSTODIAN_INSTALL_METHOD" \ + "$CUSTODIAN_BINARY" "$CUSTODIAN_TARGET_BIN" + if [[ "$CUSTODIAN_INSTALL_METHOD" == "source" ]]; then + _out " Source ref: $CUSTODIAN_SOURCE_REF" + fi + fi + + print_system_package_plan +} + +review_observer_configuration() { + local selection + + while true; do + print_observer_configuration_summary + _out "" + _out "What would you like to do?" + _out " 1) Edit service user" + _out " 2) Edit directories" + _out " 3) Edit public endpoint" + _out " 4) Edit network bootstrap" + _out " 5) Edit observer assignment" + _out " 6) Edit node software" + _out " 7) Edit summit-checkpointer" + _out " 8) Edit Centralized Custodian" + _out " 9) Accept configuration" + _out " 10) Cancel" + prompt selection "Select an action" "9" + + case "$selection" in + 1) configure_service_user ;; + 2) configure_observer_directories ;; + 3) configure_public_endpoint ;; + 4) configure_network_bootstrap ;; + 5) configure_observer_assignment ;; + 6) configure_node_software ;; + 7) configure_checkpointer ;; + 8) configure_custodian ;; + 9) + if ! validate_network_bootstrap_configuration; then + warn "Network bootstrap validation failed; please configure it again." + configure_network_bootstrap + continue + fi + if confirm "Accept this configuration?"; then + success "Configuration accepted" + return + fi + ;; + 10) + info "Configuration cancelled; no installation changes were made." + exit 0 + ;; + *) error "Select a number from 1 through 10." ;; + esac + done +} + +configure_observer() { + NODE_ROLE="observer" + section "Configuration" + configure_service_user + configure_observer_directories + configure_public_endpoint + configure_network_bootstrap + configure_observer_assignment + configure_node_software + configure_checkpointer + configure_custodian + review_observer_configuration +} diff --git a/install/lib/observer-directories.sh b/install/lib/observer-directories.sh new file mode 100644 index 0000000..f523368 --- /dev/null +++ b/install/lib/observer-directories.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Observer runtime directories in addition to the shared node directories. + +setup_observer_runtime_directories() { + setup_runtime_directories + + if [[ -L "$OBSERVER_CRITICAL_LOG_DIR" ]]; then + die "Observer critical-log directory must not be a symbolic link: $OBSERVER_CRITICAL_LOG_DIR" + fi + install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \ + -- "$OBSERVER_CRITICAL_LOG_DIR" +} diff --git a/install/lib/observer-instructions.sh b/install/lib/observer-instructions.sh new file mode 100644 index 0000000..68d2351 --- /dev/null +++ b/install/lib/observer-instructions.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +# Observer readiness notes and manual service-start commands. + +print_missing_observer_prerequisites() { + local missing=false + local security_error + + if ! security_error=$(check_service_executable_security "$RETH_TARGET_BIN"); then + warn "seismic-reth cannot start until its executable is secured at $RETH_TARGET_BIN: $security_error." + missing=true + fi + if ! security_error=$(check_service_executable_security "$SUMMIT_TARGET_BIN"); then + warn "Summit cannot start until its executable is secured at $SUMMIT_TARGET_BIN: $security_error." + missing=true + elif ! run_as_service_user "$SUMMIT_TARGET_BIN" run --help 2>&1 \ + | grep -q -- '--observer'; then + warn "Summit does not support the required --observer option: $SUMMIT_TARGET_BIN." + missing=true + fi + if [[ ! -f "$RETH_P2P_KEY_PATH" ]]; then + warn "seismic-reth cannot start until its P2P key exists at $RETH_P2P_KEY_PATH." + missing=true + fi + if [[ ! -f "$SUMMIT_KEYS_DIR/consensus_key.pem" ]]; then + warn "Summit cannot start until the observer consensus key exists at $SUMMIT_KEYS_DIR/consensus_key.pem." + missing=true + fi + if [[ ! -f "$SUMMIT_KEYS_DIR/node_key.pem" ]]; then + warn "Summit and observer Custodian cannot start until the parent node key is provisioned at $SUMMIT_KEYS_DIR/node_key.pem." + missing=true + fi + + if [[ "$INSTALL_CUSTODIAN" == true ]] \ + && ! security_error=$(check_service_executable_security "$CUSTODIAN_TARGET_BIN"); then + warn "Custodian cannot start until its executable is secured at $CUSTODIAN_TARGET_BIN: $security_error." + missing=true + fi + + if [[ "$INSTALL_CHECKPOINTER" == true ]]; then + if ! security_error=$(check_service_executable_security "$CHECKPOINTER_TARGET_BIN"); then + warn "summit-checkpointer cannot start until its executable is secured at $CHECKPOINTER_TARGET_BIN: $security_error." + missing=true + fi + if ! security_error=$(check_service_executable_security "$MDBX_COPY_TARGET_BIN"); then + warn "summit-checkpointer cannot start until a compatible mdbx_copy is secured at $MDBX_COPY_TARGET_BIN: $security_error." + missing=true + fi + fi + + if [[ "$missing" == true ]]; then + warn "Resolve the missing prerequisites above before running the start commands." + else + success "All enabled observer service executables and keys are present." + fi +} + +print_observer_manual_start_instructions() { + section "Manual observer service start" + print_missing_observer_prerequisites + + _out "The installer did not start, enable, reread, or update observer services." + _out "Before starting the observer, provide the parent validator's private node_key.pem at:" + _out " $SUMMIT_KEYS_DIR/node_key.pem" + _out "It must be owned by $SERVICE_USER:$SERVICE_GROUP with mode 0600." + _out "" + _out "After resolving all warnings, load the Supervisor configuration:" + _out "" + _out " sudo systemctl enable --now supervisor" + _out " sudo supervisorctl reread" + _out " sudo supervisorctl update" + _out "" + if [[ "$INSTALL_CUSTODIAN" == true ]]; then + _out "Start the observer Custodian first. It will fetch or verify its root key" + _out "through the parent Custodian at $PARENT_CUSTODIAN:" + _out " sudo supervisorctl start custodian" + fi + _out " sudo supervisorctl start reth" + _out " sudo supervisorctl start summit-observer" + if [[ "$INSTALL_CHECKPOINTER" == true ]]; then + _out " sudo supervisorctl start checkpointer" + fi + + if [[ "$CONFIGURE_PUBLIC_ENDPOINT" == true ]]; then + _out "" + _out "Start or reload OpenResty explicitly:" + _out " sudo systemctl enable openresty" + _out " if sudo systemctl is-active --quiet openresty; then" + _out " sudo systemctl reload openresty" + _out " else" + _out " sudo systemctl start openresty" + _out " fi" + fi + + _out "" + _out "Inspect status with:" + _out " sudo supervisorctl status" + _out "" + warn "Supervisor programs use autostart=false and autorestart=false." + warn "Start them manually again after a server or Supervisor restart." + if [[ "$INSTALL_CUSTODIAN" == true ]]; then + warn "This host needs outbound TCP access to parent Custodian $PARENT_CUSTODIAN." + warn "The parent Custodian firewall must allow this observer's source IP." + warn "Root-key and plaintext epoch-key material transit this connection; use a private network or TLS tunnel." + fi +} diff --git a/install/lib/observer-keys.sh b/install/lib/observer-keys.sh new file mode 100644 index 0000000..2b44ff2 --- /dev/null +++ b/install/lib/observer-keys.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash + +# Observer key generation, assignment preservation, and bootstrapper configuration. + +read_observer_node_public_key() { + local output + local public_key + + output=$(run_as_service_user "$SUMMIT_TARGET_BIN" keys show \ + --key-store-path "$SUMMIT_KEYS_DIR" 2>>"$LOG_FILE") \ + || return 1 + public_key=$(printf '%s\n' "$output" \ + | awk -F': ' '/Node Public Key \(ed25519\)/ {print tolower($2); exit}') + public_key=${public_key#0x} + [[ "$public_key" =~ ^[0-9a-f]{64}$ ]] || return 1 + printf '%s\n' "$public_key" +} + +validate_observer_parent_node_key() { + local node_key="$SUMMIT_KEYS_DIR/node_key.pem" + local installed_public_key + + validate_summit_key_file "$node_key" "Observer parent Summit node key" + if [[ ! -x "$SUMMIT_TARGET_BIN" ]]; then + warn "The parent node key cannot be checked against its expected public key until Summit is installed." + return + fi + + installed_public_key=$(read_observer_node_public_key) \ + || die "Could not read the public key from the provisioned observer parent node key; see $LOG_FILE" + [[ "$installed_public_key" == "$OBSERVER_PARENT_NODE_PUBLIC_KEY" ]] \ + || die "Provisioned node_key.pem does not match parent public key $OBSERVER_PARENT_NODE_PUBLIC_KEY" + success "Provisioned observer parent node key matches the configured public key" +} + +write_observer_assignment() { + local expected="$OBSERVER_PARENT_NODE_PUBLIC_KEY:$OBSERVER_INDEX" + local staging + + staging=$(mktemp "$SUMMIT_KEYS_DIR/.observer-assignment.XXXXXX") + printf '%s\n' "$expected" >"$staging" + chown "$SERVICE_USER:$SERVICE_GROUP" "$staging" + chmod 0600 "$staging" + mv -- "$staging" "$OBSERVER_ASSIGNMENT_FILE" +} + +configure_observer_bootstrapper_file() { + local staging + + if [[ -z "$OBSERVER_BOOTSTRAPPERS_SOURCE" ]]; then + if [[ -L "$OBSERVER_BOOTSTRAPPERS_FILE" ]]; then + die "Observer bootstrapper configuration must not be a symbolic link: $OBSERVER_BOOTSTRAPPERS_FILE" + fi + rm -f -- "$OBSERVER_BOOTSTRAPPERS_FILE" + return + fi + + [[ ! -L "$OBSERVER_BOOTSTRAPPERS_SOURCE" \ + && -s "$OBSERVER_BOOTSTRAPPERS_SOURCE" ]] \ + || die "Summit bootstrappers source became invalid: $OBSERVER_BOOTSTRAPPERS_SOURCE" + run_as_service_user test -r "$OBSERVER_BOOTSTRAPPERS_SOURCE" \ + || die "Service user cannot read the Summit bootstrappers source: $OBSERVER_BOOTSTRAPPERS_SOURCE" + if [[ -L "$OBSERVER_BOOTSTRAPPERS_FILE" ]]; then + die "Observer bootstrapper configuration must not be a symbolic link: $OBSERVER_BOOTSTRAPPERS_FILE" + fi + + staging=$(mktemp "$SUMMIT_KEYS_DIR/.bootstrappers.toml.XXXXXX") + if ! cp -- "$OBSERVER_BOOTSTRAPPERS_SOURCE" "$staging"; then + rm -f -- "$staging" + die "Could not install the Summit bootstrappers TOML." + fi + chown "$SERVICE_USER:$SERVICE_GROUP" "$staging" + chmod 0640 "$staging" + mv -- "$staging" "$OBSERVER_BOOTSTRAPPERS_FILE" + success "Summit bootstrappers TOML installed: $OBSERVER_BOOTSTRAPPERS_FILE" +} + +setup_summit_observer_keys() { + local node_key="$SUMMIT_KEYS_DIR/node_key.pem" + local consensus_key="$SUMMIT_KEYS_DIR/consensus_key.pem" + local expected_assignment="$OBSERVER_PARENT_NODE_PUBLIC_KEY:$OBSERVER_INDEX" + local existing_assignment="" + local staging + + prepare_runtime_directory "$SUMMIT_KEYS_DIR" 0700 "Summit observer keys directory" + + if [[ -e "$OBSERVER_ASSIGNMENT_FILE" || -L "$OBSERVER_ASSIGNMENT_FILE" ]]; then + [[ ! -L "$OBSERVER_ASSIGNMENT_FILE" && -f "$OBSERVER_ASSIGNMENT_FILE" ]] \ + || die "Observer assignment marker must be a regular file: $OBSERVER_ASSIGNMENT_FILE" + existing_assignment=$(<"$OBSERVER_ASSIGNMENT_FILE") + [[ "$existing_assignment" == "$expected_assignment" ]] \ + || die "Observer keys are assigned to $existing_assignment, not $expected_assignment; use separate persistent paths for a different assignment." + chown "$SERVICE_USER:$SERVICE_GROUP" "$OBSERVER_ASSIGNMENT_FILE" + chmod 0600 "$OBSERVER_ASSIGNMENT_FILE" + elif [[ -e "$node_key" || -L "$node_key" \ + || -e "$consensus_key" || -L "$consensus_key" ]]; then + die "Summit keys exist without an observer assignment marker; refusing to reuse or replace them." + else + write_observer_assignment + fi + + if [[ -e "$consensus_key" || -L "$consensus_key" ]]; then + validate_summit_key_file "$consensus_key" "Observer Summit consensus key" + else + if [[ ! -f "$SUMMIT_TARGET_BIN" || ! -x "$SUMMIT_TARGET_BIN" ]]; then + warn "Observer consensus key was not generated because Summit is unavailable at $SUMMIT_TARGET_BIN." + warn "Install Summit and rerun the observer installer before provisioning the parent node key." + configure_observer_bootstrapper_file + return + fi + if ! run_as_service_user "$SUMMIT_TARGET_BIN" run --help 2>&1 \ + | grep -q -- '--observer'; then + die "Installed Summit binary does not support observer mode: $SUMMIT_TARGET_BIN" + fi + + staging=$(mktemp -d /run/seismic-observer-keys.XXXXXX) + chown "$SERVICE_USER:$SERVICE_GROUP" "$staging" + chmod 0700 "$staging" + info "Generating a fresh observer-only Summit consensus key..." + if ! run_as_service_user "$SUMMIT_TARGET_BIN" keys generate \ + --key-store-path "$staging" -y >>"$LOG_FILE" 2>&1; then + rm -rf -- "$staging" + die "Observer Summit consensus-key generation failed; see $LOG_FILE" + fi + if [[ ! -s "$staging/consensus_key.pem" \ + || -L "$staging/consensus_key.pem" ]]; then + rm -rf -- "$staging" + die "Summit did not produce an observer consensus key." + fi + install -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0600 \ + -- "$staging/consensus_key.pem" "$consensus_key" + rm -rf -- "$staging" + success "Observer Summit consensus key generated: $consensus_key" + fi + + if [[ -e "$node_key" || -L "$node_key" ]]; then + validate_observer_parent_node_key + else + warn "Parent node_key.pem is not installed yet. Provision it securely before starting the observer or observer Custodian." + fi + + configure_observer_bootstrapper_file +} + +setup_observer_keys() { + section "Setting up observer keys" + setup_reth_p2p_key + setup_summit_observer_keys + success "Observer key phase complete." +} diff --git a/install/lib/observer-supervisor.sh b/install/lib/observer-supervisor.sh new file mode 100644 index 0000000..b6c66d0 --- /dev/null +++ b/install/lib/observer-supervisor.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash + +# Observer Supervisor rendering without service activation. + +SUPERVISOR_CONFIG_PATH="/etc/supervisor/conf.d/seismic-observer.conf" +SUPERVISOR_LOG_DIR="/var/log/seismic-observer" + +validate_observer_supervisor_templates() { + local template_root="$TEMPLATES_DIR/supervisor" + local required=( + "$template_root/reth.conf" + "$template_root/observer.conf" + "$template_root/observer-custodian.conf" + "$template_root/checkpointer.conf" + "$template_root/checkpointer.toml" + ) + local path + + for path in "${required[@]}"; do + [[ -f "$path" ]] || die "Required observer Supervisor template not found: $path" + done +} + +validate_observer_supervisor_runtime_inputs() { + validate_supervisor_runtime_inputs + + [[ -f "$OBSERVER_ASSIGNMENT_FILE" && ! -L "$OBSERVER_ASSIGNMENT_FILE" ]] \ + || die "Observer assignment marker is missing: $OBSERVER_ASSIGNMENT_FILE" + [[ "$(<"$OBSERVER_ASSIGNMENT_FILE")" == "$OBSERVER_PARENT_NODE_PUBLIC_KEY:$OBSERVER_INDEX" ]] \ + || die "Observer assignment marker does not match the accepted configuration." +} + +render_observer_supervisor_program() { + local program_name=$1 + local store_dir=$2 + local template="$TEMPLATES_DIR/supervisor/observer.conf" + local conf + local bootstrappers_argument="" + + if [[ -n "$OBSERVER_BOOTSTRAPPERS_SOURCE" ]]; then + bootstrappers_argument=" --bootstrappers $OBSERVER_BOOTSTRAPPERS_FILE" + fi + + conf=$(<"$template") + conf=${conf//OBSERVER_PROGRAM_PLACEHOLDER/$program_name} + conf=${conf//SUMMIT_BINARY_PLACEHOLDER/$SUMMIT_TARGET_BIN} + conf=${conf//OBSERVER_INDEX_PLACEHOLDER/$OBSERVER_INDEX} + conf=${conf//OBSERVER_PUBLIC_ADDRESS_PLACEHOLDER/$OBSERVER_PUBLIC_ADDRESS} + conf=${conf//OBSERVER_P2P_PORT_PLACEHOLDER/$OBSERVER_P2P_PORT} + conf=${conf//GENESIS_PATH_PLACEHOLDER/$GENESIS_PATH} + conf=${conf//SUMMIT_KEYS_DIR_PLACEHOLDER/$SUMMIT_KEYS_DIR} + conf=${conf//OBSERVER_STORE_DIR_PLACEHOLDER/$store_dir} + conf=${conf//BOOTSTRAPPERS_ARGUMENT_PLACEHOLDER/$bootstrappers_argument} + conf=${conf//OBSERVER_CRITICAL_LOG_DIR_PLACEHOLDER/$OBSERVER_CRITICAL_LOG_DIR} + conf=${conf//SERVICE_USER_PLACEHOLDER/$SERVICE_USER} + conf=${conf//SUPERVISOR_LOG_DIR_PLACEHOLDER/$SUPERVISOR_LOG_DIR} + printf '%s\n' "$conf" +} + +render_observer_custodian_supervisor_config() { + local template="$TEMPLATES_DIR/supervisor/observer-custodian.conf" + local conf + + conf=$(<"$template") + conf=${conf//CUSTODIAN_BINARY_PLACEHOLDER/$CUSTODIAN_TARGET_BIN} + conf=${conf//CUSTODIAN_SOCKET_PLACEHOLDER/$CUSTODIAN_SOCKET} + conf=${conf//CUSTODIAN_ROOT_KEY_PLACEHOLDER/$CUSTODIAN_DATA_DIR/root.key} + conf=${conf//CUSTODIAN_DELIVERY_DIR_PLACEHOLDER/$CUSTODIAN_DATA_DIR/deliveries} + conf=${conf//COUNCIL_LISTEN_PLACEHOLDER/$COUNCIL_LISTEN} + conf=${conf//COUNCIL_ADDRESS_PLACEHOLDER/$COUNCIL_ADDRESS} + conf=${conf//CUSTODIAN_CHAIN_ID_PLACEHOLDER/$CUSTODIAN_CHAIN_ID} + conf=${conf//OBSERVER_INDEX_PLACEHOLDER/$OBSERVER_INDEX} + conf=${conf//PARENT_CUSTODIAN_PLACEHOLDER/$PARENT_CUSTODIAN} + conf=${conf//SUMMIT_KEYS_DIR_PLACEHOLDER/$SUMMIT_KEYS_DIR} + conf=${conf//SERVICE_USER_PLACEHOLDER/$SERVICE_USER} + conf=${conf//SUPERVISOR_LOG_DIR_PLACEHOLDER/$SUPERVISOR_LOG_DIR} + printf '%s\n' "$conf" +} + +prepare_observer_supervisor_logs() { + local names=(reth summit-observer) + local name + + [[ "$INSTALL_CHECKPOINTER" != true ]] || names+=(checkpointer) + [[ "$INSTALL_CUSTODIAN" != true ]] || names+=(custodian) + + [[ ! -L "$SUPERVISOR_LOG_DIR" ]] \ + || die "Supervisor log directory must not be a symbolic link: $SUPERVISOR_LOG_DIR" + install -d -o root -g root -m 0755 "$SUPERVISOR_LOG_DIR" + for name in "${names[@]}"; do + [[ ! -L "$SUPERVISOR_LOG_DIR/$name.log" \ + && ! -L "$SUPERVISOR_LOG_DIR/$name.err" ]] \ + || die "Supervisor log files must not be symbolic links for service: $name" + touch "$SUPERVISOR_LOG_DIR/$name.log" "$SUPERVISOR_LOG_DIR/$name.err" + chown root:root "$SUPERVISOR_LOG_DIR/$name.log" "$SUPERVISOR_LOG_DIR/$name.err" + chmod 0644 "$SUPERVISOR_LOG_DIR/$name.log" "$SUPERVISOR_LOG_DIR/$name.err" + done +} + +deploy_observer_supervisor_configuration() { + local staging + local conflict + + section "Deploying observer Supervisor configuration" + validate_observer_supervisor_runtime_inputs + validate_observer_supervisor_templates + + BOOTNODE_ENODE="" + if [[ -n "$BOOTNODE_RPC" ]] && ! fetch_bootnode_enode; then + die "Bootnode RPC validation failed after configuration acceptance." + fi + + conflict=$(find_conflicting_supervisor_config || true) + [[ -z "$conflict" ]] \ + || die "Another Supervisor file already defines Seismic services: $conflict" + [[ ! -L "$SUPERVISOR_CONFIG_PATH" ]] \ + || die "Supervisor target must not be a symbolic link: $SUPERVISOR_CONFIG_PATH" + [[ ! -L "$CHECKPOINTER_CONFIG_PATH" ]] \ + || die "Checkpointer configuration target must not be a symbolic link: $CHECKPOINTER_CONFIG_PATH" + + staging=$(mktemp -d) + render_reth_supervisor_config >"$staging/seismic-observer.conf" + render_observer_supervisor_program \ + "summit-observer" "$OBSERVER_STORE_DIR" \ + >>"$staging/seismic-observer.conf" + + if [[ "$INSTALL_CHECKPOINTER" == true ]]; then + render_checkpointer_supervisor_config \ + >>"$staging/seismic-observer.conf" + render_checkpointer_toml >"$staging/summit-checkpointer.toml" + fi + if [[ "$INSTALL_CUSTODIAN" == true ]]; then + render_observer_custodian_supervisor_config \ + >>"$staging/seismic-observer.conf" + fi + + if grep -R -n '_PLACEHOLDER' "$staging" >>"$LOG_FILE" 2>&1; then + rm -rf -- "$staging" + die "Rendered observer Supervisor configuration still contains placeholders; see $LOG_FILE" + fi + + prepare_observer_supervisor_logs + install -d -o root -g root -m 0755 /etc/seismic + if [[ "$INSTALL_CHECKPOINTER" == true ]]; then + install -o root -g root -m 0644 \ + "$staging/summit-checkpointer.toml" "$CHECKPOINTER_CONFIG_PATH" + else + rm -f -- "$CHECKPOINTER_CONFIG_PATH" + fi + install -o root -g root -m 0644 \ + "$staging/seismic-observer.conf" "$SUPERVISOR_CONFIG_PATH" + rm -rf -- "$staging" + + success "Observer Supervisor configuration deployed: $SUPERVISOR_CONFIG_PATH" + info "Supervisor was not started, enabled, reread, or updated." +} diff --git a/install/lib/packages.sh b/install/lib/packages.sh index 2d4bc95..b4ec124 100644 --- a/install/lib/packages.sh +++ b/install/lib/packages.sh @@ -8,18 +8,27 @@ collect_system_packages() { local needs_git=false SYSTEM_PACKAGES=( - supervisor curl jq ca-certificates openssl ) + # Preserve the package and daemon lifecycle on installer reruns. Installing + # Supervisor for the first time is safe because Seismic program files have + # not been deployed yet; an already-installed package is deliberately not + # upgraded or reinstalled here. + if ! dpkg-query -W -f='${Status}' supervisor 2>/dev/null \ + | grep -q '^install ok installed$'; then + SYSTEM_PACKAGES=(supervisor "${SYSTEM_PACKAGES[@]}") + fi + if [[ "$SUMMIT_INSTALL_METHOD" == "source" \ || "$RETH_INSTALL_METHOD" == "source" \ || ("$INSTALL_CHECKPOINTER" == true \ && "$CHECKPOINTER_INSTALL_METHOD" == "source") \ - || "$INSTALL_CUSTODIAN" == true ]]; then + || ("$INSTALL_CUSTODIAN" == true \ + && "$CUSTODIAN_INSTALL_METHOD" == "source") ]]; then needs_source_build=true needs_git=true fi diff --git a/install/lib/supervisor.sh b/install/lib/supervisor.sh index 5439eae..aed0361 100644 --- a/install/lib/supervisor.sh +++ b/install/lib/supervisor.sh @@ -71,7 +71,8 @@ validate_supervisor_runtime_inputs() { validate_supervisor_templates() { local template_root="$TEMPLATES_DIR/supervisor" local required=( - "$template_root/validator.conf" + "$template_root/reth.conf" + "$template_root/summit-validator.conf" "$template_root/deposit-rpc.conf" "$template_root/checkpointer.conf" "$template_root/custodian.conf" @@ -84,13 +85,13 @@ validate_supervisor_templates() { done } -render_validator_supervisor_config() { - local template="$TEMPLATES_DIR/supervisor/validator.conf" +render_reth_supervisor_config() { + local template="$TEMPLATES_DIR/supervisor/reth.conf" local conf local bootnode_argument="" local purpose_key_arguments="--seismic.purpose-keys-source built-in" local rpc_bind="127.0.0.1" - local summit_bind="127.0.0.1" + [[ -z "$BOOTNODE_ENODE" ]] \ || bootnode_argument="--bootnodes $BOOTNODE_ENODE" if [[ "$INSTALL_CUSTODIAN" == true ]]; then @@ -105,6 +106,17 @@ render_validator_supervisor_config() { conf=${conf//RETH_DATA_DIR_PLACEHOLDER/$RETH_DATA_DIR} conf=${conf//PURPOSE_KEYS_ARGUMENTS_PLACEHOLDER/$purpose_key_arguments} conf=${conf//BOOTNODE_ARGUMENT_PLACEHOLDER/$bootnode_argument} + conf=${conf//SERVICE_USER_PLACEHOLDER/$SERVICE_USER} + conf=${conf//SUPERVISOR_LOG_DIR_PLACEHOLDER/$SUPERVISOR_LOG_DIR} + printf '%s\n' "$conf" +} + +render_summit_validator_supervisor_config() { + local template="$TEMPLATES_DIR/supervisor/summit-validator.conf" + local conf + local summit_bind="127.0.0.1" + + conf=$(<"$template") conf=${conf//SUMMIT_BINARY_PLACEHOLDER/$SUMMIT_TARGET_BIN} conf=${conf//GENESIS_PATH_PLACEHOLDER/$GENESIS_PATH} conf=${conf//SUMMIT_KEYS_DIR_PLACEHOLDER/$SUMMIT_KEYS_DIR} @@ -116,6 +128,11 @@ render_validator_supervisor_config() { printf '%s\n' "$conf" } +render_validator_supervisor_config() { + render_reth_supervisor_config + render_summit_validator_supervisor_config +} + render_deposit_rpc_supervisor_config() { local template="$TEMPLATES_DIR/supervisor/deposit-rpc.conf" local conf @@ -153,6 +170,7 @@ render_custodian_supervisor_config() { conf=${conf//COUNCIL_LISTEN_PLACEHOLDER/$COUNCIL_LISTEN} conf=${conf//COUNCIL_ADDRESS_PLACEHOLDER/$COUNCIL_ADDRESS} conf=${conf//CUSTODIAN_CHAIN_ID_PLACEHOLDER/$CUSTODIAN_CHAIN_ID} + conf=${conf//SUMMIT_KEYS_DIR_PLACEHOLDER/$SUMMIT_KEYS_DIR} conf=${conf//SERVICE_USER_PLACEHOLDER/$SERVICE_USER} conf=${conf//SUPERVISOR_LOG_DIR_PLACEHOLDER/$SUPERVISOR_LOG_DIR} printf '%s\n' "$conf" @@ -176,7 +194,7 @@ find_conflicting_supervisor_config() { while IFS= read -r -d '' path; do [[ "$path" == "$SUPERVISOR_CONFIG_PATH" ]] && continue - if grep -Eq '^\[program:(reth|summit|summit-deposit-rpc|checkpointer|custodian)\]' "$path"; then + if grep -Eq '^\[program:(reth|summit|summit-observer|summit-observer-checkpoint|summit-deposit-rpc|checkpointer|custodian)\]' "$path"; then printf '%s\n' "$path" return 0 fi @@ -227,7 +245,8 @@ deploy_supervisor_configuration() { staging=$(mktemp -d) render_deposit_rpc_supervisor_config >"$staging/seismic-validator.conf" - render_validator_supervisor_config >>"$staging/seismic-validator.conf" + render_reth_supervisor_config >>"$staging/seismic-validator.conf" + render_summit_validator_supervisor_config >>"$staging/seismic-validator.conf" if [[ "$INSTALL_CHECKPOINTER" == true ]]; then render_checkpointer_supervisor_config \ >>"$staging/seismic-validator.conf" diff --git a/install/provision-observer-key.sh b/install/provision-observer-key.sh new file mode 100755 index 0000000..2de8e90 --- /dev/null +++ b/install/provision-observer-key.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2029 + +# Securely stream a parent validator's Summit node_key.pem over SSH to an +# already-installed observer. The private key is sent through stdin and is +# never placed in command arguments, environment variables, or log output. + +set -euo pipefail + +TARGET="" +PARENT_NODE_KEY_FILE="" +PARENT_NODE_PUBLIC_KEY="" +OBSERVER_INDEX="" +SUMMIT_KEYS_DIR="/persistence/keys/summit" +SUMMIT_BIN="/usr/local/bin/summit" +SERVICE_USER="ubuntu" +SSH_KEY="" +REPLACE=false +YES=false + +usage() { + cat <<'EOF' +Usage: provision-observer-key.sh \ + --target USER@HOST \ + --parent-node-key-file PATH \ + --parent-node-public-key HEX \ + --observer-index N \ + [--summit-keys-dir /persistence/keys/summit] \ + [--summit-bin /usr/local/bin/summit] \ + [--service-user ubuntu] [--ssh-key PATH] [--replace] [--yes] + +Run this helper on the parent validator host. The observer installer must have +already created the matching observer-assignment marker and consensus key. +EOF +} + +require_option_value() { + local option=$1 + local value=${2:-} + [[ -n "$value" ]] || { + echo "$option requires a value" >&2 + exit 1 + } +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --target) require_option_value "$1" "${2:-}"; TARGET=$2; shift 2 ;; + --parent-node-key-file) require_option_value "$1" "${2:-}"; PARENT_NODE_KEY_FILE=$2; shift 2 ;; + --parent-node-public-key) require_option_value "$1" "${2:-}"; PARENT_NODE_PUBLIC_KEY=$2; shift 2 ;; + --observer-index) require_option_value "$1" "${2:-}"; OBSERVER_INDEX=$2; shift 2 ;; + --summit-keys-dir) require_option_value "$1" "${2:-}"; SUMMIT_KEYS_DIR=$2; shift 2 ;; + --summit-bin) require_option_value "$1" "${2:-}"; SUMMIT_BIN=$2; shift 2 ;; + --service-user) require_option_value "$1" "${2:-}"; SERVICE_USER=$2; shift 2 ;; + --ssh-key) require_option_value "$1" "${2:-}"; SSH_KEY=$2; shift 2 ;; + --replace) REPLACE=true; shift ;; + --yes) YES=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;; + esac +done + +[[ -n "$TARGET" && "$TARGET" != -* && ! "$TARGET" =~ [[:space:]\;\|\&\$\`] ]] \ + || { echo "--target is required and must be a safe SSH destination" >&2; exit 1; } +[[ -f "$PARENT_NODE_KEY_FILE" && ! -L "$PARENT_NODE_KEY_FILE" ]] \ + || { echo "Parent node key must be a regular non-symbolic-link file: $PARENT_NODE_KEY_FILE" >&2; exit 1; } +[[ "$PARENT_NODE_PUBLIC_KEY" =~ ^(0x)?[0-9a-fA-F]{64}$ ]] \ + || { echo "--parent-node-public-key must be a 32-byte hexadecimal value" >&2; exit 1; } +if [[ ! "$OBSERVER_INDEX" =~ ^[0-9]{1,3}$ ]] \ + || ((10#$OBSERVER_INDEX > 255)); then + echo "--observer-index must be an integer from 0 through 255" >&2 + exit 1 +fi +[[ "$SERVICE_USER" =~ ^[a-z_][a-z0-9_-]*\$?$ && "$SERVICE_USER" != "root" ]] \ + || { echo "--service-user is malformed or unsafe" >&2; exit 1; } +for path in "$SUMMIT_KEYS_DIR" "$SUMMIT_BIN"; do + [[ "$path" == /* && ! "$path" =~ [[:space:]\;\|\&\$\`\"\'\\] ]] \ + || { echo "Remote paths must be absolute and contain no shell metacharacters: $path" >&2; exit 1; } +done +if [[ -n "$SSH_KEY" ]]; then + [[ -f "$SSH_KEY" ]] || { echo "SSH key not found: $SSH_KEY" >&2; exit 1; } +fi + +PARENT_NODE_PUBLIC_KEY=${PARENT_NODE_PUBLIC_KEY#0x} +PARENT_NODE_PUBLIC_KEY=${PARENT_NODE_PUBLIC_KEY,,} +OBSERVER_INDEX=$((10#$OBSERVER_INDEX)) +REMOTE_KEY="$SUMMIT_KEYS_DIR/node_key.pem" +CANDIDATE_KEY="$SUMMIT_KEYS_DIR/node_key.pem.candidate" +CONSENSUS_KEY="$SUMMIT_KEYS_DIR/consensus_key.pem" +ASSIGNMENT_FILE="$SUMMIT_KEYS_DIR/observer-assignment" +EXPECTED_ASSIGNMENT="$PARENT_NODE_PUBLIC_KEY:$OBSERVER_INDEX" + +ssh_opts=(-o BatchMode=yes) +if [[ -n "$SSH_KEY" ]]; then + ssh_opts+=(-i "$SSH_KEY") +fi + +identity=$(ssh "${ssh_opts[@]}" -- "$TARGET" \ + 'printf "hostname="; hostname; printf "service_user="; id -un') +printf 'Target identity:\n%s\n' "$identity" +printf 'Destination: %s\n' "$REMOTE_KEY" +printf 'Local key SHA-256: %s\n' "$(sha256sum "$PARENT_NODE_KEY_FILE" | awk '{print $1}')" + +if [[ "$YES" != true ]]; then + read -r -p "Provision this parent key to the displayed observer? [y/N]: " answer + [[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]] || { echo "Aborted."; exit 1; } +fi + +remote_assignment_check=$(printf \ + 'sudo -n test -f %q && sudo -n grep -Fxq %q %q' \ + "$ASSIGNMENT_FILE" "$EXPECTED_ASSIGNMENT" "$ASSIGNMENT_FILE") +ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_assignment_check" \ + || { echo "Observer assignment marker is missing or does not match; run install-observer.sh first." >&2; exit 1; } + +remote_consensus_check=$(printf 'sudo -n test -s %q' "$CONSENSUS_KEY") +ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_consensus_check" \ + || { echo "Observer consensus key is missing; rerun install-observer.sh after installing Summit." >&2; exit 1; } + +remote_user_check=$(printf 'id %q >/dev/null 2>&1' "$SERVICE_USER") +ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_user_check" \ + || { echo "Observer service user does not exist: $SERVICE_USER" >&2; exit 1; } +SERVICE_GROUP=$(ssh "${ssh_opts[@]}" -- "$TARGET" \ + "id -gn $(printf '%q' "$SERVICE_USER")") +[[ "$SERVICE_GROUP" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*\$?$ ]] \ + || { echo "Could not determine a safe observer service group" >&2; exit 1; } + +local_digest=$(sha256sum "$PARENT_NODE_KEY_FILE" | awk '{print $1}') +# shellcheck disable=SC2016 +remote_digest_command=$(printf \ + 'sudo -n sha256sum %q 2>/dev/null | awk '\''{print $1}'\''' "$REMOTE_KEY") +remote_digest=$(ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_digest_command" || true) + +if [[ "$remote_digest" == "$local_digest" ]]; then + echo "Remote parent key already matches; no upload needed." +elif [[ -n "$remote_digest" && "$REPLACE" != true ]]; then + echo "Refusing to replace a different existing key at $REMOTE_KEY without --replace." >&2 + exit 1 +else + remote_script=$(cat <"\$tmp" +chmod 0600 "\$tmp" +chown '$SERVICE_USER:$SERVICE_GROUP' "\$tmp" +mv -f "\$tmp" '$CANDIDATE_KEY' +trap - EXIT +EOF +) + remote_command=$(printf 'sudo -n sh -c %q' "$remote_script") + ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_command" <"$PARENT_NODE_KEY_FILE" + + verify_script=$(cat <&2 + exit 1 + fi + + install_command=$(printf \ + 'sudo -n mv -f -- %q %q' "$CANDIDATE_KEY" "$REMOTE_KEY") + ssh "${ssh_opts[@]}" -- "$TARGET" "$install_command" +fi + +permissions_command=$(printf \ + 'sudo -n chown %q:%q %q %q %q %q && sudo -n chmod 0700 %q && sudo -n chmod 0600 %q %q %q' \ + "$SERVICE_USER" "$SERVICE_GROUP" "$SUMMIT_KEYS_DIR" "$REMOTE_KEY" \ + "$CONSENSUS_KEY" "$ASSIGNMENT_FILE" "$SUMMIT_KEYS_DIR" "$REMOTE_KEY" \ + "$CONSENSUS_KEY" "$ASSIGNMENT_FILE") +ssh "${ssh_opts[@]}" -- "$TARGET" "$permissions_command" + +installed_digest=$(ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_digest_command") +[[ "$installed_digest" == "$local_digest" ]] \ + || { echo "Remote key fingerprint verification failed" >&2; exit 1; } + +show_command=$(printf \ + 'sudo -n -u %q %q keys show --key-store-path %q' \ + "$SERVICE_USER" "$SUMMIT_BIN" "$SUMMIT_KEYS_DIR") +key_output=$(ssh "${ssh_opts[@]}" -- "$TARGET" "$show_command") +installed_public_key=$(printf '%s\n' "$key_output" \ + | awk -F': ' '/Node Public Key \(ed25519\)/ {print tolower($2); exit}') +installed_public_key=${installed_public_key#0x} +installed_consensus_key=$(printf '%s\n' "$key_output" \ + | awk -F': ' '/Consensus Public Key \(BLS\)/ {print tolower($2); exit}') +[[ "$installed_public_key" == "$PARENT_NODE_PUBLIC_KEY" ]] \ + || { echo "Installed node key does not match the expected parent public key." >&2; exit 1; } +[[ -n "$installed_consensus_key" ]] \ + || { echo "Could not read the observer consensus public key." >&2; exit 1; } + +echo "Observer parent key provisioned and verified successfully." +echo "Services remain stopped; start Reth and summit-observer explicitly." diff --git a/install/templates/supervisor/custodian.conf b/install/templates/supervisor/custodian.conf index 1f7309b..6929967 100644 --- a/install/templates/supervisor/custodian.conf +++ b/install/templates/supervisor/custodian.conf @@ -6,6 +6,7 @@ command=CUSTODIAN_BINARY_PLACEHOLDER --council-listen COUNCIL_LISTEN_PLACEHOLDER --council-address COUNCIL_ADDRESS_PLACEHOLDER --chain-id CUSTODIAN_CHAIN_ID_PLACEHOLDER + --summit-key-dir SUMMIT_KEYS_DIR_PLACEHOLDER --allow SERVICE_USER_PLACEHOLDER:tx-io,rng user=SERVICE_USER_PLACEHOLDER autostart=false diff --git a/install/templates/supervisor/observer-custodian.conf b/install/templates/supervisor/observer-custodian.conf new file mode 100644 index 0000000..eccc0c2 --- /dev/null +++ b/install/templates/supervisor/observer-custodian.conf @@ -0,0 +1,25 @@ +[program:custodian] +command=CUSTODIAN_BINARY_PLACEHOLDER + --socket CUSTODIAN_SOCKET_PLACEHOLDER + --root-key-file CUSTODIAN_ROOT_KEY_PLACEHOLDER + --delivery-dir CUSTODIAN_DELIVERY_DIR_PLACEHOLDER + --council-listen COUNCIL_LISTEN_PLACEHOLDER + --council-address COUNCIL_ADDRESS_PLACEHOLDER + --chain-id CUSTODIAN_CHAIN_ID_PLACEHOLDER + --observer OBSERVER_INDEX_PLACEHOLDER + --parent-custodian PARENT_CUSTODIAN_PLACEHOLDER + --summit-key-dir SUMMIT_KEYS_DIR_PLACEHOLDER + --allow SERVICE_USER_PLACEHOLDER:tx-io,rng +user=SERVICE_USER_PLACEHOLDER +autostart=false +autorestart=false +startsecs=3 +priority=100 +stopasgroup=true +killasgroup=true +stdout_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/custodian.log +stderr_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/custodian.err +stdout_logfile_maxbytes=200MB +stderr_logfile_maxbytes=200MB +stdout_logfile_backups=5 +stderr_logfile_backups=5 diff --git a/install/templates/supervisor/observer.conf b/install/templates/supervisor/observer.conf new file mode 100644 index 0000000..2b654a9 --- /dev/null +++ b/install/templates/supervisor/observer.conf @@ -0,0 +1,30 @@ +[program:OBSERVER_PROGRAM_PLACEHOLDER] +command=SUMMIT_BINARY_PLACEHOLDER + run + --observer OBSERVER_INDEX_PLACEHOLDER + --ip OBSERVER_PUBLIC_ADDRESS_PLACEHOLDER + --port OBSERVER_P2P_PORT_PLACEHOLDER + --genesis-path GENESIS_PATH_PLACEHOLDER + --key-store-path SUMMIT_KEYS_DIR_PLACEHOLDER + --store-path OBSERVER_STORE_DIR_PLACEHOLDER + --engine-ipc-path /tmp/reth_engine_api.ipc + --rpc-ip 127.0.0.1 + --rpc-port 3030 + --admin-rpc-port 3031 + --prom-ip 127.0.0.1 + --prom-port 9090 +BOOTSTRAPPERS_ARGUMENT_PLACEHOLDER + --critical-log-dir OBSERVER_CRITICAL_LOG_DIR_PLACEHOLDER +user=SERVICE_USER_PLACEHOLDER +autostart=false +autorestart=false +startsecs=3 +priority=300 +stopasgroup=true +killasgroup=true +stdout_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/OBSERVER_PROGRAM_PLACEHOLDER.log +stderr_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/OBSERVER_PROGRAM_PLACEHOLDER.err +stdout_logfile_maxbytes=200MB +stderr_logfile_maxbytes=200MB +stdout_logfile_backups=5 +stderr_logfile_backups=5 diff --git a/install/templates/supervisor/validator.conf b/install/templates/supervisor/reth.conf similarity index 64% rename from install/templates/supervisor/validator.conf rename to install/templates/supervisor/reth.conf index 74cb726..c172382 100644 --- a/install/templates/supervisor/validator.conf +++ b/install/templates/supervisor/reth.conf @@ -41,25 +41,3 @@ stdout_logfile_maxbytes=200MB stderr_logfile_maxbytes=200MB stdout_logfile_backups=5 stderr_logfile_backups=5 - -[program:summit] -command=SUMMIT_BINARY_PLACEHOLDER - run - --genesis-path GENESIS_PATH_PLACEHOLDER - --key-store-path SUMMIT_KEYS_DIR_PLACEHOLDER - --store-path SUMMIT_DATA_DIR_PLACEHOLDER - --rpc-ip SUMMIT_RPC_BIND_PLACEHOLDER - --prom-ip SUMMIT_PROM_BIND_PLACEHOLDER -user=SERVICE_USER_PLACEHOLDER -autostart=false -autorestart=false -startsecs=3 -priority=300 -stopasgroup=true -killasgroup=true -stdout_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/summit.log -stderr_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/summit.err -stdout_logfile_maxbytes=200MB -stderr_logfile_maxbytes=200MB -stdout_logfile_backups=5 -stderr_logfile_backups=5 diff --git a/install/templates/supervisor/summit-validator.conf b/install/templates/supervisor/summit-validator.conf new file mode 100644 index 0000000..3a4bada --- /dev/null +++ b/install/templates/supervisor/summit-validator.conf @@ -0,0 +1,21 @@ +[program:summit] +command=SUMMIT_BINARY_PLACEHOLDER + run + --genesis-path GENESIS_PATH_PLACEHOLDER + --key-store-path SUMMIT_KEYS_DIR_PLACEHOLDER + --store-path SUMMIT_DATA_DIR_PLACEHOLDER + --rpc-ip SUMMIT_RPC_BIND_PLACEHOLDER + --prom-ip SUMMIT_PROM_BIND_PLACEHOLDER +user=SERVICE_USER_PLACEHOLDER +autostart=false +autorestart=false +startsecs=3 +priority=300 +stopasgroup=true +killasgroup=true +stdout_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/summit.log +stderr_logfile=SUPERVISOR_LOG_DIR_PLACEHOLDER/summit.err +stdout_logfile_maxbytes=200MB +stderr_logfile_maxbytes=200MB +stdout_logfile_backups=5 +stderr_logfile_backups=5 From a9f9377ec79c2c083b2c79423cb0af700d76ec92 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Wed, 26 Aug 2026 22:05:46 +0800 Subject: [PATCH 2/9] chore: add shellcheck, markdownlint, prettier, and shfmt to CI --- .editorconfig | 8 ++ .github/workflows/checks.yml | 62 +++++++++ .markdownlint-cli2.jsonc | 9 ++ .prettierrc.json | 4 + README.md | 33 ++++- install/OBSERVER.md | 69 +++++----- install/README.md | 87 ++++++------ install/lib/configuration.sh | 6 +- install/lib/instructions.sh | 4 +- install/lib/keys.sh | 8 +- install/lib/observer-keys.sh | 12 +- install/lib/observer-supervisor.sh | 4 +- install/lib/openresty.sh | 6 +- install/lib/packages.sh | 18 +-- install/lib/supervisor.sh | 4 +- install/provision-observer-key.sh | 209 ----------------------------- 16 files changed, 221 insertions(+), 322 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/checks.yml create mode 100644 .markdownlint-cli2.jsonc create mode 100644 .prettierrc.json delete mode 100755 install/provision-observer-key.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..08de6ae --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.sh] +shell_variant = bash +indent_style = space +indent_size = 4 +binary_next_line = true +switch_case_indent = true diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..8f1ca87 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,62 @@ +name: Checks + +on: + pull_request: + push: + +permissions: + contents: read + +jobs: + shell: + name: Shell checks + runs-on: ubuntu-24.04 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install ShellCheck + run: | + sudo apt-get update + sudo apt-get install --yes shellcheck + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.24.x + cache: false + + - name: Install shfmt + run: | + go install mvdan.cc/sh/v3/cmd/shfmt@v3.12.0 + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Run shfmt + run: shfmt -d install + + - name: Run ShellCheck + working-directory: install + run: | + shellcheck -x \ + install-validator.sh \ + install-observer.sh + + markdown: + name: Markdown checks + runs-on: ubuntu-24.04 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Run Prettier + run: npx --yes prettier@3.6.2 --check '**/*.md' + + - name: Run Markdownlint + run: npx --yes markdownlint-cli2@0.21.0 './**/*.md' '#./.git/**' diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..9e80f1e --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,9 @@ +{ + "config": { + "MD013": { + "line_length": 80, + "code_blocks": false, + "tables": false + } + } +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..a68a6f4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 80, + "proseWrap": "always" +} diff --git a/README.md b/README.md index 5204831..e167ead 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Seismic Node Operations -Operational tooling and configuration for installing and running Seismic validator and observer nodes. +Operational tooling and configuration for installing and running Seismic +validator and observer nodes. ## Validator installation @@ -10,7 +11,9 @@ The interactive validator installer is located at: install/install-validator.sh ``` -For requirements, installation steps, persistent storage, MDBX notes, validator registration, service startup, OpenResty, and troubleshooting, see the **[Validator Installer and First-Start Guide](install/README.md)**. +For requirements, installation steps, persistent storage, MDBX notes, validator +registration, service startup, OpenResty, and troubleshooting, see the +**[Validator Installer and First-Start Guide](install/README.md)**. ## Observer installation @@ -27,3 +30,29 @@ The Summit internal-testnet genesis file is provided at: ```text internal_testnet_genesis.toml ``` + +## Repository checks + +GitHub Actions checks shell scripts and Markdown through +[`.github/workflows/checks.yml`](.github/workflows/checks.yml). + +Run the same checks locally from the repository root. ShellCheck must be +installed; Go and Node.js with `npx` are also required. + +```bash +go run mvdan.cc/sh/v3/cmd/shfmt@v3.12.0 -d install + +( + cd install + shellcheck -x \ + install-validator.sh \ + install-observer.sh +) + +npx --yes prettier@3.6.2 --check '**/*.md' +npx --yes markdownlint-cli2@0.21.0 './**/*.md' '#./.git/**' +``` + +Shell formatting is defined in [`.editorconfig`](.editorconfig). Markdown +formatting and lint rules are defined in [`.prettierrc.json`](.prettierrc.json) +and [`.markdownlint-cli2.jsonc`](.markdownlint-cli2.jsonc). diff --git a/install/OBSERVER.md b/install/OBSERVER.md index cf01b83..b743f78 100644 --- a/install/OBSERVER.md +++ b/install/OBSERVER.md @@ -22,8 +22,7 @@ The installer is designed to avoid replacing persistent observer state: - Persistent directories must be absolute, non-root, and non-overlapping. - Existing Reth P2P keys are validated and preserved. - Existing observer consensus and parent node keys are preserved only when the - assignment marker matches the configured parent public key and observer - index. + assignment marker matches the configured parent public key and observer index. - Keys without an assignment marker, or a marker for another assignment, cause the installer to stop rather than reuse or replace them. - Existing observer Custodian root keys are preserved for verification against @@ -71,13 +70,13 @@ The installation log is written to: The installer does not configure cloud firewall, security-group, or host firewall rules. Configure the required access before starting the observer. -| Port | Protocol | Purpose | Required exposure | -| --- | --- | --- | --- | -| `30303` | TCP and UDP | seismic-reth P2P and discovery | Public | -| `18551` or configured port | TCP and UDP | Summit observer P2P | Public | -| `80` | TCP | HTTP redirect and ACME challenge | Public when OpenResty is enabled | -| `443` | TCP | OpenResty HTTPS endpoint | Public when OpenResty is enabled | -| `7876` or configured port | TCP | Observer Custodian council listener | Externally reachable when Custodian is enabled; restrict sources to intended participants | +| Port | Protocol | Purpose | Required exposure | +| -------------------------- | ----------- | ----------------------------------- | ----------------------------------------------------------------------------------------- | +| `30303` | TCP and UDP | seismic-reth P2P and discovery | Public | +| `18551` or configured port | TCP and UDP | Summit observer P2P | Public | +| `80` | TCP | HTTP redirect and ACME challenge | Public when OpenResty is enabled | +| `443` | TCP | OpenResty HTTPS endpoint | Public when OpenResty is enabled | +| `7876` or configured port | TCP | Observer Custodian council listener | Externally reachable when Custodian is enabled; restrict sources to intended participants | When observer Custodian is enabled, the observer also needs outbound TCP access to the configured parent validator Custodian council endpoint. The parent @@ -190,13 +189,13 @@ Reth bootnode RPC used to discover an execution-layer enode. The default persistent paths are: -| State | Default path | -| --- | --- | -| Reth data | `/persistence/reth` | -| Summit observer data | `/persistence/summit` | -| Observer and Reth keys | `/persistence/keys` | +| State | Default path | +| --------------------------------- | -------------------------- | +| Reth data | `/persistence/reth` | +| Summit observer data | `/persistence/summit` | +| Observer and Reth keys | `/persistence/keys` | | Checkpointer output, when enabled | `/persistence/checkpoints` | -| Custodian data, when enabled | `/persistence/custodian` | +| Custodian data, when enabled | `/persistence/custodian` | Important files derived from those paths include: @@ -237,9 +236,9 @@ required destination is: /persistence/keys/summit/node_key.pem ``` -The file must be owned by the configured service user and have mode `0600`. -The Summit key directory must have mode `0700`. The private key must correspond -to the parent public key entered during installation. +The file must be owned by the configured service user and have mode `0600`. The +Summit key directory must have mode `0700`. The private key must correspond to +the parent public key entered during installation. Summit derives the observer's secondary P2P identity from the parent node key and observer index. The observer's separate consensus key does not make it a @@ -254,8 +253,8 @@ seismic-reth stores its MDBX execution database under: ``` Treat this as persistent execution-layer state. Do not delete it during an -installer rerun, and do not manually copy or modify the live database while -Reth is running. +installer rerun, and do not manually copy or modify the live database while Reth +is running. When summit-checkpointer is enabled, it uses `mdbx_copy` to produce a consistent Reth database copy. `mdbx_copy` must be built from the same vendored libmdbx @@ -462,16 +461,16 @@ operator-managed tunnel. The configured routes are: -| Public path | Local upstream | Notes | -| --- | --- | --- | -| `/` | `127.0.0.1:3000` | Grafana | -| `/staking` | `/var/www/html/staking` | Static staking UI | -| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | -| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | -| `/summit` | `127.0.0.1:3030` | Summit RPC | -| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | -| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | -| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | +| Public path | Local upstream | Notes | +| -------------- | ----------------------- | ------------------------------------------- | +| `/` | `127.0.0.1:3000` | Grafana | +| `/staking` | `/var/www/html/staking` | Static staking UI | +| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | +| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | +| `/summit` | `127.0.0.1:3030` | Summit RPC | +| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | +| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | +| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | The Summit admin RPC on loopback port `3031` is deliberately not proxied. @@ -538,8 +537,8 @@ On a normal rerun, the installer: - Preserves Summit keys when the assignment marker matches. - Refuses to reuse keys for another parent or observer index. - Preserves an existing observer Custodian root key. -- Replaces the installer-managed bootstrappers file from the accepted source, - or removes it when no source is configured. +- Replaces the installer-managed bootstrappers file from the accepted source, or + removes it when no source is configured. - Replaces generated OpenResty and Supervisor configuration. - Leaves services stopped and does not reload Supervisor or OpenResty. @@ -607,9 +606,9 @@ first troubleshooting step. ### Summit cannot start -Check the Summit executable, genesis readability, assignment marker, both -Summit key files, Engine API IPC availability, public P2P address, and Summit -error log. +Check the Summit executable, genesis readability, assignment marker, both Summit +key files, Engine API IPC availability, public P2P address, and Summit error +log. ### Observer cannot find Summit peers diff --git a/install/README.md b/install/README.md index 3dd9eaa..f211904 100644 --- a/install/README.md +++ b/install/README.md @@ -49,14 +49,13 @@ The installer does not configure cloud firewall, security-group, or host firewall rules. Configure the required inbound access before starting the validator. -| Port | Protocol | Purpose | Required exposure | -| --- | --- | --- | --- | -| `30303` | TCP and UDP | seismic-reth P2P and discovery | Public | -| `18551` | TCP and UDP | Summit consensus P2P | Public | -| `80` | TCP | HTTP redirect and ACME challenge | Public when OpenResty is enabled | -| `443` | TCP | OpenResty HTTPS endpoint | Public when OpenResty is enabled | -| `7876` | TCP | Custodian council communication | Externally reachable when Custodian is enabled; restrict sources to intended council participants | - +| Port | Protocol | Purpose | Required exposure | +| ------- | ----------- | -------------------------------- | ------------------------------------------------------------------------------------------------- | +| `30303` | TCP and UDP | seismic-reth P2P and discovery | Public | +| `18551` | TCP and UDP | Summit consensus P2P | Public | +| `80` | TCP | HTTP redirect and ACME challenge | Public when OpenResty is enabled | +| `443` | TCP | OpenResty HTTPS endpoint | Public when OpenResty is enabled | +| `7876` | TCP | Custodian council communication | Externally reachable when Custodian is enabled; restrict sources to intended council participants | The installation log is written to: @@ -105,15 +104,14 @@ When Centralized Custodian is enabled, the installer always uses its publicly known shared default root key and does not prompt for a custom key. This makes epoch-0 purpose keys public. -The Custodian council listener defaults to `0.0.0.0:7876`. TCP port `7876` -must be reachable from outside the node for key rotation. The generated -validator Custodian program also receives the Summit key directory so it can -authenticate and serve configured observer Custodians. -Configure the cloud firewall, security group, and host firewall as needed, and -restrict allowed source addresses to intended council participants and observer -hosts rather than exposing the port more broadly than necessary. Observer root -keys and plaintext epoch-key material transit the parent-Custodian connection; -use a private network or TLS tunnel. +The Custodian council listener defaults to `0.0.0.0:7876`. TCP port `7876` must +be reachable from outside the node for key rotation. The generated validator +Custodian program also receives the Summit key directory so it can authenticate +and serve configured observer Custodians. Configure the cloud firewall, security +group, and host firewall as needed, and restrict allowed source addresses to +intended council participants and observer hosts rather than exposing the port +more broadly than necessary. Observer root keys and plaintext epoch-key material +transit the parent-Custodian connection; use a private network or TLS tunnel. For Summit, seismic-reth, summit-checkpointer, and Centralized Custodian, supported installation modes are: @@ -137,13 +135,13 @@ corresponding services are started. The default persistent paths are: -| State | Default path | -| --- | --- | -| Reth data | `/persistence/reth` | -| Summit data | `/persistence/summit` | -| Validator keys | `/persistence/keys` | +| State | Default path | +| --------------------------------- | -------------------------- | +| Reth data | `/persistence/reth` | +| Summit data | `/persistence/summit` | +| Validator keys | `/persistence/keys` | | Checkpointer output, when enabled | `/persistence/checkpoints` | -| Custodian data, when enabled | `/persistence/custodian` | +| Custodian data, when enabled | `/persistence/custodian` | Important files derived from those paths include: @@ -165,8 +163,8 @@ seismic-reth stores its MDBX execution database under: ``` Treat this as persistent execution-layer state. Do not delete it during an -installer rerun, and do not manually copy or modify the live database while -Reth is running. +installer rerun, and do not manually copy or modify the live database while Reth +is running. When summit-checkpointer is enabled, it uses `mdbx_copy` to produce a consistent Reth database copy. `mdbx_copy` must be built from the same vendored libmdbx @@ -272,8 +270,7 @@ exits to the encoded address. ### 4. Save the signed deposit response as JSON -Run the following on the validator server while `summit-deposit-rpc` is -running: +Run the following on the validator server while `summit-deposit-rpc` is running: ```bash curl --fail-with-body --silent --show-error \ @@ -313,16 +310,16 @@ Do not leave the signing endpoint running after obtaining the file. ### 6. Send the signature file to Seismic operations -Send the `deposit-signature.json` to Seismic operations through a secure transfer channel. +Send the `deposit-signature.json` to Seismic operations through a secure +transfer channel. Do **not** send validator private-key files, a wallet private key, seed phrases, or JWT secrets. Do not publish the signature file in a public issue or source repository. -Seismic operations will submit the staking transaction on your behalf. Wait -for the transaction hash and confirmation that its receipt has status -`0x1` before starting the full validator. - +Seismic operations will submit the staking transaction on your behalf. Wait for +the transaction hash and confirmation that its receipt has status `0x1` before +starting the full validator. ### 7. Start the validator after staking confirmation @@ -357,19 +354,19 @@ operator-managed tunnel; they are not exposed directly on public interfaces. The configured routes are: -| Public path | Local upstream | Notes | -| --- | --- | --- | -| `/` | `127.0.0.1:3000` | Grafana | -| `/staking` | `/var/www/html/staking` | Static staking UI | -| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | -| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | -| `/summit` | `127.0.0.1:3030` | Summit RPC | -| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | -| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | -| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | - -The localhost-only `summit-deposit-rpc` service on port 3031 is deliberately -not proxied. +| Public path | Local upstream | Notes | +| -------------- | ----------------------- | ------------------------------------------- | +| `/` | `127.0.0.1:3000` | Grafana | +| `/staking` | `/var/www/html/staking` | Static staking UI | +| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | +| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | +| `/summit` | `127.0.0.1:3030` | Summit RPC | +| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | +| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | +| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | + +The localhost-only `summit-deposit-rpc` service on port 3031 is deliberately not +proxied. ### Start or reload OpenResty diff --git a/install/lib/configuration.sh b/install/lib/configuration.sh index d2c4a12..64984fe 100644 --- a/install/lib/configuration.sh +++ b/install/lib/configuration.sh @@ -124,9 +124,9 @@ directory_conflicts_with_existing() { configured_path=${!configured_variable} [[ -n "$configured_path" ]] || continue - if [[ "$selected" == "$configured_path" \ - || "$selected" == "$configured_path/"* \ - || "$configured_path" == "$selected/"* ]]; then + if [[ "$selected" == "$configured_path" || + "$selected" == "$configured_path/"* || + "$configured_path" == "$selected/"* ]]; then error "$description and ${configured_descriptions[i]} must not be identical or nested." return 0 fi diff --git a/install/lib/instructions.sh b/install/lib/instructions.sh index b7e8dd5..4681e25 100644 --- a/install/lib/instructions.sh +++ b/install/lib/instructions.sh @@ -22,8 +22,8 @@ print_missing_service_prerequisites() { warn "seismic-reth cannot start until its P2P key exists at $RETH_P2P_KEY_PATH." missing=true fi - if [[ ! -f "$SUMMIT_KEYS_DIR/node_key.pem" \ - || ! -f "$SUMMIT_KEYS_DIR/consensus_key.pem" ]]; then + if [[ ! -f "$SUMMIT_KEYS_DIR/node_key.pem" || + ! -f "$SUMMIT_KEYS_DIR/consensus_key.pem" ]]; then warn "Summit cannot start until node_key.pem and consensus_key.pem exist under $SUMMIT_KEYS_DIR." missing=true fi diff --git a/install/lib/keys.sh b/install/lib/keys.sh index 4c71fd1..d92d9e0 100644 --- a/install/lib/keys.sh +++ b/install/lib/keys.sh @@ -97,10 +97,10 @@ setup_summit_validator_keys() { die "Summit validator key generation failed; see $LOG_FILE" fi - if [[ ! -s "$staging/node_key.pem" \ - || ! -s "$staging/consensus_key.pem" \ - || -L "$staging/node_key.pem" \ - || -L "$staging/consensus_key.pem" ]]; then + if [[ ! -s "$staging/node_key.pem" || + ! -s "$staging/consensus_key.pem" || + -L "$staging/node_key.pem" || + -L "$staging/consensus_key.pem" ]]; then rm -rf -- "$staging" die "Summit reported success but did not produce a complete validator key set." fi diff --git a/install/lib/observer-keys.sh b/install/lib/observer-keys.sh index 2b44ff2..a60bab6 100644 --- a/install/lib/observer-keys.sh +++ b/install/lib/observer-keys.sh @@ -55,8 +55,8 @@ configure_observer_bootstrapper_file() { return fi - [[ ! -L "$OBSERVER_BOOTSTRAPPERS_SOURCE" \ - && -s "$OBSERVER_BOOTSTRAPPERS_SOURCE" ]] \ + [[ ! -L "$OBSERVER_BOOTSTRAPPERS_SOURCE" && + -s "$OBSERVER_BOOTSTRAPPERS_SOURCE" ]] \ || die "Summit bootstrappers source became invalid: $OBSERVER_BOOTSTRAPPERS_SOURCE" run_as_service_user test -r "$OBSERVER_BOOTSTRAPPERS_SOURCE" \ || die "Service user cannot read the Summit bootstrappers source: $OBSERVER_BOOTSTRAPPERS_SOURCE" @@ -92,8 +92,8 @@ setup_summit_observer_keys() { || die "Observer keys are assigned to $existing_assignment, not $expected_assignment; use separate persistent paths for a different assignment." chown "$SERVICE_USER:$SERVICE_GROUP" "$OBSERVER_ASSIGNMENT_FILE" chmod 0600 "$OBSERVER_ASSIGNMENT_FILE" - elif [[ -e "$node_key" || -L "$node_key" \ - || -e "$consensus_key" || -L "$consensus_key" ]]; then + elif [[ -e "$node_key" || -L "$node_key" || + -e "$consensus_key" || -L "$consensus_key" ]]; then die "Summit keys exist without an observer assignment marker; refusing to reuse or replace them." else write_observer_assignment @@ -122,8 +122,8 @@ setup_summit_observer_keys() { rm -rf -- "$staging" die "Observer Summit consensus-key generation failed; see $LOG_FILE" fi - if [[ ! -s "$staging/consensus_key.pem" \ - || -L "$staging/consensus_key.pem" ]]; then + if [[ ! -s "$staging/consensus_key.pem" || + -L "$staging/consensus_key.pem" ]]; then rm -rf -- "$staging" die "Summit did not produce an observer consensus key." fi diff --git a/install/lib/observer-supervisor.sh b/install/lib/observer-supervisor.sh index b6c66d0..b044582 100644 --- a/install/lib/observer-supervisor.sh +++ b/install/lib/observer-supervisor.sh @@ -88,8 +88,8 @@ prepare_observer_supervisor_logs() { || die "Supervisor log directory must not be a symbolic link: $SUPERVISOR_LOG_DIR" install -d -o root -g root -m 0755 "$SUPERVISOR_LOG_DIR" for name in "${names[@]}"; do - [[ ! -L "$SUPERVISOR_LOG_DIR/$name.log" \ - && ! -L "$SUPERVISOR_LOG_DIR/$name.err" ]] \ + [[ ! -L "$SUPERVISOR_LOG_DIR/$name.log" && + ! -L "$SUPERVISOR_LOG_DIR/$name.err" ]] \ || die "Supervisor log files must not be symbolic links for service: $name" touch "$SUPERVISOR_LOG_DIR/$name.log" "$SUPERVISOR_LOG_DIR/$name.err" chown root:root "$SUPERVISOR_LOG_DIR/$name.log" "$SUPERVISOR_LOG_DIR/$name.err" diff --git a/install/lib/openresty.sh b/install/lib/openresty.sh index d7c7151..9d239c6 100644 --- a/install/lib/openresty.sh +++ b/install/lib/openresty.sh @@ -70,7 +70,7 @@ install_openresty() { printf '%s\n' \ "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] https://openresty.org/package/ubuntu $(lsb_release -sc) main" \ - > /etc/apt/sources.list.d/openresty.list + >/etc/apt/sources.list.d/openresty.list if ! apt-get update >>"$LOG_FILE" 2>&1; then die "apt-get update failed after adding the OpenResty repository; see $LOG_FILE" @@ -122,8 +122,8 @@ install_openresty() { "$LUA_RESTY_JWT_RELEASE" \ "$LUA_RESTY_JWT_REVISION" - if [[ ! -f /etc/ssl/resty-auto-ssl-fallback.crt \ - || ! -f /etc/ssl/resty-auto-ssl-fallback.key ]]; then + if [[ ! -f /etc/ssl/resty-auto-ssl-fallback.crt || + ! -f /etc/ssl/resty-auto-ssl-fallback.key ]]; then info "Generating the OpenResty fallback TLS certificate..." if ! openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \ -subj '/CN=sni-support-required-for-valid-ssl' \ diff --git a/install/lib/packages.sh b/install/lib/packages.sh index b4ec124..2936776 100644 --- a/install/lib/packages.sh +++ b/install/lib/packages.sh @@ -23,19 +23,19 @@ collect_system_packages() { SYSTEM_PACKAGES=(supervisor "${SYSTEM_PACKAGES[@]}") fi - if [[ "$SUMMIT_INSTALL_METHOD" == "source" \ - || "$RETH_INSTALL_METHOD" == "source" \ - || ("$INSTALL_CHECKPOINTER" == true \ - && "$CHECKPOINTER_INSTALL_METHOD" == "source") \ - || ("$INSTALL_CUSTODIAN" == true \ - && "$CUSTODIAN_INSTALL_METHOD" == "source") ]]; then + if [[ "$SUMMIT_INSTALL_METHOD" == "source" || + "$RETH_INSTALL_METHOD" == "source" || + ("$INSTALL_CHECKPOINTER" == true && + "$CHECKPOINTER_INSTALL_METHOD" == "source") || + ("$INSTALL_CUSTODIAN" == true && + "$CUSTODIAN_INSTALL_METHOD" == "source") ]]; then needs_source_build=true needs_git=true fi - if [[ "$INSTALL_CHECKPOINTER" == true \ - && ("$RETH_INSTALL_METHOD" == "source" \ - || -n "$RETH_SOURCE_DIR_FOR_MDBX") ]]; then + if [[ "$INSTALL_CHECKPOINTER" == true && + ("$RETH_INSTALL_METHOD" == "source" || + -n "$RETH_SOURCE_DIR_FOR_MDBX") ]]; then needs_mdbx_build=true fi diff --git a/install/lib/supervisor.sh b/install/lib/supervisor.sh index aed0361..a8570fc 100644 --- a/install/lib/supervisor.sh +++ b/install/lib/supervisor.sh @@ -213,8 +213,8 @@ prepare_supervisor_logs() { || die "Supervisor log directory must not be a symbolic link: $SUPERVISOR_LOG_DIR" install -d -o root -g root -m 0755 "$SUPERVISOR_LOG_DIR" for name in "${names[@]}"; do - [[ ! -L "$SUPERVISOR_LOG_DIR/$name.log" \ - && ! -L "$SUPERVISOR_LOG_DIR/$name.err" ]] \ + [[ ! -L "$SUPERVISOR_LOG_DIR/$name.log" && + ! -L "$SUPERVISOR_LOG_DIR/$name.err" ]] \ || die "Supervisor log files must not be symbolic links for service: $name" touch "$SUPERVISOR_LOG_DIR/$name.log" "$SUPERVISOR_LOG_DIR/$name.err" chown root:root "$SUPERVISOR_LOG_DIR/$name.log" "$SUPERVISOR_LOG_DIR/$name.err" diff --git a/install/provision-observer-key.sh b/install/provision-observer-key.sh deleted file mode 100755 index 2de8e90..0000000 --- a/install/provision-observer-key.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2029 - -# Securely stream a parent validator's Summit node_key.pem over SSH to an -# already-installed observer. The private key is sent through stdin and is -# never placed in command arguments, environment variables, or log output. - -set -euo pipefail - -TARGET="" -PARENT_NODE_KEY_FILE="" -PARENT_NODE_PUBLIC_KEY="" -OBSERVER_INDEX="" -SUMMIT_KEYS_DIR="/persistence/keys/summit" -SUMMIT_BIN="/usr/local/bin/summit" -SERVICE_USER="ubuntu" -SSH_KEY="" -REPLACE=false -YES=false - -usage() { - cat <<'EOF' -Usage: provision-observer-key.sh \ - --target USER@HOST \ - --parent-node-key-file PATH \ - --parent-node-public-key HEX \ - --observer-index N \ - [--summit-keys-dir /persistence/keys/summit] \ - [--summit-bin /usr/local/bin/summit] \ - [--service-user ubuntu] [--ssh-key PATH] [--replace] [--yes] - -Run this helper on the parent validator host. The observer installer must have -already created the matching observer-assignment marker and consensus key. -EOF -} - -require_option_value() { - local option=$1 - local value=${2:-} - [[ -n "$value" ]] || { - echo "$option requires a value" >&2 - exit 1 - } -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --target) require_option_value "$1" "${2:-}"; TARGET=$2; shift 2 ;; - --parent-node-key-file) require_option_value "$1" "${2:-}"; PARENT_NODE_KEY_FILE=$2; shift 2 ;; - --parent-node-public-key) require_option_value "$1" "${2:-}"; PARENT_NODE_PUBLIC_KEY=$2; shift 2 ;; - --observer-index) require_option_value "$1" "${2:-}"; OBSERVER_INDEX=$2; shift 2 ;; - --summit-keys-dir) require_option_value "$1" "${2:-}"; SUMMIT_KEYS_DIR=$2; shift 2 ;; - --summit-bin) require_option_value "$1" "${2:-}"; SUMMIT_BIN=$2; shift 2 ;; - --service-user) require_option_value "$1" "${2:-}"; SERVICE_USER=$2; shift 2 ;; - --ssh-key) require_option_value "$1" "${2:-}"; SSH_KEY=$2; shift 2 ;; - --replace) REPLACE=true; shift ;; - --yes) YES=true; shift ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;; - esac -done - -[[ -n "$TARGET" && "$TARGET" != -* && ! "$TARGET" =~ [[:space:]\;\|\&\$\`] ]] \ - || { echo "--target is required and must be a safe SSH destination" >&2; exit 1; } -[[ -f "$PARENT_NODE_KEY_FILE" && ! -L "$PARENT_NODE_KEY_FILE" ]] \ - || { echo "Parent node key must be a regular non-symbolic-link file: $PARENT_NODE_KEY_FILE" >&2; exit 1; } -[[ "$PARENT_NODE_PUBLIC_KEY" =~ ^(0x)?[0-9a-fA-F]{64}$ ]] \ - || { echo "--parent-node-public-key must be a 32-byte hexadecimal value" >&2; exit 1; } -if [[ ! "$OBSERVER_INDEX" =~ ^[0-9]{1,3}$ ]] \ - || ((10#$OBSERVER_INDEX > 255)); then - echo "--observer-index must be an integer from 0 through 255" >&2 - exit 1 -fi -[[ "$SERVICE_USER" =~ ^[a-z_][a-z0-9_-]*\$?$ && "$SERVICE_USER" != "root" ]] \ - || { echo "--service-user is malformed or unsafe" >&2; exit 1; } -for path in "$SUMMIT_KEYS_DIR" "$SUMMIT_BIN"; do - [[ "$path" == /* && ! "$path" =~ [[:space:]\;\|\&\$\`\"\'\\] ]] \ - || { echo "Remote paths must be absolute and contain no shell metacharacters: $path" >&2; exit 1; } -done -if [[ -n "$SSH_KEY" ]]; then - [[ -f "$SSH_KEY" ]] || { echo "SSH key not found: $SSH_KEY" >&2; exit 1; } -fi - -PARENT_NODE_PUBLIC_KEY=${PARENT_NODE_PUBLIC_KEY#0x} -PARENT_NODE_PUBLIC_KEY=${PARENT_NODE_PUBLIC_KEY,,} -OBSERVER_INDEX=$((10#$OBSERVER_INDEX)) -REMOTE_KEY="$SUMMIT_KEYS_DIR/node_key.pem" -CANDIDATE_KEY="$SUMMIT_KEYS_DIR/node_key.pem.candidate" -CONSENSUS_KEY="$SUMMIT_KEYS_DIR/consensus_key.pem" -ASSIGNMENT_FILE="$SUMMIT_KEYS_DIR/observer-assignment" -EXPECTED_ASSIGNMENT="$PARENT_NODE_PUBLIC_KEY:$OBSERVER_INDEX" - -ssh_opts=(-o BatchMode=yes) -if [[ -n "$SSH_KEY" ]]; then - ssh_opts+=(-i "$SSH_KEY") -fi - -identity=$(ssh "${ssh_opts[@]}" -- "$TARGET" \ - 'printf "hostname="; hostname; printf "service_user="; id -un') -printf 'Target identity:\n%s\n' "$identity" -printf 'Destination: %s\n' "$REMOTE_KEY" -printf 'Local key SHA-256: %s\n' "$(sha256sum "$PARENT_NODE_KEY_FILE" | awk '{print $1}')" - -if [[ "$YES" != true ]]; then - read -r -p "Provision this parent key to the displayed observer? [y/N]: " answer - [[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]] || { echo "Aborted."; exit 1; } -fi - -remote_assignment_check=$(printf \ - 'sudo -n test -f %q && sudo -n grep -Fxq %q %q' \ - "$ASSIGNMENT_FILE" "$EXPECTED_ASSIGNMENT" "$ASSIGNMENT_FILE") -ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_assignment_check" \ - || { echo "Observer assignment marker is missing or does not match; run install-observer.sh first." >&2; exit 1; } - -remote_consensus_check=$(printf 'sudo -n test -s %q' "$CONSENSUS_KEY") -ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_consensus_check" \ - || { echo "Observer consensus key is missing; rerun install-observer.sh after installing Summit." >&2; exit 1; } - -remote_user_check=$(printf 'id %q >/dev/null 2>&1' "$SERVICE_USER") -ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_user_check" \ - || { echo "Observer service user does not exist: $SERVICE_USER" >&2; exit 1; } -SERVICE_GROUP=$(ssh "${ssh_opts[@]}" -- "$TARGET" \ - "id -gn $(printf '%q' "$SERVICE_USER")") -[[ "$SERVICE_GROUP" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*\$?$ ]] \ - || { echo "Could not determine a safe observer service group" >&2; exit 1; } - -local_digest=$(sha256sum "$PARENT_NODE_KEY_FILE" | awk '{print $1}') -# shellcheck disable=SC2016 -remote_digest_command=$(printf \ - 'sudo -n sha256sum %q 2>/dev/null | awk '\''{print $1}'\''' "$REMOTE_KEY") -remote_digest=$(ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_digest_command" || true) - -if [[ "$remote_digest" == "$local_digest" ]]; then - echo "Remote parent key already matches; no upload needed." -elif [[ -n "$remote_digest" && "$REPLACE" != true ]]; then - echo "Refusing to replace a different existing key at $REMOTE_KEY without --replace." >&2 - exit 1 -else - remote_script=$(cat <"\$tmp" -chmod 0600 "\$tmp" -chown '$SERVICE_USER:$SERVICE_GROUP' "\$tmp" -mv -f "\$tmp" '$CANDIDATE_KEY' -trap - EXIT -EOF -) - remote_command=$(printf 'sudo -n sh -c %q' "$remote_script") - ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_command" <"$PARENT_NODE_KEY_FILE" - - verify_script=$(cat <&2 - exit 1 - fi - - install_command=$(printf \ - 'sudo -n mv -f -- %q %q' "$CANDIDATE_KEY" "$REMOTE_KEY") - ssh "${ssh_opts[@]}" -- "$TARGET" "$install_command" -fi - -permissions_command=$(printf \ - 'sudo -n chown %q:%q %q %q %q %q && sudo -n chmod 0700 %q && sudo -n chmod 0600 %q %q %q' \ - "$SERVICE_USER" "$SERVICE_GROUP" "$SUMMIT_KEYS_DIR" "$REMOTE_KEY" \ - "$CONSENSUS_KEY" "$ASSIGNMENT_FILE" "$SUMMIT_KEYS_DIR" "$REMOTE_KEY" \ - "$CONSENSUS_KEY" "$ASSIGNMENT_FILE") -ssh "${ssh_opts[@]}" -- "$TARGET" "$permissions_command" - -installed_digest=$(ssh "${ssh_opts[@]}" -- "$TARGET" "$remote_digest_command") -[[ "$installed_digest" == "$local_digest" ]] \ - || { echo "Remote key fingerprint verification failed" >&2; exit 1; } - -show_command=$(printf \ - 'sudo -n -u %q %q keys show --key-store-path %q' \ - "$SERVICE_USER" "$SUMMIT_BIN" "$SUMMIT_KEYS_DIR") -key_output=$(ssh "${ssh_opts[@]}" -- "$TARGET" "$show_command") -installed_public_key=$(printf '%s\n' "$key_output" \ - | awk -F': ' '/Node Public Key \(ed25519\)/ {print tolower($2); exit}') -installed_public_key=${installed_public_key#0x} -installed_consensus_key=$(printf '%s\n' "$key_output" \ - | awk -F': ' '/Consensus Public Key \(BLS\)/ {print tolower($2); exit}') -[[ "$installed_public_key" == "$PARENT_NODE_PUBLIC_KEY" ]] \ - || { echo "Installed node key does not match the expected parent public key." >&2; exit 1; } -[[ -n "$installed_consensus_key" ]] \ - || { echo "Could not read the observer consensus public key." >&2; exit 1; } - -echo "Observer parent key provisioned and verified successfully." -echo "Services remain stopped; start Reth and summit-observer explicitly." From 6985bfd4070a36315d0463389588102d9874a7a8 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Fri, 28 Aug 2026 23:52:23 +0800 Subject: [PATCH 3/9] feat: secure and expose checkpointer RPC --- install/OBSERVER.md | 54 ++++++++++++------- install/README.md | 52 +++++++++++------- install/lib/checkpointer.sh | 27 ++++++++++ install/lib/configuration.sh | 2 +- install/lib/instructions.sh | 3 ++ install/lib/observer-instructions.sh | 3 ++ install/templates/openresty/nginx.conf | 17 ++++++ .../templates/supervisor/checkpointer.conf | 2 +- install/templates/supervisor/reth.conf | 1 - 9 files changed, 118 insertions(+), 43 deletions(-) diff --git a/install/OBSERVER.md b/install/OBSERVER.md index b743f78..ec0fdea 100644 --- a/install/OBSERVER.md +++ b/install/OBSERVER.md @@ -89,9 +89,14 @@ send it over an untrusted plaintext network. The following application ports must remain loopback-only: ```text -3000 3030 3031 8545 8546 8552 8999 9001 9090 +3000 3030 3031 8545 8546 8552 8999 9001 9090 42069 ``` +When summit-checkpointer is enabled, its direct listener remains on +`127.0.0.1:42069`. When OpenResty is enabled, remote clients use the +rate-limited and JWT-protected `/checkpointer` HTTPS route instead of direct +access to that port. + ### Supplied internal-testnet genesis The repository includes the Summit genesis file for the internal testnet: @@ -147,10 +152,14 @@ The current source-build defaults are: ```text Summit: m/metrics seismic-reth: feat/purpose-key-rotation-reth -Checkpointer: m/dynamic-epochs-and-ckpt-chain +Checkpointer: main Custodian: d/centralized-custodian ``` +A prebuilt or already-present deferred summit-checkpointer must support +`--bind-address`; the generated Supervisor program uses it to keep the +checkpointer RPC and snapshot server on `127.0.0.1:42069`. + A prebuilt or already-present deferred observer Custodian is checked for these required options: @@ -451,28 +460,32 @@ started manually again after a server or Supervisor restart. ## OpenResty public endpoint When enabled, OpenResty terminates HTTPS, obtains certificates through -`lua-resty-auto-ssl`, applies per-client rate limiting, and proxies local Reth -and Summit endpoints. +`lua-resty-auto-ssl`, applies per-client rate limiting, and proxies local Reth, +Summit, and summit-checkpointer endpoints. -Reth HTTP and WebSocket RPC, Reth Ops RPC, Summit RPC, and metrics listeners -remain bound to loopback whether or not OpenResty is enabled. When OpenResty is -disabled, these endpoints are available only from the node itself or through an -operator-managed tunnel. +Reth HTTP and WebSocket RPC, Reth Ops RPC, Summit RPC, metrics listeners, and +the summit-checkpointer RPC remain bound to loopback whether or not OpenResty is +enabled. When OpenResty is disabled, these endpoints are available only from the +node itself or through an operator-managed tunnel. The configured routes are: -| Public path | Local upstream | Notes | -| -------------- | ----------------------- | ------------------------------------------- | -| `/` | `127.0.0.1:3000` | Grafana | -| `/staking` | `/var/www/html/staking` | Static staking UI | -| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | -| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | -| `/summit` | `127.0.0.1:3030` | Summit RPC | -| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | -| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | -| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | - -The Summit admin RPC on loopback port `3031` is deliberately not proxied. +| Public path | Local upstream | Notes | +| --------------- | ----------------------- | ------------------------------------------------ | +| `/` | `127.0.0.1:3000` | Grafana | +| `/staking` | `/var/www/html/staking` | Static staking UI | +| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | +| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | +| `/summit` | `127.0.0.1:3030` | Summit RPC | +| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | +| `/checkpointer` | `127.0.0.1:42069` | Rate-limited and JWT-protected RPC and snapshots | +| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | +| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | + +The `/checkpointer` route uses the same secret stored at +`/etc/seismic/openresty-jwt-secret` as the protected metrics routes. Clients +must send `Authorization: Bearer `. The Summit admin RPC on loopback port +`3031` remains deliberately unproxied. ### Start or reload OpenResty @@ -510,6 +523,7 @@ Confirm that: `18551`. - Reth ports `8545`, `8546`, `8552`, and `9001` are loopback-only. - Summit ports `3030`, `3031`, and `9090` are loopback-only. +- summit-checkpointer port `42069` is loopback-only when enabled. - Custodian listens on the configured council address when enabled. - OpenResty listens on ports `80` and `443` only when configured and explicitly started. diff --git a/install/README.md b/install/README.md index f211904..60c3661 100644 --- a/install/README.md +++ b/install/README.md @@ -57,6 +57,11 @@ validator. | `443` | TCP | OpenResty HTTPS endpoint | Public when OpenResty is enabled | | `7876` | TCP | Custodian council communication | Externally reachable when Custodian is enabled; restrict sources to intended council participants | +When summit-checkpointer is enabled, its RPC and snapshot server binds only to +`127.0.0.1:42069`. Do not expose TCP port `42069` directly through a firewall. +When OpenResty is enabled, it provides the rate-limited and JWT-protected +`/checkpointer` HTTPS route instead. + The installation log is written to: ```text @@ -125,11 +130,14 @@ The current source-build defaults are: ```text Summit: m/metrics seismic-reth: feat/purpose-key-rotation-reth +Checkpointer: main Custodian: d/centralized-custodian ``` Deferred binaries must be installed at the configured target paths before the -corresponding services are started. +corresponding services are started. A prebuilt or already-present deferred +summit-checkpointer must support `--bind-address`; the generated Supervisor +program uses it to keep the checkpointer RPC on loopback. ## Persistent layout @@ -344,29 +352,33 @@ started manually again after a server or Supervisor restart. ## OpenResty public endpoint When enabled, OpenResty terminates HTTPS, obtains certificates through -`lua-resty-auto-ssl`, applies per-client rate limiting, and proxies local Reth -and Summit endpoints. +`lua-resty-auto-ssl`, applies per-client rate limiting, and proxies local Reth, +Summit, and summit-checkpointer endpoints. -Reth HTTP and WebSocket RPC, Summit RPC, and their metrics listeners remain -bound to loopback whether or not OpenResty is enabled. When OpenResty is -disabled, these endpoints are available only from the node itself or through an -operator-managed tunnel; they are not exposed directly on public interfaces. +Reth HTTP and WebSocket RPC, Summit RPC, their metrics listeners, and the +summit-checkpointer RPC remain bound to loopback whether or not OpenResty is +enabled. When OpenResty is disabled, these endpoints are available only from the +node itself or through an operator-managed tunnel; they are not exposed directly +on public interfaces. The configured routes are: -| Public path | Local upstream | Notes | -| -------------- | ----------------------- | ------------------------------------------- | -| `/` | `127.0.0.1:3000` | Grafana | -| `/staking` | `/var/www/html/staking` | Static staking UI | -| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | -| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | -| `/summit` | `127.0.0.1:3030` | Summit RPC | -| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | -| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | -| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | - -The localhost-only `summit-deposit-rpc` service on port 3031 is deliberately not -proxied. +| Public path | Local upstream | Notes | +| --------------- | ----------------------- | ------------------------------------------------ | +| `/` | `127.0.0.1:3000` | Grafana | +| `/staking` | `/var/www/html/staking` | Static staking UI | +| `/rpc` | `127.0.0.1:8545` | Reth HTTP JSON-RPC | +| `/ws` | `127.0.0.1:8546` | Reth WebSocket JSON-RPC | +| `/summit` | `127.0.0.1:3030` | Summit RPC | +| `/ops` | `127.0.0.1:8552` | Signature-authenticated privileged Reth RPC | +| `/checkpointer` | `127.0.0.1:42069` | Rate-limited and JWT-protected RPC and snapshots | +| `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | +| `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | + +The `/checkpointer` route uses the same secret stored at +`/etc/seismic/openresty-jwt-secret` as the protected metrics routes. Clients +must send `Authorization: Bearer `. The localhost-only +`summit-deposit-rpc` service on port `3031` remains deliberately unproxied. ### Start or reload OpenResty diff --git a/install/lib/checkpointer.sh b/install/lib/checkpointer.sh index ad74261..fe5e920 100644 --- a/install/lib/checkpointer.sh +++ b/install/lib/checkpointer.sh @@ -43,6 +43,29 @@ install_checkpointer_binary() { esac } +check_checkpointer_cli_support() { + local help_output + + [[ -x "$CHECKPOINTER_TARGET_BIN" ]] || return 1 + help_output=$("$CHECKPOINTER_TARGET_BIN" --help 2>&1) || return 1 + grep -q -- '--bind-address' <<<"$help_output" +} + +validate_checkpointer_cli_support() { + local security_error + + if [[ ! -e "$CHECKPOINTER_TARGET_BIN" && ! -L "$CHECKPOINTER_TARGET_BIN" ]]; then + return 1 + fi + if ! security_error=$(check_service_executable_security "$CHECKPOINTER_TARGET_BIN"); then + die "summit-checkpointer executable is not safe for service use: $security_error" + fi + check_checkpointer_cli_support \ + || die "Installed summit-checkpointer does not support --bind-address." + + success "summit-checkpointer command-line compatibility validated" +} + install_mdbx_copy() { local reth_source_dir local libmdbx_dir @@ -94,6 +117,10 @@ install_checkpointer() { section "Installing summit-checkpointer" install_checkpointer_binary + if ! validate_checkpointer_cli_support; then + warn "summit-checkpointer executable is deferred and not present at $CHECKPOINTER_TARGET_BIN." + warn "Provide a compatible executable with --bind-address support before starting the checkpointer." + fi install_mdbx_copy success "summit-checkpointer binary and compatible mdbx_copy installation phase complete." } diff --git a/install/lib/configuration.sh b/install/lib/configuration.sh index 64984fe..7871dad 100644 --- a/install/lib/configuration.sh +++ b/install/lib/configuration.sh @@ -541,7 +541,7 @@ configure_checkpointer() { INSTALL_CHECKPOINTER=false CHECKPOINTER_TARGET_BIN="/usr/local/bin/summit-checkpointer" MDBX_COPY_TARGET_BIN="/usr/local/bin/mdbx_copy" - CHECKPOINTER_SOURCE_REF="m/dynamic-epochs-and-ckpt-chain" + CHECKPOINTER_SOURCE_REF="main" CHECKPOINTER_INSTALL_METHOD="" CHECKPOINTS_DIR="" CHECKPOINTER_BINARY="" diff --git a/install/lib/instructions.sh b/install/lib/instructions.sh index 4681e25..d44f8ed 100644 --- a/install/lib/instructions.sh +++ b/install/lib/instructions.sh @@ -38,6 +38,9 @@ print_missing_service_prerequisites() { if ! security_error=$(check_service_executable_security "$CHECKPOINTER_TARGET_BIN"); then warn "summit-checkpointer cannot start until its executable is secured at $CHECKPOINTER_TARGET_BIN: $security_error." missing=true + elif ! check_checkpointer_cli_support; then + warn "summit-checkpointer cannot start until its executable supports --bind-address: $CHECKPOINTER_TARGET_BIN." + missing=true fi if ! security_error=$(check_service_executable_security "$MDBX_COPY_TARGET_BIN"); then warn "summit-checkpointer cannot start until a compatible mdbx_copy is secured at $MDBX_COPY_TARGET_BIN: $security_error." diff --git a/install/lib/observer-instructions.sh b/install/lib/observer-instructions.sh index 68d2351..c1d1314 100644 --- a/install/lib/observer-instructions.sh +++ b/install/lib/observer-instructions.sh @@ -41,6 +41,9 @@ print_missing_observer_prerequisites() { if ! security_error=$(check_service_executable_security "$CHECKPOINTER_TARGET_BIN"); then warn "summit-checkpointer cannot start until its executable is secured at $CHECKPOINTER_TARGET_BIN: $security_error." missing=true + elif ! check_checkpointer_cli_support; then + warn "summit-checkpointer cannot start until its executable supports --bind-address: $CHECKPOINTER_TARGET_BIN." + missing=true fi if ! security_error=$(check_service_executable_security "$MDBX_COPY_TARGET_BIN"); then warn "summit-checkpointer cannot start until a compatible mdbx_copy is secured at $MDBX_COPY_TARGET_BIN: $security_error." diff --git a/install/templates/openresty/nginx.conf b/install/templates/openresty/nginx.conf index 356f617..2c08c86 100644 --- a/install/templates/openresty/nginx.conf +++ b/install/templates/openresty/nginx.conf @@ -90,6 +90,23 @@ http { proxy_pass http://localhost:8552; } + # Summit checkpointer RPC and snapshot downloads + # (rate limited + JWT protected) + location ~ ^/checkpointer(?:/|$) { + access_by_lua_block { + local rate_limit = loadfile("/usr/local/openresty/nginx/lua/rate_limit.lua") + rate_limit() + local jwt_auth = loadfile("/usr/local/openresty/nginx/lua/jwt_auth.lua") + jwt_auth() + } + rewrite ^/checkpointer/?(.*)$ /$1 break; + proxy_set_header Authorization ""; + proxy_buffering off; + proxy_read_timeout 1h; + proxy_send_timeout 1h; + proxy_pass http://127.0.0.1:42069; + } + # Prometheus Summit (rate limited + JWT protected) location /prom-summit { access_by_lua_block { diff --git a/install/templates/supervisor/checkpointer.conf b/install/templates/supervisor/checkpointer.conf index bb482ee..656cc51 100644 --- a/install/templates/supervisor/checkpointer.conf +++ b/install/templates/supervisor/checkpointer.conf @@ -1,5 +1,5 @@ [program:checkpointer] -command=CHECKPOINTER_BINARY_PLACEHOLDER --config CHECKPOINTER_CONFIG_PLACEHOLDER +command=CHECKPOINTER_BINARY_PLACEHOLDER --config CHECKPOINTER_CONFIG_PLACEHOLDER --bind-address 127.0.0.1 --port 42069 user=SERVICE_USER_PLACEHOLDER autostart=false autorestart=false diff --git a/install/templates/supervisor/reth.conf b/install/templates/supervisor/reth.conf index c172382..e8a8b87 100644 --- a/install/templates/supervisor/reth.conf +++ b/install/templates/supervisor/reth.conf @@ -18,7 +18,6 @@ command=RETH_BINARY_PLACEHOLDER --ipcdisable --auth-ipc --auth-ipc.path /tmp/reth_engine_api.ipc - --rpc.enable-storage-apis PURPOSE_KEYS_ARGUMENTS_PLACEHOLDER --disable-discv4-discovery --disable-dns-discovery From a65d56ca74fc06ec3a023f88c3ff3dc19eb48903 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Tue, 1 Sep 2026 03:07:09 +0800 Subject: [PATCH 4/9] chore: clean generated libmdbx build artifacts --- install/lib/binaries.sh | 27 +++++++++++++++++++++++++++ install/lib/checkpointer.sh | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/install/lib/binaries.sh b/install/lib/binaries.sh index 26977bf..d7de3ef 100644 --- a/install/lib/binaries.sh +++ b/install/lib/binaries.sh @@ -132,6 +132,29 @@ prepare_source_root() { || die "Could not prepare source root: $source_root" } +clean_reth_mdbx_build_artifacts() { + local source_dir=$1 + local libmdbx_dir="$source_dir/crates/storage/libmdbx-rs/mdbx-sys/libmdbx" + local tracked_status + local status + + [[ -d "$source_dir/.git" && -f "$libmdbx_dir/Makefile" ]] || return + + tracked_status=$(run_as_service_user git -C "$source_dir" \ + status --porcelain --untracked-files=no) \ + || die "Could not inspect tracked seismic-reth source changes." + [[ -z "$tracked_status" ]] || return + + status=$(run_as_service_user git -C "$source_dir" status --porcelain) \ + || die "Could not inspect generated seismic-reth build artifacts." + [[ -n "$status" ]] || return + + info "Cleaning generated libmdbx artifacts from the seismic-reth checkout..." + run_as_service_user make -C "$libmdbx_dir" clean \ + >>"$LOG_FILE" 2>&1 \ + || die "Could not clean generated seismic-reth libmdbx artifacts; see $LOG_FILE" +} + prepare_source_checkout() { local description=$1 local repository=$2 @@ -164,6 +187,10 @@ prepare_source_checkout() { [[ "$origin" == "$repository" ]] \ || die "$description checkout has unexpected origin: $origin" + if [[ "$description" == "seismic-reth" ]]; then + clean_reth_mdbx_build_artifacts "$source_dir" + fi + status=$(run_as_service_user git -C "$source_dir" status --porcelain) \ || die "Could not inspect the $description working tree." if [[ -n "$status" ]]; then diff --git a/install/lib/checkpointer.sh b/install/lib/checkpointer.sh index fe5e920..9dae407 100644 --- a/install/lib/checkpointer.sh +++ b/install/lib/checkpointer.sh @@ -106,6 +106,12 @@ install_mdbx_copy() { if ! "$MDBX_COPY_TARGET_BIN" -V >>"$LOG_FILE" 2>&1; then die "mdbx_copy validation failed; see $LOG_FILE" fi + + info "Cleaning generated libmdbx build artifacts..." + run_as_service_user make -C "$libmdbx_dir" clean \ + >>"$LOG_FILE" 2>&1 \ + || die "Could not clean generated libmdbx build artifacts; see $LOG_FILE" + success "Reth-compatible mdbx_copy installed from $reth_source_dir" } From 7ce3c95a032e578dc2fe29f4de9eda14f2df64f4 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Tue, 1 Sep 2026 03:17:50 +0800 Subject: [PATCH 5/9] chore: replace conflicting supervisor config --- install/lib/observer-supervisor.sh | 5 +--- install/lib/supervisor.sh | 39 ++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/install/lib/observer-supervisor.sh b/install/lib/observer-supervisor.sh index b044582..ec1c8e0 100644 --- a/install/lib/observer-supervisor.sh +++ b/install/lib/observer-supervisor.sh @@ -99,7 +99,6 @@ prepare_observer_supervisor_logs() { deploy_observer_supervisor_configuration() { local staging - local conflict section "Deploying observer Supervisor configuration" validate_observer_supervisor_runtime_inputs @@ -110,9 +109,7 @@ deploy_observer_supervisor_configuration() { die "Bootnode RPC validation failed after configuration acceptance." fi - conflict=$(find_conflicting_supervisor_config || true) - [[ -z "$conflict" ]] \ - || die "Another Supervisor file already defines Seismic services: $conflict" + replace_conflicting_supervisor_config [[ ! -L "$SUPERVISOR_CONFIG_PATH" ]] \ || die "Supervisor target must not be a symbolic link: $SUPERVISOR_CONFIG_PATH" [[ ! -L "$CHECKPOINTER_CONFIG_PATH" ]] \ diff --git a/install/lib/supervisor.sh b/install/lib/supervisor.sh index a8570fc..83dd51a 100644 --- a/install/lib/supervisor.sh +++ b/install/lib/supervisor.sh @@ -190,16 +190,44 @@ render_checkpointer_toml() { } find_conflicting_supervisor_config() { + local config_dir local path + local found=false + config_dir=$(dirname -- "$SUPERVISOR_CONFIG_PATH") while IFS= read -r -d '' path; do [[ "$path" == "$SUPERVISOR_CONFIG_PATH" ]] && continue if grep -Eq '^\[program:(reth|summit|summit-observer|summit-observer-checkpoint|summit-deposit-rpc|checkpointer|custodian)\]' "$path"; then printf '%s\n' "$path" - return 0 + found=true fi - done < <(find /etc/supervisor/conf.d -maxdepth 1 -type f -print0) - return 1 + done < <(find "$config_dir" -maxdepth 1 -type f -print0) + + [[ "$found" == true ]] +} + +replace_conflicting_supervisor_config() { + local conflicts=() + local path + + mapfile -t conflicts < <(find_conflicting_supervisor_config || true) + ((${#conflicts[@]} > 0)) || return + + warn "Existing Supervisor files define Seismic services:" + for path in "${conflicts[@]}"; do + warn " $path" + done + + if ! confirm "Replace these files with $SUPERVISOR_CONFIG_PATH?"; then + die "Existing Supervisor configuration was not replaced." + fi + + for path in "${conflicts[@]}"; do + [[ ! -L "$path" && -f "$path" ]] \ + || die "Conflicting Supervisor path changed before replacement: $path" + info "Removing conflicting Supervisor configuration: $path" + rm -f -- "$path" + done } prepare_supervisor_logs() { @@ -224,7 +252,6 @@ prepare_supervisor_logs() { deploy_supervisor_configuration() { local staging - local conflict section "Deploying Supervisor configuration" validate_supervisor_runtime_inputs @@ -235,9 +262,7 @@ deploy_supervisor_configuration() { die "Bootnode RPC validation failed after configuration acceptance." fi - conflict=$(find_conflicting_supervisor_config || true) - [[ -z "$conflict" ]] \ - || die "Another Supervisor file already defines Seismic services: $conflict" + replace_conflicting_supervisor_config [[ ! -L "$SUPERVISOR_CONFIG_PATH" ]] \ || die "Supervisor target must not be a symbolic link: $SUPERVISOR_CONFIG_PATH" [[ ! -L "$CHECKPOINTER_CONFIG_PATH" ]] \ From 1bea5e92da19aafc77ae2d50dc567ac6a27b303e Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Tue, 1 Sep 2026 03:26:19 +0800 Subject: [PATCH 6/9] chore: prevent silent exits and handle config conflicts --- install/install-observer.sh | 3 ++- install/install-validator.sh | 3 ++- install/lib/binaries.sh | 6 +++--- install/lib/common.sh | 9 +++++++++ install/lib/supervisor.sh | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/install/install-observer.sh b/install/install-observer.sh index ce2492a..3b61013 100755 --- a/install/install-observer.sh +++ b/install/install-observer.sh @@ -6,7 +6,7 @@ # keeping observer assignment, parent-key provisioning, and Custodian parent # synchronization explicit. It never starts node services. -set -euo pipefail +set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEMPLATES_DIR="$SCRIPT_DIR/templates" @@ -14,6 +14,7 @@ LOG_FILE="${LOG_FILE:-/var/log/seismic-observer-install.log}" # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" +trap 'unexpected_error "$?" "$LINENO"' ERR # shellcheck source=lib/preflight.sh source "$SCRIPT_DIR/lib/preflight.sh" # shellcheck source=lib/packages.sh diff --git a/install/install-validator.sh b/install/install-validator.sh index 41248a3..d76ad34 100755 --- a/install/install-validator.sh +++ b/install/install-validator.sh @@ -6,7 +6,7 @@ # validator components, prepares persistent state, and deploys service # configuration without starting validator services. -set -euo pipefail +set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEMPLATES_DIR="$SCRIPT_DIR/templates" @@ -14,6 +14,7 @@ LOG_FILE="${LOG_FILE:-/var/log/seismic-validator-install.log}" # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" +trap 'unexpected_error "$?" "$LINENO"' ERR # shellcheck source=lib/preflight.sh source "$SCRIPT_DIR/lib/preflight.sh" # shellcheck source=lib/packages.sh diff --git a/install/lib/binaries.sh b/install/lib/binaries.sh index d7de3ef..56d0eb3 100644 --- a/install/lib/binaries.sh +++ b/install/lib/binaries.sh @@ -138,16 +138,16 @@ clean_reth_mdbx_build_artifacts() { local tracked_status local status - [[ -d "$source_dir/.git" && -f "$libmdbx_dir/Makefile" ]] || return + [[ -d "$source_dir/.git" && -f "$libmdbx_dir/Makefile" ]] || return 0 tracked_status=$(run_as_service_user git -C "$source_dir" \ status --porcelain --untracked-files=no) \ || die "Could not inspect tracked seismic-reth source changes." - [[ -z "$tracked_status" ]] || return + [[ -z "$tracked_status" ]] || return 0 status=$(run_as_service_user git -C "$source_dir" status --porcelain) \ || die "Could not inspect generated seismic-reth build artifacts." - [[ -n "$status" ]] || return + [[ -n "$status" ]] || return 0 info "Cleaning generated libmdbx artifacts from the seismic-reth checkout..." run_as_service_user make -C "$libmdbx_dir" clean \ diff --git a/install/lib/common.sh b/install/lib/common.sh index 737cf65..b3235ef 100644 --- a/install/lib/common.sh +++ b/install/lib/common.sh @@ -31,3 +31,12 @@ die() { error "$*" exit 1 } + +unexpected_error() { + local exit_code=$1 + local line=$2 + + trap - ERR + error "Installer stopped unexpectedly at line $line with exit status $exit_code. See $LOG_FILE." + exit "$exit_code" +} diff --git a/install/lib/supervisor.sh b/install/lib/supervisor.sh index 83dd51a..f8d3e04 100644 --- a/install/lib/supervisor.sh +++ b/install/lib/supervisor.sh @@ -211,7 +211,7 @@ replace_conflicting_supervisor_config() { local path mapfile -t conflicts < <(find_conflicting_supervisor_config || true) - ((${#conflicts[@]} > 0)) || return + ((${#conflicts[@]} > 0)) || return 0 warn "Existing Supervisor files define Seismic services:" for path in "${conflicts[@]}"; do From 888aececf31d749c86bf4a21c449ae8e568955fd Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Tue, 1 Sep 2026 17:44:46 +0800 Subject: [PATCH 7/9] feat: support branch updates and configurable checkpointer config --- install/OBSERVER.md | 6 +- install/README.md | 10 +++ install/lib/binaries.sh | 6 +- install/lib/configuration.sh | 95 +++++++++++++++++++++++++++ install/lib/observer-configuration.sh | 1 + install/lib/observer-supervisor.sh | 2 +- install/lib/supervisor.sh | 39 ++++++++++- 7 files changed, 155 insertions(+), 4 deletions(-) diff --git a/install/OBSERVER.md b/install/OBSERVER.md index ec0fdea..9b227dd 100644 --- a/install/OBSERVER.md +++ b/install/OBSERVER.md @@ -303,12 +303,16 @@ Supervisor logs are written under: /var/log/seismic-observer/ ``` -When summit-checkpointer is enabled, its generated configuration is written to: +When summit-checkpointer is enabled, the installer prompts for an absolute +configuration-file path and defaults to: ```text /etc/seismic/summit-checkpointer.toml ``` +The generated Supervisor command uses the selected path. Its parent directory +hierarchy must be root-owned and must not be group- or world-writable. + When OpenResty is enabled, the installer writes: ```text diff --git a/install/README.md b/install/README.md index 60c3661..ce3c6cf 100644 --- a/install/README.md +++ b/install/README.md @@ -206,6 +206,16 @@ enabled. Supervisor logs are written under: /var/log/seismic-validator/ ``` +When summit-checkpointer is enabled, the installer prompts for an absolute +configuration-file path and defaults to: + +```text +/etc/seismic/summit-checkpointer.toml +``` + +The generated Supervisor command uses the selected path. Its parent directory +hierarchy must be root-owned and must not be group- or world-writable. + When OpenResty is enabled, the installer writes: ```text diff --git a/install/lib/binaries.sh b/install/lib/binaries.sh index 56d0eb3..08c21e4 100644 --- a/install/lib/binaries.sh +++ b/install/lib/binaries.sh @@ -198,7 +198,11 @@ prepare_source_checkout() { fi info "Updating $description branch $branch with fast-forward only..." - run_as_service_user git -C "$source_dir" fetch origin "$branch" \ + run_as_service_user git -C "$source_dir" remote \ + set-branches origin "$branch" \ + >>"$LOG_FILE" 2>&1 \ + || die "Could not configure $description branch $branch for fetching." + run_as_service_user git -C "$source_dir" fetch origin \ >>"$LOG_FILE" 2>&1 \ || die "Could not fetch $description branch $branch; see $LOG_FILE" diff --git a/install/lib/configuration.sh b/install/lib/configuration.sh index 7871dad..59306b0 100644 --- a/install/lib/configuration.sh +++ b/install/lib/configuration.sh @@ -178,6 +178,92 @@ configure_directory() { print_available_disk_space "$selected" } +configure_file_path() { + local variable_name=$1 + local description=$2 + local default=$3 + local selected + local parent + local probe + local current + local owner_uid + local mode + local mode_value + local parent_safe + + while true; do + prompt "$variable_name" "$description" "$default" + selected=${!variable_name} + + if [[ "$selected" != /* ]]; then + error "$description must be an absolute path." + continue + fi + if [[ "$selected" == "/" ]]; then + error "The filesystem root cannot be used for $description." + continue + fi + if contains_unsafe_path_characters "$selected"; then + error "$description contains unsupported whitespace or shell characters." + continue + fi + if [[ -L "$selected" ]]; then + error "$description must not be a symbolic link: $selected" + continue + fi + if [[ -e "$selected" && ! -f "$selected" ]]; then + error "$description exists but is not a regular file: $selected" + continue + fi + + parent=$(dirname -- "$selected") + probe=$parent + while [[ ! -e "$probe" ]]; do + probe=$(dirname -- "$probe") + done + if [[ ! -d "$probe" ]]; then + error "$description has a non-directory parent: $probe" + continue + fi + if [[ -L "$probe" ]]; then + error "$description parent must not be a symbolic link: $probe" + continue + fi + + current=$probe + parent_safe=true + while true; do + if [[ -L "$current" ]]; then + error "$description parent chain contains a symbolic link: $current" + parent_safe=false + break + fi + owner_uid=$(stat -c %u -- "$current") + if [[ "$owner_uid" != "0" ]]; then + error "$description parent must be root-owned: $current" + parent_safe=false + break + fi + mode=$(stat -c %a -- "$current") + mode_value=$((8#$mode)) + if ((mode_value & 0022)); then + error "$description parent must not be group- or world-writable: $current" + parent_safe=false + break + fi + [[ "$current" == "/" ]] && break + current=$(dirname -- "$current") + done + [[ "$parent_safe" == true ]] || continue + + selected=$(realpath -m -- "$selected") + break + done + + printf -v "$variable_name" '%s' "$selected" + success "$description selected: $selected" +} + configure_prebuilt_binary() { local variable_name=$1 local description=$2 @@ -324,9 +410,11 @@ configure_node_software() { configure_component_installation \ SUMMIT_INSTALL_METHOD SUMMIT_BINARY "Summit" SUMMIT_TARGET_BIN + _out "" configure_component_installation \ RETH_INSTALL_METHOD RETH_BINARY "seismic-reth" RETH_TARGET_BIN + _out "" _out "Node software:" print_component_installation \ "Summit" "$SUMMIT_INSTALL_METHOD" "$SUMMIT_BINARY" "$SUMMIT_TARGET_BIN" @@ -541,6 +629,7 @@ configure_checkpointer() { INSTALL_CHECKPOINTER=false CHECKPOINTER_TARGET_BIN="/usr/local/bin/summit-checkpointer" MDBX_COPY_TARGET_BIN="/usr/local/bin/mdbx_copy" + CHECKPOINTER_CONFIG_PATH=${CHECKPOINTER_CONFIG_PATH:-/etc/seismic/summit-checkpointer.toml} CHECKPOINTER_SOURCE_REF="main" CHECKPOINTER_INSTALL_METHOD="" CHECKPOINTS_DIR="" @@ -553,6 +642,10 @@ configure_checkpointer() { "Checkpointer output directory" \ "/persistence/checkpoints" \ "Stores Reth snapshots and Summit verification bundles produced by summit-checkpointer." + configure_file_path \ + CHECKPOINTER_CONFIG_PATH \ + "Checkpointer configuration file" \ + "$CHECKPOINTER_CONFIG_PATH" configure_component_installation \ CHECKPOINTER_INSTALL_METHOD \ CHECKPOINTER_BINARY \ @@ -571,6 +664,7 @@ configure_checkpointer() { if [[ "$CHECKPOINTER_INSTALL_METHOD" == "source" ]]; then _out " Checkpointer source ref: $CHECKPOINTER_SOURCE_REF" fi + _out " Checkpointer config: $CHECKPOINTER_CONFIG_PATH" print_mdbx_copy_plan fi } @@ -808,6 +902,7 @@ print_configuration_summary() { if [[ "$CHECKPOINTER_INSTALL_METHOD" == "source" ]]; then _out " Source ref: $CHECKPOINTER_SOURCE_REF" fi + _out " Config: $CHECKPOINTER_CONFIG_PATH" print_mdbx_copy_plan else _out " Enabled: false" diff --git a/install/lib/observer-configuration.sh b/install/lib/observer-configuration.sh index 30f7dec..a6df177 100644 --- a/install/lib/observer-configuration.sh +++ b/install/lib/observer-configuration.sh @@ -173,6 +173,7 @@ print_observer_configuration_summary() { print_component_installation \ "Checkpointer" "$CHECKPOINTER_INSTALL_METHOD" \ "$CHECKPOINTER_BINARY" "$CHECKPOINTER_TARGET_BIN" + _out " Config: $CHECKPOINTER_CONFIG_PATH" print_mdbx_copy_plan fi diff --git a/install/lib/observer-supervisor.sh b/install/lib/observer-supervisor.sh index ec1c8e0..b8404f9 100644 --- a/install/lib/observer-supervisor.sh +++ b/install/lib/observer-supervisor.sh @@ -137,8 +137,8 @@ deploy_observer_supervisor_configuration() { fi prepare_observer_supervisor_logs - install -d -o root -g root -m 0755 /etc/seismic if [[ "$INSTALL_CHECKPOINTER" == true ]]; then + prepare_checkpointer_config_parent install -o root -g root -m 0644 \ "$staging/summit-checkpointer.toml" "$CHECKPOINTER_CONFIG_PATH" else diff --git a/install/lib/supervisor.sh b/install/lib/supervisor.sh index f8d3e04..cf92373 100644 --- a/install/lib/supervisor.sh +++ b/install/lib/supervisor.sh @@ -230,6 +230,43 @@ replace_conflicting_supervisor_config() { done } +prepare_checkpointer_config_parent() { + local parent + local resolved_parent + local current + local owner_uid + local mode + local mode_value + + parent=$(dirname -- "$CHECKPOINTER_CONFIG_PATH") + resolved_parent=$(realpath -m -- "$parent") + [[ "$resolved_parent" == "$parent" ]] \ + || die "Checkpointer configuration parent must not contain symbolic links: $parent" + + if [[ ! -e "$parent" ]]; then + install -d -o root -g root -m 0755 -- "$parent" + fi + [[ -d "$parent" && ! -L "$parent" ]] \ + || die "Checkpointer configuration parent is not a safe directory: $parent" + + current=$parent + while true; do + [[ ! -L "$current" ]] \ + || die "Checkpointer configuration parent chain contains a symbolic link: $current" + owner_uid=$(stat -c %u -- "$current") \ + || die "Could not inspect checkpointer configuration parent ownership: $current" + [[ "$owner_uid" == "0" ]] \ + || die "Checkpointer configuration parent must be root-owned: $current" + mode=$(stat -c %a -- "$current") \ + || die "Could not inspect checkpointer configuration parent permissions: $current" + mode_value=$((8#$mode)) + ((!(mode_value & 0022))) \ + || die "Checkpointer configuration parent must not be group- or world-writable: $current" + [[ "$current" == "/" ]] && break + current=$(dirname -- "$current") + done +} + prepare_supervisor_logs() { local names=(summit-deposit-rpc reth summit) local name @@ -288,8 +325,8 @@ deploy_supervisor_configuration() { fi prepare_supervisor_logs - install -d -o root -g root -m 0755 /etc/seismic if [[ "$INSTALL_CHECKPOINTER" == true ]]; then + prepare_checkpointer_config_parent install -o root -g root -m 0644 \ "$staging/summit-checkpointer.toml" "$CHECKPOINTER_CONFIG_PATH" else From 905b62387c600d885357eb10c78edce287f6a4c3 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Tue, 1 Sep 2026 19:50:36 +0800 Subject: [PATCH 8/9] feat: add configurable JWT auth tooling --- .github/workflows/checks.yml | 5 +- README.md | 11 +- install/OBSERVER.md | 46 ++++- install/README.md | 46 ++++- install/lib/configuration.sh | 14 ++ install/lib/observer-configuration.sh | 1 + install/lib/openresty.sh | 169 ++++++++++++++- install/templates/openresty/lua/jwt_auth.lua | 2 +- tools/generate-openresty-jwt.sh | 205 +++++++++++++++++++ 9 files changed, 474 insertions(+), 25 deletions(-) create mode 100755 tools/generate-openresty-jwt.sh diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 8f1ca87..12687e5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -33,14 +33,15 @@ jobs: echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - name: Run shfmt - run: shfmt -d install + run: shfmt -d install tools - name: Run ShellCheck working-directory: install run: | shellcheck -x \ install-validator.sh \ - install-observer.sh + install-observer.sh \ + ../tools/generate-openresty-jwt.sh markdown: name: Markdown checks diff --git a/README.md b/README.md index e167ead..fdc3ef5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ install/install-observer.sh See the **[Observer Installer and First-Start Guide](install/OBSERVER.md)**. +Generate short-lived bearer tokens for OpenResty-protected endpoints with: + +```text +tools/generate-openresty-jwt.sh +``` + The Summit internal-testnet genesis file is provided at: ```text @@ -40,13 +46,14 @@ Run the same checks locally from the repository root. ShellCheck must be installed; Go and Node.js with `npx` are also required. ```bash -go run mvdan.cc/sh/v3/cmd/shfmt@v3.12.0 -d install +go run mvdan.cc/sh/v3/cmd/shfmt@v3.12.0 -d install tools ( cd install shellcheck -x \ install-validator.sh \ - install-observer.sh + install-observer.sh \ + ../tools/generate-openresty-jwt.sh ) npx --yes prettier@3.6.2 --check '**/*.md' diff --git a/install/OBSERVER.md b/install/OBSERVER.md index 9b227dd..f020eb3 100644 --- a/install/OBSERVER.md +++ b/install/OBSERVER.md @@ -313,17 +313,48 @@ configuration-file path and defaults to: The generated Supervisor command uses the selected path. Its parent directory hierarchy must be root-owned and must not be group- or world-writable. -When OpenResty is enabled, the installer writes: +When OpenResty is enabled, the installer prompts for an absolute JWT-secret file +path and defaults to: + +```text +/etc/seismic/openresty-jwt-secret +``` + +It also writes: ```text /usr/local/openresty/nginx/conf/nginx.conf /usr/local/openresty/nginx/lua/rate_limit.lua /usr/local/openresty/nginx/lua/jwt_auth.lua -/etc/seismic/openresty-jwt-secret /etc/logrotate.d/openresty ``` -The JWT secret is root-owned and is not printed by the installer. +The generated JWT middleware reads the selected secret path. The secret is +installed as `root:nogroup` with mode `0640` and is not printed by the +installer. Its parent hierarchy must be root-owned, non-writable by group or +others, and traversable by the OpenResty worker user. Reruns reuse a secret +already at the selected path. When changing from the default to a custom path +that does not yet exist, the installer copies the existing default secret +without removing the old file; remove it only after validating the new OpenResty +configuration. + +The installer records the selected path in: + +```text +/etc/seismic/openresty-jwt-secret.path +``` + +This root-owned metadata file contains only the path, not the secret. Installer +reruns use it as the JWT-secret prompt default. Generate a one-hour bearer token +from the repository root with: + +```bash +TOKEN=$(sudo ./tools/generate-openresty-jwt.sh) +``` + +The script reads the metadata file, falls back to the standard secret path for +older installations, and prints only the token. Use `--secret-path` to override +the lookup or `--ttl-seconds` to select a lifetime up to 86400 seconds. Reth HTTP, WebSocket, Ops, and metrics endpoints and Summit RPC, admin RPC, and metrics endpoints bind to loopback whether or not OpenResty is enabled. @@ -486,10 +517,11 @@ The configured routes are: | `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | | `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | -The `/checkpointer` route uses the same secret stored at -`/etc/seismic/openresty-jwt-secret` as the protected metrics routes. Clients -must send `Authorization: Bearer `. The Summit admin RPC on loopback port -`3031` remains deliberately unproxied. +The `/checkpointer` route uses the same secret at the selected JWT-secret path +as the protected metrics routes. The default path is +`/etc/seismic/openresty-jwt-secret`. Clients must send +`Authorization: Bearer `. The Summit admin RPC on loopback port `3031` +remains deliberately unproxied. ### Start or reload OpenResty diff --git a/install/README.md b/install/README.md index ce3c6cf..e46706b 100644 --- a/install/README.md +++ b/install/README.md @@ -216,17 +216,48 @@ configuration-file path and defaults to: The generated Supervisor command uses the selected path. Its parent directory hierarchy must be root-owned and must not be group- or world-writable. -When OpenResty is enabled, the installer writes: +When OpenResty is enabled, the installer prompts for an absolute JWT-secret file +path and defaults to: + +```text +/etc/seismic/openresty-jwt-secret +``` + +It also writes: ```text /usr/local/openresty/nginx/conf/nginx.conf /usr/local/openresty/nginx/lua/rate_limit.lua /usr/local/openresty/nginx/lua/jwt_auth.lua -/etc/seismic/openresty-jwt-secret /etc/logrotate.d/openresty ``` -The JWT secret is root-owned and is not printed by the installer. +The generated JWT middleware reads the selected secret path. The secret is +installed as `root:nogroup` with mode `0640` and is not printed by the +installer. Its parent hierarchy must be root-owned, non-writable by group or +others, and traversable by the OpenResty worker user. Reruns reuse a secret +already at the selected path. When changing from the default to a custom path +that does not yet exist, the installer copies the existing default secret +without removing the old file; remove it only after validating the new OpenResty +configuration. + +The installer records the selected path in: + +```text +/etc/seismic/openresty-jwt-secret.path +``` + +This root-owned metadata file contains only the path, not the secret. Installer +reruns use it as the JWT-secret prompt default. Generate a one-hour bearer token +from the repository root with: + +```bash +TOKEN=$(sudo ./tools/generate-openresty-jwt.sh) +``` + +The script reads the metadata file, falls back to the standard secret path for +older installations, and prints only the token. Use `--secret-path` to override +the lookup or `--ttl-seconds` to select a lifetime up to 86400 seconds. ## First validator startup @@ -385,10 +416,11 @@ The configured routes are: | `/prom-summit` | `127.0.0.1:9090` | Rate-limited and JWT-protected | | `/prom-reth` | `127.0.0.1:9001` | Rate-limited and JWT-protected | -The `/checkpointer` route uses the same secret stored at -`/etc/seismic/openresty-jwt-secret` as the protected metrics routes. Clients -must send `Authorization: Bearer `. The localhost-only -`summit-deposit-rpc` service on port `3031` remains deliberately unproxied. +The `/checkpointer` route uses the same secret at the selected JWT-secret path +as the protected metrics routes. The default path is +`/etc/seismic/openresty-jwt-secret`. Clients must send +`Authorization: Bearer `. The localhost-only `summit-deposit-rpc` service +on port `3031` remains deliberately unproxied. ### Start or reload OpenResty diff --git a/install/lib/configuration.sh b/install/lib/configuration.sh index 59306b0..bbde5af 100644 --- a/install/lib/configuration.sh +++ b/install/lib/configuration.sh @@ -433,6 +433,7 @@ configure_public_endpoint() { DOMAIN="" RATE_LIMIT_RPS="" RATE_LIMIT_BURST="" + OPENRESTY_JWT_SECRET_PATH=${OPENRESTY_JWT_SECRET_PATH:-/etc/seismic/openresty-jwt-secret} if ! confirm "Configure a public HTTPS endpoint with OpenResty?"; then _out "Public HTTPS endpoint: disabled" @@ -440,6 +441,12 @@ configure_public_endpoint() { fi CONFIGURE_PUBLIC_ENDPOINT=true + if [[ "${OPENRESTY_JWT_SECRET_PATH_CONFIGURED:-false}" != true ]] \ + && load_persisted_openresty_jwt_secret_path; then + OPENRESTY_JWT_SECRET_PATH=$PERSISTED_OPENRESTY_JWT_SECRET_PATH + info "Using the previously installed OpenResty JWT secret path as the default." + fi + while true; do prompt DOMAIN "Public domain" "" DOMAIN=${DOMAIN,,} @@ -476,9 +483,16 @@ configure_public_endpoint() { break done + configure_file_path \ + OPENRESTY_JWT_SECRET_PATH \ + "OpenResty JWT secret file" \ + "$OPENRESTY_JWT_SECRET_PATH" + OPENRESTY_JWT_SECRET_PATH_CONFIGURED=true + _out "Public HTTPS endpoint enabled: $CONFIGURE_PUBLIC_ENDPOINT" success "Public HTTPS endpoint configured: https://$DOMAIN" _out "Rate limit: $RATE_LIMIT_RPS requests/sec, burst $RATE_LIMIT_BURST" + _out "JWT secret: $OPENRESTY_JWT_SECRET_PATH (contents hidden)" } validate_http_url() { diff --git a/install/lib/observer-configuration.sh b/install/lib/observer-configuration.sh index a6df177..8c26b67 100644 --- a/install/lib/observer-configuration.sh +++ b/install/lib/observer-configuration.sh @@ -158,6 +158,7 @@ print_observer_configuration_summary() { if [[ "$CONFIGURE_PUBLIC_ENDPOINT" == true ]]; then _out " https://$DOMAIN" _out " Rate limit: $RATE_LIMIT_RPS requests/sec, burst $RATE_LIMIT_BURST" + _out " JWT secret: $OPENRESTY_JWT_SECRET_PATH (contents hidden)" else _out " Disabled" fi diff --git a/install/lib/openresty.sh b/install/lib/openresty.sh index 9d239c6..7067458 100644 --- a/install/lib/openresty.sh +++ b/install/lib/openresty.sh @@ -11,7 +11,10 @@ LUA_RESTY_JWT_RELEASE="v0.1.11" LUA_RESTY_JWT_REVISION="ee1d024071f872e2b5a66eaaf9aeaf86c5bab3ed" LUA_RESTY_AUTO_SSL_VERSION="0.13.1-1" -OPENRESTY_JWT_SECRET_PATH="/etc/seismic/openresty-jwt-secret" +DEFAULT_OPENRESTY_JWT_SECRET_PATH="/etc/seismic/openresty-jwt-secret" +OPENRESTY_JWT_SECRET_PATH_FILE="/etc/seismic/openresty-jwt-secret.path" +OPENRESTY_JWT_SECRET_PATH="$DEFAULT_OPENRESTY_JWT_SECRET_PATH" +PERSISTED_OPENRESTY_JWT_SECRET_PATH="" install_pinned_openresty_lua_library() { local description=$1 @@ -141,31 +144,180 @@ install_openresty() { success "OpenResty dependencies are ready; configuration has not been deployed or started." } +load_persisted_openresty_jwt_secret_path() { + local owner_uid + local mode + local mode_value + local lines=() + local selected + local normalized + + PERSISTED_OPENRESTY_JWT_SECRET_PATH="" + if [[ ! -e "$OPENRESTY_JWT_SECRET_PATH_FILE" && ! -L "$OPENRESTY_JWT_SECRET_PATH_FILE" ]]; then + return 1 + fi + + [[ ! -L "$OPENRESTY_JWT_SECRET_PATH_FILE" ]] \ + || die "OpenResty JWT secret path metadata must not be a symbolic link: $OPENRESTY_JWT_SECRET_PATH_FILE" + [[ -f "$OPENRESTY_JWT_SECRET_PATH_FILE" ]] \ + || die "OpenResty JWT secret path metadata is not a regular file: $OPENRESTY_JWT_SECRET_PATH_FILE" + owner_uid=$(stat -c %u -- "$OPENRESTY_JWT_SECRET_PATH_FILE") \ + || die "Could not inspect OpenResty JWT secret path metadata ownership." + [[ "$owner_uid" == "0" ]] \ + || die "OpenResty JWT secret path metadata must be root-owned." + mode=$(stat -c %a -- "$OPENRESTY_JWT_SECRET_PATH_FILE") \ + || die "Could not inspect OpenResty JWT secret path metadata permissions." + mode_value=$((8#$mode)) + ((!(mode_value & 0022))) \ + || die "OpenResty JWT secret path metadata must not be group- or world-writable." + + mapfile -t lines <"$OPENRESTY_JWT_SECRET_PATH_FILE" + ((${#lines[@]} == 1)) \ + || die "OpenResty JWT secret path metadata must contain exactly one line." + selected=${lines[0]} + [[ "$selected" == /* && "$selected" != "/" ]] \ + || die "OpenResty JWT secret path metadata does not contain a valid absolute path." + normalized=$(realpath -m -- "$selected") + [[ "$normalized" == "$selected" ]] \ + || die "OpenResty JWT secret path metadata is not normalized: $selected" + + PERSISTED_OPENRESTY_JWT_SECRET_PATH=$selected + return 0 +} + +persist_openresty_jwt_secret_path() { + local parent + local resolved_parent + local current + local owner_uid + local mode + local mode_value + local staging + + parent=$(dirname -- "$OPENRESTY_JWT_SECRET_PATH_FILE") + resolved_parent=$(realpath -m -- "$parent") + [[ "$resolved_parent" == "$parent" ]] \ + || die "OpenResty JWT secret path metadata parent must not contain symbolic links: $parent" + + if [[ ! -e "$parent" ]]; then + install -d -o root -g root -m 0755 -- "$parent" + fi + [[ -d "$parent" && ! -L "$parent" ]] \ + || die "OpenResty JWT secret path metadata parent is not a safe directory: $parent" + + current=$parent + while true; do + [[ ! -L "$current" ]] \ + || die "OpenResty JWT secret path metadata parent chain contains a symbolic link: $current" + owner_uid=$(stat -c %u -- "$current") \ + || die "Could not inspect OpenResty JWT secret path metadata parent ownership: $current" + [[ "$owner_uid" == "0" ]] \ + || die "OpenResty JWT secret path metadata parent must be root-owned: $current" + mode=$(stat -c %a -- "$current") \ + || die "Could not inspect OpenResty JWT secret path metadata parent permissions: $current" + mode_value=$((8#$mode)) + ((!(mode_value & 0022))) \ + || die "OpenResty JWT secret path metadata parent must not be group- or world-writable: $current" + [[ "$current" == "/" ]] && break + current=$(dirname -- "$current") + done + + staging=$(mktemp "$parent/.openresty-jwt-secret-path.XXXXXX") + chmod 0600 "$staging" + printf '%s\n' "$OPENRESTY_JWT_SECRET_PATH" >"$staging" + chown root:root "$staging" + chmod 0644 "$staging" + mv -- "$staging" "$OPENRESTY_JWT_SECRET_PATH_FILE" +} + +prepare_openresty_jwt_secret_parent() { + local parent + local resolved_parent + local current + local owner_uid + local mode + local mode_value + + parent=$(dirname -- "$OPENRESTY_JWT_SECRET_PATH") + resolved_parent=$(realpath -m -- "$parent") + [[ "$resolved_parent" == "$parent" ]] \ + || die "OpenResty JWT secret parent must not contain symbolic links: $parent" + + if [[ ! -e "$parent" ]]; then + install -d -o root -g root -m 0755 -- "$parent" + fi + [[ -d "$parent" && ! -L "$parent" ]] \ + || die "OpenResty JWT secret parent is not a safe directory: $parent" + + current=$parent + while true; do + [[ ! -L "$current" ]] \ + || die "OpenResty JWT secret parent chain contains a symbolic link: $current" + owner_uid=$(stat -c %u -- "$current") \ + || die "Could not inspect OpenResty JWT secret parent ownership: $current" + [[ "$owner_uid" == "0" ]] \ + || die "OpenResty JWT secret parent must be root-owned: $current" + mode=$(stat -c %a -- "$current") \ + || die "Could not inspect OpenResty JWT secret parent permissions: $current" + mode_value=$((8#$mode)) + ((!(mode_value & 0022))) \ + || die "OpenResty JWT secret parent must not be group- or world-writable: $current" + [[ "$current" == "/" ]] && break + current=$(dirname -- "$current") + done + + command -v runuser >/dev/null 2>&1 \ + || die "runuser is required to validate OpenResty JWT secret access." + runuser -u nobody -- test -x "$parent" \ + || die "OpenResty worker user cannot traverse JWT secret parent: $parent" +} + setup_openresty_jwt_secret() { local legacy_lua="/usr/local/openresty/nginx/lua/jwt_auth.lua" local legacy_secret="" + local secret_parent local staging - install -d -o root -g root -m 0755 /etc/seismic + prepare_openresty_jwt_secret_parent + secret_parent=$(dirname -- "$OPENRESTY_JWT_SECRET_PATH") if [[ -e "$OPENRESTY_JWT_SECRET_PATH" || -L "$OPENRESTY_JWT_SECRET_PATH" ]]; then [[ ! -L "$OPENRESTY_JWT_SECRET_PATH" ]] \ || die "OpenResty JWT secret must not be a symbolic link: $OPENRESTY_JWT_SECRET_PATH" + [[ -f "$OPENRESTY_JWT_SECRET_PATH" ]] \ + || die "OpenResty JWT secret exists but is not a regular file: $OPENRESTY_JWT_SECRET_PATH" [[ -s "$OPENRESTY_JWT_SECRET_PATH" ]] \ || die "OpenResty JWT secret exists but is empty: $OPENRESTY_JWT_SECRET_PATH" chown root:nogroup "$OPENRESTY_JWT_SECRET_PATH" chmod 0640 "$OPENRESTY_JWT_SECRET_PATH" + runuser -u nobody -- test -r "$OPENRESTY_JWT_SECRET_PATH" \ + || die "OpenResty worker user cannot read the JWT secret." info "Reusing the existing OpenResty JWT secret." return fi - if [[ -f "$legacy_lua" && ! -L "$legacy_lua" ]]; then + if [[ "$OPENRESTY_JWT_SECRET_PATH" != "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" ]] \ + && [[ -e "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" || -L "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" ]]; then + [[ ! -L "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" ]] \ + || die "Default OpenResty JWT secret must not be a symbolic link: $DEFAULT_OPENRESTY_JWT_SECRET_PATH" + [[ -f "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" ]] \ + || die "Default OpenResty JWT secret exists but is not a regular file: $DEFAULT_OPENRESTY_JWT_SECRET_PATH" + [[ -s "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" ]] \ + || die "Default OpenResty JWT secret exists but is empty: $DEFAULT_OPENRESTY_JWT_SECRET_PATH" + elif [[ -f "$legacy_lua" && ! -L "$legacy_lua" ]]; then legacy_secret=$(sed -n 's/.*local JWT_SECRET = "\([^"]*\)".*/\1/p' "$legacy_lua" | head -n 1) fi - staging=$(mktemp /etc/seismic/.openresty-jwt-secret.XXXXXX) + staging=$(mktemp "$secret_parent/.openresty-jwt-secret.XXXXXX") chmod 0600 "$staging" - if [[ -n "$legacy_secret" ]]; then + if [[ "$OPENRESTY_JWT_SECRET_PATH" != "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" ]] \ + && [[ -s "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" ]]; then + if ! cp -- "$DEFAULT_OPENRESTY_JWT_SECRET_PATH" "$staging"; then + rm -f -- "$staging" + die "Could not copy the existing default JWT secret to the selected path." + fi + info "Copying the existing default JWT secret to the selected path." + elif [[ -n "$legacy_secret" ]]; then printf '%s\n' "$legacy_secret" >"$staging" info "Migrating the existing embedded OpenResty JWT secret." elif ! openssl rand -base64 32 >"$staging"; then @@ -182,6 +334,8 @@ setup_openresty_jwt_secret() { chown root:nogroup "$staging" chmod 0640 "$staging" mv -- "$staging" "$OPENRESTY_JWT_SECRET_PATH" + runuser -u nobody -- test -r "$OPENRESTY_JWT_SECRET_PATH" \ + || die "OpenResty worker user cannot read the installed JWT secret." success "OpenResty JWT secret stored securely; its value was not logged." } @@ -221,7 +375,9 @@ deploy_openresty_configuration() { -e "s|RATE_LIMIT_RPS_PLACEHOLDER|$RATE_LIMIT_RPS|g" \ -e "s|RATE_LIMIT_BURST_PLACEHOLDER|$RATE_LIMIT_BURST|g" \ "$template_root/lua/rate_limit.lua" >"$staging/rate_limit.lua" \ - || ! cp "$template_root/lua/jwt_auth.lua" "$staging/jwt_auth.lua"; then + || ! sed \ + "s|OPENRESTY_JWT_SECRET_PATH_PLACEHOLDER|$OPENRESTY_JWT_SECRET_PATH|g" \ + "$template_root/lua/jwt_auth.lua" >"$staging/jwt_auth.lua"; then rm -rf -- "$staging" die "Could not render the OpenResty configuration templates." fi @@ -260,6 +416,7 @@ deploy_openresty_configuration() { die "Installed OpenResty configuration validation failed; see $LOG_FILE" fi + persist_openresty_jwt_secret_path success "OpenResty configuration deployed for https://$DOMAIN." info "OpenResty was not started, enabled, or reloaded." } diff --git a/install/templates/openresty/lua/jwt_auth.lua b/install/templates/openresty/lua/jwt_auth.lua index e648296..375bd1c 100644 --- a/install/templates/openresty/lua/jwt_auth.lua +++ b/install/templates/openresty/lua/jwt_auth.lua @@ -1,6 +1,6 @@ local jwt = require "resty.jwt" -local secret_file, secret_error = io.open("/etc/seismic/openresty-jwt-secret", "r") +local secret_file, secret_error = io.open("OPENRESTY_JWT_SECRET_PATH_PLACEHOLDER", "r") if not secret_file then ngx.log(ngx.ERR, "could not open JWT secret: ", secret_error) return ngx.exit(500) diff --git a/tools/generate-openresty-jwt.sh b/tools/generate-openresty-jwt.sh new file mode 100755 index 0000000..20e067f --- /dev/null +++ b/tools/generate-openresty-jwt.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +DEFAULT_SECRET_PATH="/etc/seismic/openresty-jwt-secret" +SECRET_PATH_FILE=${OPENRESTY_JWT_SECRET_PATH_FILE:-/etc/seismic/openresty-jwt-secret.path} +SECRET_PATH="" +TTL_SECONDS=3600 +SUBJECT="seismic-operator" + +usage() { + cat <&2 + exit 1 +} + +contains_control_characters() { + [[ "$1" =~ [[:cntrl:]] ]] +} + +validate_root_owned_nonwritable_file() { + local description=$1 + local path=$2 + local owner_uid + local mode + local mode_value + + [[ ! -L "$path" ]] || die "$description must not be a symbolic link: $path" + [[ -f "$path" ]] || die "$description is not a regular file: $path" + owner_uid=$(stat -c %u -- "$path") || die "Could not inspect $description ownership." + [[ "$owner_uid" == "0" ]] || die "$description must be root-owned: $path" + mode=$(stat -c %a -- "$path") || die "Could not inspect $description permissions." + mode_value=$((8#$mode)) + ((!(mode_value & 0022))) || die "$description must not be group- or world-writable: $path" +} + +read_installed_secret_path() { + local lines=() + local selected + + validate_root_owned_nonwritable_file "JWT secret path metadata" "$SECRET_PATH_FILE" + mapfile -t lines <"$SECRET_PATH_FILE" + ((${#lines[@]} == 1)) || die "JWT secret path metadata must contain exactly one line." + selected=${lines[0]} + [[ "$selected" == /* && "$selected" != "/" ]] \ + || die "JWT secret path metadata does not contain a valid absolute path." + [[ $(realpath -m -- "$selected") == "$selected" ]] \ + || die "JWT secret path metadata is not normalized: $selected" + SECRET_PATH=$selected +} + +validate_secret_file_security() { + local mode + local mode_value + + validate_root_owned_nonwritable_file "JWT secret" "$SECRET_PATH" + mode=$(stat -c %a -- "$SECRET_PATH") || die "Could not inspect JWT secret permissions." + mode_value=$((8#$mode)) + ((!(mode_value & 0007))) \ + || die "JWT secret must not be accessible by users outside its owner and group: $SECRET_PATH" +} + +validate_secret_parent_chain() { + local parent + local resolved_parent + local current + local owner_uid + local mode + local mode_value + + parent=$(dirname -- "$SECRET_PATH") + resolved_parent=$(realpath -m -- "$parent") + [[ "$resolved_parent" == "$parent" ]] \ + || die "JWT secret parent must not contain symbolic links: $parent" + + current=$parent + while true; do + [[ -d "$current" && ! -L "$current" ]] \ + || die "JWT secret parent chain contains an unsafe directory: $current" + owner_uid=$(stat -c %u -- "$current") \ + || die "Could not inspect JWT secret parent ownership: $current" + [[ "$owner_uid" == "0" ]] \ + || die "JWT secret parent must be root-owned: $current" + mode=$(stat -c %a -- "$current") \ + || die "Could not inspect JWT secret parent permissions: $current" + mode_value=$((8#$mode)) + ((!(mode_value & 0022))) \ + || die "JWT secret parent must not be group- or world-writable: $current" + [[ "$current" == "/" ]] && break + current=$(dirname -- "$current") + done +} + +while (($# > 0)); do + case "$1" in + --secret-path) + (($# >= 2)) || die "--secret-path requires a value." + SECRET_PATH=$2 + shift 2 + ;; + --ttl-seconds) + (($# >= 2)) || die "--ttl-seconds requires a value." + TTL_SECONDS=$2 + shift 2 + ;; + --subject) + (($# >= 2)) || die "--subject requires a value." + SUBJECT=$2 + shift 2 + ;; + --help | -h) + usage + exit 0 + ;; + *) + die "Unknown option: $1" + ;; + esac +done + +command -v python3 >/dev/null 2>&1 || die "python3 is required." +command -v realpath >/dev/null 2>&1 || die "realpath is required." + +[[ "$TTL_SECONDS" =~ ^[1-9][0-9]*$ ]] \ + || die "--ttl-seconds must be a positive integer." +((TTL_SECONDS <= 86400)) || die "--ttl-seconds must not exceed 86400." +[[ -n "$SUBJECT" ]] || die "--subject must not be empty." +((${#SUBJECT} <= 128)) || die "--subject must not exceed 128 characters." +contains_control_characters "$SUBJECT" && die "--subject must not contain control characters." + +if [[ -z "$SECRET_PATH" ]]; then + if [[ -e "$SECRET_PATH_FILE" || -L "$SECRET_PATH_FILE" ]]; then + read_installed_secret_path + else + SECRET_PATH=$DEFAULT_SECRET_PATH + fi +fi + +[[ "$SECRET_PATH" == /* && "$SECRET_PATH" != "/" ]] \ + || die "JWT secret path must be an absolute file path." +NORMALIZED_SECRET_PATH=$(realpath -m -- "$SECRET_PATH") +[[ "$NORMALIZED_SECRET_PATH" == "$SECRET_PATH" ]] \ + || die "JWT secret path must be normalized and must not contain symbolic links: $SECRET_PATH" +validate_secret_parent_chain +validate_secret_file_security +[[ -s "$SECRET_PATH" ]] || die "JWT secret is empty: $SECRET_PATH" +[[ -r "$SECRET_PATH" ]] \ + || die "JWT secret is not readable; run this command with sudo: $SECRET_PATH" + +python3 - "$SECRET_PATH" "$TTL_SECONDS" "$SUBJECT" <<'PY' +import base64 +import hashlib +import hmac +import json +import sys +import time +from pathlib import Path + + +def base64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +secret_path = Path(sys.argv[1]) +ttl_seconds = int(sys.argv[2]) +subject = sys.argv[3] +secret = secret_path.read_bytes().rstrip() +if not secret: + raise SystemExit("JWT secret is empty after trimming trailing whitespace") + +now = int(time.time()) +header = {"alg": "HS256", "typ": "JWT"} +payload = { + "sub": subject, + "iat": now, + "nbf": now - 5, + "exp": now + ttl_seconds, +} +encoded_header = base64url(json.dumps(header, separators=(",", ":")).encode()) +encoded_payload = base64url(json.dumps(payload, separators=(",", ":")).encode()) +signing_input = f"{encoded_header}.{encoded_payload}".encode() +signature = hmac.new(secret, signing_input, hashlib.sha256).digest() +print(f"{encoded_header}.{encoded_payload}.{base64url(signature)}") +PY From d4356048f281df1fcad0eaca5c0f562404c8b415 Mon Sep 17 00:00:00 2001 From: Matthias Wright Date: Tue, 1 Sep 2026 20:58:26 +0800 Subject: [PATCH 9/9] chore: remove single-branch restriction --- install/OBSERVER.md | 8 ++++++++ install/README.md | 8 ++++++++ install/lib/binaries.sh | 9 ++++----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/install/OBSERVER.md b/install/OBSERVER.md index f020eb3..ea88828 100644 --- a/install/OBSERVER.md +++ b/install/OBSERVER.md @@ -595,6 +595,14 @@ On a normal rerun, the installer: Changing a persistent path does not migrate existing data. It creates or uses a separate store. +For source installations, new checkouts fetch all remote branches. On a rerun, +the installer validates the origin and clean working tree, configures `origin` +to fetch all branches, fetches and prunes remote references, checks out or +creates the configured local branch, and merges `origin/` with +`--ff-only` before rebuilding. Dirty, diverged, or force-pushed checkouts are +rejected rather than reset. The installer does not update its own +`seismic-node-ops` checkout. + Before applying a later `supervisorctl update`, inspect running services: ```bash diff --git a/install/README.md b/install/README.md index e46706b..b3d12f8 100644 --- a/install/README.md +++ b/install/README.md @@ -448,6 +448,14 @@ On a normal rerun, the installer preserves existing validator keys and state, then replaces its generated OpenResty and Supervisor configuration. It does not start or reload those services. +For source installations, new checkouts fetch all remote branches. On a rerun, +the installer validates the origin and clean working tree, configures `origin` +to fetch all branches, fetches and prunes remote references, checks out or +creates the configured local branch, and merges `origin/` with +`--ff-only` before rebuilding. Dirty, diverged, or force-pushed checkouts are +rejected rather than reset. The installer does not update its own +`seismic-node-ops` checkout. + Before applying a later `supervisorctl update`, inspect running services: ```bash diff --git a/install/lib/binaries.sh b/install/lib/binaries.sh index 08c21e4..fe4176d 100644 --- a/install/lib/binaries.sh +++ b/install/lib/binaries.sh @@ -171,7 +171,6 @@ prepare_source_checkout() { info "Cloning $description branch $branch into $source_dir..." if ! run_as_service_user git clone \ --branch "$branch" \ - --single-branch \ "$repository" \ "$source_dir" >>"$LOG_FILE" 2>&1; then die "Could not clone $description; see $LOG_FILE" @@ -199,12 +198,12 @@ prepare_source_checkout() { info "Updating $description branch $branch with fast-forward only..." run_as_service_user git -C "$source_dir" remote \ - set-branches origin "$branch" \ + set-branches origin '*' \ >>"$LOG_FILE" 2>&1 \ - || die "Could not configure $description branch $branch for fetching." - run_as_service_user git -C "$source_dir" fetch origin \ + || die "Could not configure $description to fetch all origin branches." + run_as_service_user git -C "$source_dir" fetch --prune origin \ >>"$LOG_FILE" 2>&1 \ - || die "Could not fetch $description branch $branch; see $LOG_FILE" + || die "Could not fetch $description branches; see $LOG_FILE" if run_as_service_user git -C "$source_dir" show-ref \ --verify --quiet "refs/heads/$branch"; then