Solo Weaver is a Kubernetes-native deployment automation platform for Hedera network components. It enables node operators to migrate from traditional deployment models to modern, containerized infrastructure with automated lifecycle management.
Below is a quickstart guide to get you up and running with Solo Weaver.
- Unix operating system (Tested on: Debian 13.1.0, Ubuntu 22.04)
curlinstalled
Note: No system users need to be pre-created. The
weaver:2500service account is created automatically duringsolo-provisioner install. Thehedera:2000user and group (used for node storage ownership — block node, consensus node, and similar workloads) are created automatically when the relevantnode installcommand is first run.
- Run the single-line installer:
curl -sSL https://raw.githubusercontent.com/hashgraph/solo-weaver/main/install.sh | bash- Verify installation:
solo-provisioner --help
sudo solo-provisioner uninstall --yes--yes is required — the command refuses to run without it. It removes, irreversibly:
- the
solo-provisionerCLI and its/usr/local/binsymlink - the
solo-provisioner-daemonbinary and its systemd service - the
solo-provisioner-network-nftandsolo-provisioner-bandwidth-shaperboot units - the configuration tree under
/etc/solo-provisioner(rendered.nftfiles, the policy registry, tc device/class configs)
Tear down the workloads first — the command fails while a Kubernetes cluster is still provisioned, since every teardown command lives in the binary it is about to delete:
sudo solo-provisioner block node uninstall
sudo solo-provisioner kube cluster uninstall
sudo solo-provisioner uninstall --yesLoaded nft tables and tc qdiscs are left alone; they do not survive a reboot once the boot-replay units and their inputs are gone.
Additional Flags
| Flag | Description |
|---|---|
--yes |
Confirm removal of the CLI, the daemon and its service, the network boot units, and /etc/solo-provisioner |
These flags are available for all commands:
| Flag | Short | Description | Default |
|---|---|---|---|
--config |
-c |
Path to configuration file | None |
--profile |
-p |
Deployment profile | Required for most commands |
--output |
-o |
Output format (text, json) | text |
--non-interactive |
— | Disable TUI and output raw logs; useful for CI/pipelines | false |
--version |
-v |
Show version | - |
--help |
-h |
Show help | - |
About
--output: selects the stdout format.
text(default) — human-readable output: the interactive TUI on a terminal, or plain console log lines when piped /--non-interactive.json— machine-readable output for automation (Ansible,jq, CI). Emits one JSON object per log event (NDJSON) on stdout, followed by a final tagged summary object{"type":"summary","status":…,"report_path":…,"report":{…}}. Select it withjq 'select(.type=="summary")'(a trailing log line may follow, so filter by the tag rather than assuming it is the last line). Passing-o jsonforces non-interactive mode (the TUI never renders), matching common tooling (kubectl -o json,terraform -json). Human error panels still go to stderr, so the stdout JSON stream stays clean.The YAML workflow report file (
setup_report_<timestamp>.yaml) is written in both modes regardless of-o; its path is reported in thereport_path=…field (and in the JSON summary object).
Most installation commands support these execution control flags:
| Flag | Description | Default |
|---|---|---|
--stop-on-error |
Stop execution on first error | true |
--rollback-on-error |
Rollback executed steps on error | false |
--continue-on-error |
Continue executing steps even if some fail | false |
Solo Provisioner supports five deployment profiles that configure behavior and defaults:
| Profile | Description | Use Case |
|---|---|---|
local |
Local development and testing | Development, CI/CD |
perfnet |
Performance testing network | Load testing |
testnet |
Hedera Testnet | Integration testing |
previewnet |
Hedera Previewnet | Preview/staging testing |
mainnet |
Hedera Mainnet | Production deployment |
Important: Always use
--profileto specify your target environment.
The primary commands for managing Hedera Block Nodes.
Run preflight checks to validate the system is ready for Block Node deployment:
# Basic preflight check
sudo solo-provisioner block node check --profile=mainnet
# With custom config file
sudo solo-provisioner block node check --profile=testnet --config=/path/to/config.yaml
# Check hardware requirements for a specific plugin preset
sudo solo-provisioner block node check --profile=mainnet --plugin-preset=tier1-lfh
# Check with explicit plugin list
sudo solo-provisioner block node check --profile=mainnet --plugins=com.hedera.block.suites.BlockStreamPublishing,com.hedera.block.suites.LocalFileSystemRecorderWhat it checks:
- System requirements (CPU, memory, disk)
- Required dependencies
- Network connectivity
- Storage availability
Additional Flags:
| Flag | Description | Default |
|---|---|---|
--plugin-preset |
Plugin preset to deploy (tier1-lfh, tier1-rfh, or custom); used for hardware sizing |
"" |
--plugins |
Comma-separated plugin list; overrides --plugin-preset when set |
"" |
Deploy a complete Hedera Block Node with Kubernetes cluster:
# Basic installation with defaults
sudo solo-provisioner block node install --profile=local
# Production installation with custom values
sudo solo-provisioner block node install \
--profile=mainnet \
--config=/path/to/config.yaml \
--values=/path/to/custom-values.yaml
# With custom storage configuration
# Note: --verification-size applies to chart versions below 0.37.0;
# --application-state-size applies to chart versions 0.37.0 and above
# (verification retires and application-state appears in the same chart cutover,
# hiero-ledger/hiero-block-node#3025). The flag for the inactive storage is
# silently ignored outside its applicable range.
sudo solo-provisioner block node install \
--profile=mainnet \
--base-path=/mnt/nvme \
--live-size=50Gi \
--archive-size=500Gi \
--verification-size=50Gi \
--log-size=10Gi \
--application-state-size=500Mi
# With specific chart version
sudo solo-provisioner block node install \
--profile=testnet \
--chart-version=0.22.1 \
--namespace=hedera-blockAvailable Flags:
| Flag | Description |
|---|---|
--values, -f |
Custom Helm values file |
--chart-repo |
Helm chart repository URL |
--chart-version |
Specific chart version |
--timeout |
Timeout for the block node Helm install/upgrade, as a Go duration (e.g. 10m, 600s, 1h). The operation is rolled back (--atomic) if it exceeds this budget (default: 5m0s) |
--namespace |
Kubernetes namespace |
--release-name |
Helm release name |
--base-path |
Base path for all storage |
--archive-path |
Archive storage path |
--live-path |
Live storage path |
--verification-path |
Verification storage path (chart versions below 0.37.0) |
--log-path |
Log storage path |
--application-state-path |
Application-state storage path (chart versions 0.37.0 and above; introduced by hiero-ledger/hiero-block-node#3025) |
--live-size |
Live storage size (e.g., 10Gi) |
--archive-size |
Archive storage size |
--verification-size |
Verification storage size (chart versions below 0.37.0) |
--log-size |
Log storage size |
--application-state-size |
PV/PVC size for application-state storage (e.g., 500Mi, 1Gi); chart versions 0.37.0 and above |
--plugin-preset |
Plugin preset to deploy (tier1-lfh, tier1-rfh, custom, or none for no override — use --values/chart default); prompts interactively when omitted |
--plugins |
Comma-separated plugin list; overrides --plugin-preset when set |
--plugins-size |
PV/PVC size for plugins storage (e.g., 5Gi, 10Gi) |
--plugins-path |
Path for plugins storage |
--historic-retention |
Historic block retention threshold (0 = unlimited) |
--recent-retention |
Recent block retention threshold (default: 96000) |
--load-balancer-enabled |
Inject MetalLB address-pool annotation into the block node service; set to false for environments without MetalLB (default: true). See Block-node service exposure for how this interacts with service.type and the chart's split topology. |
--firewall-enabled |
Apply the node-level host firewall (inet weaver-host-firewall table: SSH/mgmt allowlist, ICMP policy, in-cluster ports). Opt-in (default: false); set to true to have this tool manage the host firewall |
--mgmt-cidrs |
Host firewall SSH/management allowlist CIDRs (IPv4 and/or IPv6 — each entry is routed to the matching ipv4_addr/ipv6_addr set). Empty skips the host firewall. |
--blocked-cidrs |
Host firewall operator-curated block list CIDRs (IPv4 and/or IPv6), dropped inbound, outbound, and forwarded — including established connections, and including pod-bound traffic. Distinct from the BN workload plane's bn-restricted set, which the traffic-shaper daemon manages automatically. |
--ssh-port |
Host firewall SSH/management TCP port (default 22) |
--pod-cidr |
Host firewall pod CIDR for the in-cluster host-service ports rule (defaults to the cluster pod subnet). May be IPv4 and/or IPv6 (repeat or comma-separate for dual-stack). |
--in-cluster-ports |
Host firewall in-cluster host-service ports (defaults to 6443,4244,7472,10250) |
--traffic-shaping-enabled |
Create the BN workload network-policy plane (inet weaver-workload-policy classification) and tc HTB traffic shaping, and install the traffic-shaper daemon. Opt-in (default: false); set to true to get all three |
--egress-interface |
Physical NIC for the $EGRESS HTB traffic-shaper hierarchy (e.g. eth0). Auto-detected from the default route when omitted; use this flag to override on multi-NIC hosts. Renders /usr/local/sbin/solo-provisioner-bandwidth-shaper.sh and installs solo-provisioner-bandwidth-shaper.service so the HTB hierarchy survives reboot. |
--link-rate |
NIC line rate in tc-style format (e.g. 1gbit, 100mbit), or auto to detect and store the link speed at install time (written as explicit proportional class rates). Auto-detected from sysfs at each boot when omitted. Interactively, the prompt accepts a rate, auto, or blank. The link is full-duplex, so this rate parameterizes both the $EGRESS and $VETH HTB trunks. |
--shape |
Per-class HTB bandwidth override, repeatable: --shape <class>=rate=<r>,ceil=<c>,prio=<p> (e.g. --shape publisher=rate=800mbit,ceil=1gbit,prio=0). Any subset of rate/ceil/prio may be given; classes not overridden use the profile defaults. Valid classes: publisher, backfill-response, reserve-ingress, partner, public, reserve-egress. |
--daemon-version |
Version of solo-provisioner-daemon to auto-download when traffic shaping installs the daemon (defaults to this CLI's own version). Ignored when --daemon-bin is set. |
--daemon-bin |
Local path to a pre-built solo-provisioner-daemon binary, installed as-is instead of downloaded. Highest-precedence source; omit it to resolve the binary automatically (SOLO_PROVISIONER_DAEMON_BIN, then an already-installed binary, then the catalog). |
--statusz-base-url |
Override the daemon's block-node statusz endpoint with an explicit http(s) base URL (e.g. http://127.0.0.1:8080) for a port-forward or directly-reachable BN. Merged into daemon.yaml (components.block_node.statusz.base_url); omitting the flag preserves any value already on disk. Only when no base_url is set at all does the daemon discover the endpoint from the watched BN pod. |
--statusz-poll-interval |
Cadence at which the daemon's block-node traffic-shaper monitor polls statusz, as a positive Go duration (e.g. 5s, 30s). Merged into daemon.yaml (components.block_node.statusz.poll_interval); omitting the flag preserves any value already on disk. Only when no poll_interval is set at all does the daemon fall back to its 5s default. |
Host firewall:
block node installcan lay down the node-levelinet hostfirewall (SSH/management allowlist, ICMP policy, in-cluster host-service ports) — regardless of whether it's bootstrapping a bare-metal host or deploying onto an already-existing cluster. This is opt-in (--firewall-enabled, defaultfalse) so existing non-interactive callers are unaffected; when enabled, the firewall is owned by the block-node workflow, not by the generickube cluster install(which provisions clusters for other purposes too and should not unconditionally apply node-specific rules). The--mgmt-cidrs/--blocked-cidrs/--ssh-port/--pod-cidr/--in-cluster-portsflags (and interactive prompts) configure it once enabled. An empty management allowlist skips the firewall to avoid locking the host out of SSH.--blocked-cidrsonly takes effect when--firewall-enabledis also true — both live on the sameinet hosttable, rendered by the same step — but the set itself is a plain deny list, dropped before every other rule (including established connections), and is purely operator-managed for its whole lifecycle, unlike the daemon-ownedbn-restrictedset on the BN workload plane.
IPv6 / dual-stack: both the host firewall (
inet weaver-host-firewall) and the BN workload plane (inet weaver-workload-policy, which drives traffic-shaping classification) are dual-stack. The v4 and v6 sets are always rendered;--mgmt-cidrs,--blocked-cidrs,--pod-cidr(andnetwork policy --cidrs) accept mixed IPv4/IPv6 lists and route each entry to the matching family's set. The host firewall's default-drop input chain explicitly admits ICMPv6 Neighbor Discovery (NS/NA/RS/RA, hop-limit-255 guarded), MLD, andpacket-too-big(the IPv6 PMTUD signal) — without these IPv6 would be non-functional under the drop policy. IPv6 workload classification is active once a v6--pod-cidris supplied (auto-detection resolves only the v4 pod CIDR today; pass the v6 companion explicitly).
Traffic shaping gate: daemon activation is not a separate decision from traffic shaping —
block node installautomatically installs and provisions the traffic-shaper daemon whenever traffic shaping is enabled, with no separate prompt or flag.--traffic-shaping-enabledis opt-in (defaultfalse) so existing non-interactive callers are unaffected; enabling it creates the BN workload network-policy plane (inet weaver-workload-policyclassification), tc HTB shaping, and installs the daemon, all together — declining (or the default) skips the egress NIC/link-rate prompts too, since there would be nothing left for any of it to reconcile. This decision is durable: it's recorded in the runtime state and honored by laterreconfigure/upgraderuns too, not just the install where it was made. This whole gate is independent of--firewall-enabled, which only gates the host's own SSH/mgmt firewall.
Traffic-shaper defaults:
block node installrecords both the$EGRESS(physical NIC) and the$VETH(per-pod ingress) HTB shapes using the design's default per-class budgets, so the daemon can install the ingress hierarchy on the next pod create without any manualnetwork shapestep. The$EGRESSshape is persisted for reboot replay (solo-provisioner-bandwidth-shaper.service); the ingress shape is recorded as config only (the$VETHis ephemeral and replayed per-pod). Ingress bandwidth defaults to the egress--link-rate. Tune any class afterward withnetwork shape set --class <name> --rate/--ceil/--prio, or at install time with--shape <class>=rate=...,ceil=...,prio=....
Network policies: when traffic shaping is enabled (see the traffic shaping gate above),
block node installlays down theinet weaver-workload-policyclassification plane by creating the fixed set of BN policies (bn-publisher,bn-subscriber-in,bn-partner-out,bn-public-out,bn-status-in/out,bn-mgmt-in/out,bn-restricted,bn-backfill) idempotently, then persists the renderednetwork-weaver-workload-policy.nftfor reboot replay. The one operator-curated set,bn-mgmt-in/out, is seeded here from the host management allowlist (--mgmt-cidrs). Every other set's membership — includingbn-restricted— is reconciled at runtime by the traffic-shaper daemon from the block node's statusz;bn-restrictedin particular reflects a "restricted" category the block node itself reports, so it always starts empty and has no install-time flag. A permanent, purely operator-managed block list lives instead on the host firewall (--blocked-cidrs, a different table). Re-running the install never clobbers operator-applied set membership or per-class shape values;--forcere-renders the static rules from these definitions.
Traffic-shaper daemon: a block node without its traffic-shaper daemon has no ingress prioritization (the
$VETHHTB is never installed) and nothing reconciling the daemon-owned nft sets from statusz. At the end of a successful install,block node installinstalls and provisions the daemon for this block node's namespace automatically whenever traffic shaping is enabled (see the traffic-shaping gate above) — no separate prompt or flag. If the daemon is already active, this is a no-op.The daemon binary itself is resolved automatically, in this order:
--daemon-bin, thenSOLO_PROVISIONER_DAEMON_BIN, then a daemon binary already installed in the weaver bin directory, then a download from the infrastructure catalog at--daemon-version(defaulting to this CLI's own version). Locally-built CLIs carry a placeholder version (0.0.0) with no release to download, butsudo solo-provisioner installcopies the co-builtsolo-provisioner-daemonfrom the samebin/directory onto the host, so atask buildfollowed by a self-install leaves a matching daemon already in place and nothing needs to be supplied. When every source comes up empty the command fails with a resolution hint listing the ways to supply a binary — it never prompts for a path.
Upgrade an existing Block Node deployment:
# Upgrade with new values file
sudo solo-provisioner block node upgrade \
--profile=mainnet \
--values=/path/to/new-values.yaml
# Upgrade to specific chart version
sudo solo-provisioner block node upgrade \
--profile=mainnet \
--chart-version=0.23.0
# Upgrade and reset to chart defaults (don't reuse previous values)
sudo solo-provisioner block node upgrade \
--profile=mainnet \
--values=/path/to/values.yaml \
--no-reuse-valuesAdditional Flags:
| Flag | Description | Default |
|---|---|---|
--no-reuse-values |
Don't reuse previous release values | false |
--with-reset |
Wipe block node data directories; PVs and PVCs are preserved | false |
--timeout |
Timeout for the block node Helm install/upgrade, as a Go duration (e.g. 10m, 600s, 1h). The operation is rolled back (--atomic) if it exceeds this budget |
5m0s |
Firewall & traffic shaping on upgrade:
upgradedoes not expose the--firewall-enabled/--traffic-shaping-enabledgates and never prompts for them — a version bump is a routine operation, so it reads the persisted install decision and silently re-asserts the matching network plane (host firewall, BN policy plane, tc shaping, daemon monitor) create-if-missing. It never turns a feature on or off from an upgrade; to change enablement, usereconfigure.
Reset Block Node storage by clearing all data files. This is useful for re-provisioning or when you need to start fresh:
# Basic reset - clears all storage directories
sudo solo-provisioner block node reset --profile=mainnet
# Reset with custom config
sudo solo-provisioner block node reset \
--profile=mainnet \
--config=/path/to/config.yamlWhat it does:
- Scales down the Block Node StatefulSet to 0 replicas
- Waits for all pods to terminate
- Clears all storage directories (archive, live, log, plus the version-specific optional storages:
verificationon chart versions below 0.37.0,application-stateon chart versions 0.37.0 and above, andpluginson chart versions 0.28.1 and above) - Scales the StatefulSet back up to 1 replica
- Waits for pods to become ready
Warning: This command will delete all block data. Use with caution in production environments.
Upgrade with Reset:
If you need to upgrade and reset storage in one operation, use the upgrade command with --with-reset:
# Upgrade chart version and reset storage
sudo solo-provisioner block node upgrade \
--profile=mainnet \
--chart-version=0.24.0 \
--with-reset
# Upgrade with new values and reset
sudo solo-provisioner block node upgrade \
--profile=mainnet \
--values=/path/to/new-values.yaml \
--with-resetRe-apply configuration to an existing Block Node deployment without changing its chart version:
# Reconfigure with updated values file
sudo solo-provisioner block node reconfigure \
--profile=mainnet \
--values=/path/to/updated-values.yaml
# Reconfigure without reusing previous Helm values
sudo solo-provisioner block node reconfigure \
--profile=mainnet \
--values=/path/to/values.yaml \
--no-reuse-values
# Reconfigure and skip the pod rollout-restart
sudo solo-provisioner block node reconfigure \
--profile=mainnet \
--values=/path/to/values.yaml \
--no-restart
# Enable traffic shaping on a block node that was installed without it
sudo solo-provisioner block node reconfigure \
--profile=mainnet \
--traffic-shaping-enabled=true
# Disable the host firewall that was previously enabled (tears the table down)
sudo solo-provisioner block node reconfigure \
--profile=mainnet \
--firewall-enabled=false
# Point the daemon's statusz monitor at a port-forwarded BN and slow its poll
sudo solo-provisioner block node reconfigure \
--profile=mainnet \
--statusz-base-url=http://127.0.0.1:8080 \
--statusz-poll-interval=10sAdditional Flags:
| Flag | Description | Default |
|---|---|---|
--no-reuse-values |
Don't reuse previous release values | false |
--no-restart |
Skip rollout-restart of the block node pod after reconfiguring | false |
--with-reset |
Wipe block node data directories; PVs and PVCs are preserved | false |
--purge-storage |
Delete PersistentVolumes and PersistentVolumeClaims in addition to wiping data (implies --with-reset) | false |
--firewall-enabled |
Enable or disable the node-level host firewall (inet weaver-host-firewall table) on an existing install. Seeded from the firewall's current on-host state, so a no-flag reconfigure keeps it as-is; pass =false to tear the table down, =true (with --mgmt-cidrs) to create it. Same sub-flags as install (--mgmt-cidrs, --blocked-cidrs, --ssh-port, --pod-cidr, --in-cluster-ports). |
current state |
--traffic-shaping-enabled |
Enable or disable the BN traffic-shaping bundle (network-policy plane + tc HTB shaping + daemon traffic-shaper monitor) on an existing install. Seeded from the persisted install decision, so a no-flag reconfigure keeps it; pass =true to create it (with --egress-interface/--link-rate/--shape/--daemon-bin as on install), =false to tear it down. |
persisted state |
--statusz-base-url |
Override the daemon's block-node statusz endpoint with an explicit http(s) base URL (e.g. http://127.0.0.1:8080) for a port-forward or directly-reachable BN. Merged per-field into daemon.yaml (components.block_node.statusz.base_url); omitting the flag preserves whatever is already on disk. Only when no base_url exists on disk does the daemon fall back to discovering the endpoint from the watched BN pod. |
preserved on disk |
--statusz-poll-interval |
Cadence at which the daemon's block-node traffic-shaper monitor polls statusz, as a positive Go duration (e.g. 5s, 30s). Merged per-field into daemon.yaml (components.block_node.statusz.poll_interval); omitting the flag preserves whatever is already on disk. Only when no poll_interval exists on disk does the daemon fall back to its 5s default. |
preserved on disk |
Storage path changes: Local PV
hostPath.pathis immutable. If your reconfigure changes any storage path, you must pass--purge-storageso the existing PV/PVCs are deleted and recreated at the new paths. Runningreconfigure --with-resetwith a path change is rejected with a clear error.
Enabling/disabling firewall & traffic shaping after install:
reconfigureexposes the same--firewall-enabled/--traffic-shaping-enabledgates asinstall, so an operator can turn either feature on or off on an already-deployed block node without a fullinstall --forcereinstall. Both gates are seeded from the block node's current state — the host firewall from whether theinet weaver-host-firewalltable exists, traffic shaping from the persisted install decision — so a routine reconfigure that doesn't pass the flag (or accepts the interactive default) never changes enablement. Teardown only happens on an explicit toggle: answering No (or passing=false) for a currently-enabled feature removes it — theinet weaver-host-firewalltable for the firewall; everybn-*policy, the tc egress shaping, and the daemon's block-node traffic-shaper monitor for traffic shaping. Disabling the traffic-shaper monitor does not uninstall the shared daemon service, so a co-located component (e.g. consensus-node monitoring) keeps running. Enabling traffic shaping activates the daemon automatically, just likeinstall.
block node uninstall has three variants depending on what you want to keep:
| Command | Helm release | Data on disk | PV/PVC objects |
|---|---|---|---|
block node uninstall |
removed | kept | kept |
block node uninstall --with-reset |
removed | wiped | kept |
block node uninstall --purge-storage |
removed | wiped | deleted |
# Basic uninstall — release removed, data and PV/PVCs preserved for a future re-install
sudo solo-provisioner block node uninstall --profile=mainnet
# Wipe data but keep PV/PVCs so a re-install can reuse them
sudo solo-provisioner block node uninstall \
--profile=mainnet \
--with-reset
# Fully clean up — release, data, PVCs, and PVs all removed
sudo solo-provisioner block node uninstall \
--profile=mainnet \
--purge-storageAdditional Flags:
| Flag | Description | Default |
|---|---|---|
--with-reset |
Wipe block node data directories; PVs and PVCs are preserved | false |
--purge-storage |
Delete PersistentVolumes and PersistentVolumeClaims in addition to wiping data (implies --with-reset) | false |
Picking the right one: use the default uninstall if you plan to re-install against the same data. Use
--with-resetto start fresh on disk but keep the PV/PVC topology. Use--purge-storagefor a full cleanup; this is the only targeted way to remove the block-node PVs without tearing down the whole cluster viakube cluster uninstall.
Reconcile the block node traffic-shaper's inet weaver-workload-policy nft policy set membership
from the block node's statusz endpoints. The command fetches the active
inbound/outbound endpoints, maps them to the owned policy sets
(bn-publisher, bn-partner, bn-restricted, bn-backfill), and applies only
the policies whose membership changed.
# Detect only (unprivileged): print a digest of the desired membership; touches no nft state
solo-provisioner block node reconcile-shaper \
--statusz-url=http://10.0.0.5:8080 \
--check
# Apply (root): read live nft, diff, and rewrite the changed policies
sudo solo-provisioner block node reconcile-shaper \
--statusz-url=http://10.0.0.5:8080
# Machine-readable output
sudo solo-provisioner block node reconcile-shaper \
--statusz-url=http://10.0.0.5:8080 \
--output=jsonAdditional Flags:
| Flag | Description | Default |
|---|---|---|
--statusz-url |
Base URL of the block node's statusz endpoints (required) | — |
--check |
Only fetch and print the desired-membership digest; read/write no nft state (unprivileged) | false |
Privilege:
--checknever touches nft and needs no privilege. The apply path (no--check) reads and writes the liveinet weaver-workload-policysets and must run as root; the daemon invokes it via sudo.
Install the $VETH ingress HTB hierarchy (classes 1:10/1:20/1:30, default
reserve-ingress, fq_codel leaves, no tc filters) on a block node pod's
host-side veth, using the per-class ingress budgets recorded by
network shape. The solo-provisioner-daemon's pod-lifecycle watcher runs this
via sudo on each block node pod create, passing the veth it resolved for the
pod; you can also run it by hand for debugging.
# Install the ingress HTB hierarchy on a resolved host-side veth (root)
sudo solo-provisioner block node tc-attach --veth lxc1a2b3c
# Tear it down (best-effort; the kernel also removes it when the pod's veth disappears)
sudo solo-provisioner block node tc-attach --veth lxc1a2b3c --detachAdditional Flags:
| Flag | Description | Default |
|---|---|---|
--veth |
Host-side veth interface to (de)install the ingress HTB hierarchy on, e.g. lxc1a2b3c (required) |
— |
--detach |
Tear down the ingress HTB hierarchy on the veth instead of installing it (best-effort) | false |
Privilege:
tc-attachalways mutates live kernel tc state and must run as root (the daemon invokes it via sudo). Unlikereconcile-shaper, it has no unprivileged--checkmode. It requires the ingress shape to have been recorded first (normally byblock node installvianetwork shape).
Manage the underlying Kubernetes cluster and its components.
Sets up a complete single-node Kubernetes environment with all required components:
Components Installed:
- kubeadm/kubelet: Kubernetes cluster initialization and node agent
- CRI-O: Container runtime
- Cilium: Container networking (CNI)
- MetalLB: Load balancer for bare-metal Kubernetes
- Helm: Kubernetes package manager
- kubectl: Kubernetes CLI
- k9s: Terminal-based Kubernetes UI
- Metrics Server: Resource metrics for pods and nodes
kube cluster install provisions a Kubernetes cluster independent of any specific node type — it does not apply any node-specific firewall rules. The node-level host firewall (the inet weaver-host-firewall nftables table) is applied by the block-node workflow instead (see Install Block Node below).
Cluster install is workload-agnostic: it validates only the Kubernetes substrate
hardware floor (what Kubernetes itself needs to run), not any per-workload sizing. It
therefore takes no --profile or --node-type. Workload-sized hardware validation
happens later, at block node check / block node install, where the deployment
profile and plugin preset are known.
# Install the full Kubernetes stack
sudo solo-provisioner kube cluster install
# With error handling
sudo solo-provisioner kube cluster install --rollback-on-errorFlags:
| Flag | Short | Description | Required |
|---|---|---|---|
--rollback-on-error |
— | Roll back completed steps if a later step fails | No |
--stop-on-error |
— | Stop at the first failing step (default) | No |
--continue-on-error |
— | Continue past failing steps | No |
Deprecated:
--profileand--node-typeare no longer used bykube cluster install. They are still accepted (hidden) so existing scripts do not break, but their values are ignored and a notice is printed if you pass them. Remove them from your invocations.
Tears down the entire Kubernetes stack including all components (kubeadm, CRI-O, Cilium, etc.) while preserving the downloads cache:
# Basic uninstall
sudo solo-provisioner kube cluster uninstall
# Continue even if some steps fail
sudo solo-provisioner kube cluster uninstall --continue-on-errorWarning: This tears down the entire cluster. All running workloads will be stopped.
Manage node-level network state behind the traffic shaper. The firewall scope manages the node-agnostic inet weaver-host-firewall nftables table — the host's own SSH/management allowlist, ICMP policy, and in-cluster host-service ports. It is separate from the inet weaver-workload-policy workload plane and applies to every node type (block, consensus, mirror, relay).
create-if-missing: if the inet weaver-host-firewall table already exists, the command makes no changes unless --force is passed (which re-renders from the flags). Every mutation applies to the live kernel in one atomic nft -f transaction and atomically rewrites /etc/solo-provisioner/network-weaver-host-firewall.nft.
# Create with a management allowlist and the default in-cluster ports
sudo solo-provisioner network firewall create \
--mgmt-cidrs 10.0.0.0/8 \
--ssh-port 22 \
--pod-cidr 10.4.0.0/24 \
--in-cluster-ports 6443,4244,10250
# Re-render an existing table from new flags
sudo solo-provisioner network firewall create --mgmt-cidrs 10.0.0.0/8,192.168.0.0/16 --forceFlags:
| Flag | Description | Default |
|---|---|---|
--mgmt-cidrs |
Management/SSH allowlist CIDRs (comma-separated or repeated) — omitting this flag leaves the SSH allow rule with an empty source set under the default-drop policy, which will lock you out of new SSH connections | (none) |
--in-cluster-ports |
Host-service ports reachable from the pod CIDR | 4244,6443,7472,10250 |
--ssh-port |
SSH/management TCP port accepted from the allowlist | 22 |
--pod-cidr |
Pod CIDR allowed to reach the in-cluster host-service ports | auto-detected |
--force |
Re-render the table even if it already exists (global flag) | false |
When --pod-cidr is omitted it is auto-detected from the local node's .spec.podCIDR via the Kubernetes API (the node is matched by hostname, or the sole node on a single-node host). Detection is best-effort: network firewall create is node-agnostic and may run before a cluster exists, so if no cluster is reachable the command logs a warning and omits the in-cluster-ports rule — pass --pod-cidr explicitly to render it anyway.
ICMP is a fixed, safe ruleset (not configurable): full ICMP from the management allowlist, and from every other source the path-health subset — destination-unreachable (Path MTU Discovery) and time-exceeded (traceroute) always accepted, with echo-request (ping) rate-limited to 10/second. There are deliberately no ICMP flags: dropping ICMP errors would silently break PMTUD for legitimate clients.
There is no
--service-ports: BN ports live only innetwork policy --ports(the host firewall is bypassed by the eBPF datapath).
add/remove operate on a single element; set atomically replaces the full list.
sudo solo-provisioner network firewall add --mgmt-cidr 10.1.0.0/16
sudo solo-provisioner network firewall remove --mgmt-cidr 10.0.0.0/8
sudo solo-provisioner network firewall set --mgmt-cidrs 10.0.0.0/8,192.168.0.0/16
sudo solo-provisioner network firewall add --in-cluster-port 9100
sudo solo-provisioner network firewall remove --in-cluster-port 10250
sudo solo-provisioner network firewall set --in-cluster-ports 6443,4244Flags:
| Verb | Flag | Description |
|---|---|---|
add/remove |
--mgmt-cidr |
A single management CIDR (mutually exclusive with --in-cluster-port) |
add/remove |
--in-cluster-port |
A single in-cluster host-service port |
set |
--mgmt-cidrs |
Full management allowlist (replaces the existing list) |
set |
--in-cluster-ports |
Full in-cluster host-service port list (replaces the existing list) |
# Show the live inet weaver-host-firewall table
sudo solo-provisioner network firewall show
# Remove the table and /etc/solo-provisioner/network-weaver-host-firewall.nft
sudo solo-provisioner network firewall delete
deleteremoves the table and/etc/solo-provisioner/network-weaver-host-firewall.nftbut does not disable the sharedsolo-provisioner-network-nft.service(shared withinet weaver-workload-policy); disable it manually if you need it off.
The policy scope is a generic, category-agnostic primitive that manages the inet weaver-workload-policy workload traffic plane: named per-category rules that classify traffic into an HTB priority class, or quarantine a set of CIDRs. It is not tied to any specific node type — the CLI takes CIDRs and class names directly (statusz-agnostic); the examples below use the block-node categories because block node install is the only caller today. Each create renders the rule(s) into the inet weaver-workload-policy forward chain, ensures the policy's nft set @<name> exists, writes a per-policy registry file under /etc/solo-provisioner/policies/, applies the full chain to the live kernel with nft -f, and atomically rewrites /etc/solo-provisioner/network-weaver-workload-policy.nft.
create is create-if-missing, mirroring network firewall create: a policy that already exists is left untouched unless --force is passed, in which case its config and membership are replaced (not merged) from the given flags/--cidrs. Without --force, an existing policy warns and makes no changes — even if the flags/--cidrs given this time differ from before.
Specify exactly one action: --stamp <class> (classify into an HTB priority class) or --deny (drop the CIDRs both directions). There is no --direction flag — every class has exactly one direction (see the class list below), so --stamp <class> determines it.
# Publisher: highest-priority ingress class on the publisher listener port
sudo solo-provisioner network policy create --name bn-publisher \
--ports 40840 --stamp publisher
# Subscriber ingress from any source (no IP-set clause): reserve class
sudo solo-provisioner network policy create --name bn-subscriber-in \
--ports 40980,40981 --stamp reserve-ingress --from-entity world
# Partner egress (specific destinations), curated CIDR list
sudo solo-provisioner network policy create --name bn-partner-out \
--ports 40980,40981 --stamp partner --cidrs 10.20.0.0/16
# Backfill egress with an asymmetric reply class (conntrack reply gets higher priority)
sudo solo-provisioner network policy create --name bn-backfill \
--stamp reserve-egress --reply-stamp backfill-response \
--cidrs 10.30.5.7:43473
# Quarantine: drop all traffic to/from a set of CIDRs, both directions
sudo solo-provisioner network policy create --name bn-restricted \
--deny --cidrs 10.99.0.0/16Flags:
| Flag | Description | Default |
|---|---|---|
--name |
Policy name; also the nft set name @<name> (required) |
(none) |
--stamp |
HTB class to classify matching packets into; also fixes the policy's direction (mutually exclusive with --deny) |
(none) |
--deny |
Drop the --cidrs in both directions (mutually exclusive with --stamp) |
false |
--reply-stamp |
Reply class for an asymmetric conntrack reply (requires --stamp to resolve to an egress class; --reply-stamp must resolve to the mirror ingress class) |
(none) |
--from-entity |
world — match any source/dest with no IP-set clause (mutually exclusive with --cidrs) |
(none) |
--ports |
Workload listener ports for the match key (comma-separated or repeated) | (none) |
--cidrs |
Initial set membership (comma-separated or repeated); ip:port entries for --reply-stamp policies |
(none) |
--cidrs-file |
Alternative to --cidrs: a file of CIDRs (one per line or comma-separated) |
(none) |
--pod-cidr |
Pod CIDR to scope classification to | auto-detected |
--force |
Replace an existing policy's config and membership (root flag, -y); without it, an existing policy is left untouched |
false |
--stamp references a QoS class name — publisher, reserve-ingress (ingress); partner, public, reserve-egress (egress); backfill-response (ingress, --reply-stamp only) — referencing an unknown class is an error. Rule position in the chain is determined by action type and match specificity (deny → reply-restore → specific stamp → fallthrough stamp), never by creation order.
When --pod-cidr is omitted it is auto-detected from the local node's .spec.podCIDR via the Kubernetes API — but only for --stamp policies; --deny never references POD_CIDR (it just drops on set membership), so detection is skipped entirely for it. Unlike network firewall create, a --stamp policy's detection failure with no --pod-cidr is a hard error, not a warning-and-continue. If a --deny create's merged chain still includes a --stamp sibling that needs POD_CIDR, the value is recovered from the existing /etc/solo-provisioner/network-weaver-workload-policy.nft instead of being required again — it's a deployment-wide constant, not a per-call argument.
Set membership (the CIDRs) is never persisted to
network-weaver-workload-policy.nft— statusz is the source of truth and the daemon reconciles it.--cidrsseeds the live set only, and only takes effect on a brand-new policy or a--forcere-create (which replaces membership with exactly what's passed, not a merge with what was live before).
Use these verbs to modify a policy's live CIDR set after it has been created. None of them re-render network-weaver-workload-policy.nft — only the live kernel set changes (§8.3.1).
# Add one or more CIDRs to a policy's live set (repeatable or comma-separated)
sudo solo-provisioner network policy add --name bn-publisher --cidr 10.1.0.1/32
sudo solo-provisioner network policy add --name bn-publisher --cidr 10.1.0.2/32,10.1.0.3/32
# Remove one or more CIDRs from a policy's live set
sudo solo-provisioner network policy remove --name bn-publisher --cidr 10.1.0.1/32
# Atomically replace the full membership list (flush + re-add in one kernel transaction)
sudo solo-provisioner network policy set --name bn-publisher --cidrs 10.2.0.0/16
# Or clear the set entirely (omit --cidrs):
sudo solo-provisioner network policy set --name bn-publisheradd / remove flags (use --cidr for each CIDR; comma-separated lists are also accepted):
| Flag | Description | Required |
|---|---|---|
--name |
Policy name | yes |
--cidr |
CIDR to add or remove (repeatable or comma-separated) | yes |
set flags:
| Flag | Description | Required |
|---|---|---|
--name |
Policy name | yes |
--cidrs |
Replacement membership (comma-separated or repeated); omit to clear | no |
--cidrs-file |
Alternative to --cidrs: a file of CIDRs (one per line or comma-separated) |
no |
For --reply-stamp policies the CIDR entries must be ip:port pairs for all three verbs, same as create --cidrs.
Print a policy's registry config and current live set membership. Without --name, show lists all configured policies (sorted by name); with --name, it prints just that one. This mirrors network shape show, where a bare show lists everything and flags narrow the scope.
# List every configured policy
sudo solo-provisioner network policy show
# Inspect a single policy
sudo solo-provisioner network policy show --name bn-publisherOutput example (single policy). direction leads, and the live set is nested under the policy:
policy: bn-publisher
direction: ingress
action: stamp
class: publisher
ports: 40840
created: 2026-01-01T00:00:00Z
live set @bn-publisher:
10.1.0.1/32
10.1.0.2/32
With no policies configured, a bare show prints no policies configured rather than failing.
| Flag | Description | Required |
|---|---|---|
--name |
Policy name (omit to list all policies) | no |
Remove a policy's rules, set, and registry file, and re-render the inet weaver-workload-policy chain:
sudo solo-provisioner network policy delete --name bn-restricteddelete re-renders the full chain without the removed policy, snapshots and restores remaining policies' live membership (so the destructive delete table; add table does not wipe their sets), removes the registry file, and atomically overwrites network-weaver-workload-policy.nft. If this is the last policy, the table is torn down entirely (live and on disk); the boot oneshot stays enabled.
| Flag | Description | Required |
|---|---|---|
--name |
Policy name | yes |
Manage the tc HTB shaping hierarchy for the node's egress NIC. Each create/set/delete mutation atomically re-renders /usr/local/sbin/solo-provisioner-bandwidth-shaper.sh and restarts solo-provisioner-bandwidth-shaper.service so the live kernel and boot script stay in sync.
block node install drives the shape registry automatically (from --link-rate); network shape lets you inspect or adjust individual classes after install.
Create device root
# Explicit trunk rate: written into the boot script as concrete tc values; no sysfs detection.
sudo solo-provisioner network shape create \
--device egress --rate 1gbit --default reserve-egress
# Auto-detect trunk rate NOW from sysfs (/sys/class/net/<NIC>/speed) and store
# the resolved value (e.g. 1gbit) as an explicit rate. Unreadable speed → 1gbit.
sudo solo-provisioner network shape create \
--device egress --rate auto --default reserve-egress
# Replace an existing device config.
sudo solo-provisioner network shape create \
--device egress --rate 1gbit --default reserve-egress --force--rate auto reads the detected NIC's link speed from sysfs at create time — while the link is up and stable — and stores the resolved value (e.g. 1gbit) as an ordinary explicit rate. network shape show then reports the concrete number and the boot script carries explicit values, with no SPEED variable and no sysfs read at boot. If the speed is not readable then (e.g. a virtual NIC reporting -1), auto falls back to a concrete 1gbit (1000 Mbit) — still explicit and SPEED-free, not a dynamic script. Either way, --rate auto always produces a concrete stored rate; the sysfs-at-boot form only appears when no shape device is configured at all (e.g. block node install run without --link-rate in non-interactive mode).
Until you add the first --class, the device root renders a placeholder hierarchy (the three default egress classes at proportional rates: partner 40%/70%, public 30%/70%, reserve-egress 30%/100% of the trunk) at the resolved concrete rate. Adding explicit --class configs replaces the placeholder.
Create / replace HTB leaf classes
sudo solo-provisioner network shape create --class partner --rate 400mbit --ceil 700mbit --prio 0
sudo solo-provisioner network shape create --class public --rate 300mbit --ceil 700mbit --prio 5
sudo solo-provisioner network shape create --class reserve-egress --rate 300mbit --ceil 1000mbit --prio 1Once all three egress class configs are present, the boot script switches to fully explicit rates (no SPEED variable at all).
Live update (no qdisc teardown)
sudo solo-provisioner network shape set --class partner --rate 500mbit
sudo solo-provisioner network shape set --class public --ceil 600mbitset runs tc class change on the live kernel and re-renders the boot script immediately.
Show
sudo solo-provisioner network shape show # all devices and classes
sudo solo-provisioner network shape show --class partner # single classshow reports the stored configuration (rate/ceil/prio). To see live traffic flowing through the classes, use watch.
Watch (live counters, read-only)
# Watch the egress NIC every 2s — both --device and --iface are required.
# Runs until Ctrl-C.
sudo solo-provisioner network shape watch --device egress --iface enp0s1
# Narrow to a single class, faster sampling, bounded to 5 samples then exit.
sudo solo-provisioner network shape watch --device egress --iface enp0s1 --class partner --interval 1s --count 5
# Watch a block node's ingress veth (find it with `ip link` or `tc qdisc show`).
sudo solo-provisioner network shape watch --device ingress --iface lxc1a2b3cwatch samples tc -s class show dev <iface> at --interval and prints, per class, the throughput (from the byte delta) plus the change in overlimits and drops since the previous sample — "rate over time". It is read-only: it never mutates tc or the shape registry. Use it to confirm traffic is actually being classified and shaped — e.g. partner traffic landing in 1:40 with a non-zero rate and climbing overlimits. Both --device (egress or ingress — selects the class set) and --iface (the interface to sample) are required: the command does no environment probing — no NIC or veth auto-detection — so it stays independent of any running block node. For egress, --iface is the physical NIC (e.g. enp0s1); for ingress, the per-pod host veth (e.g. lxc1a2b3c), which you can find with ip link or tc qdisc show. --class narrows the output to one class within --device. This complements the Prometheus counters, which target dashboards rather than an operator at a terminal.
Delete
# Fails if the class is the device default or referenced by a policy --stamp.
sudo solo-provisioner network shape delete --class reserve-egress| Flag | Description | Required |
|---|---|---|
--device |
Traffic direction: egress or ingress |
one of --device / --class |
--class |
HTB class name (partner, public, reserve-egress, …) |
one of --device / --class |
--rate |
Bandwidth rate (100mbit, 1gbit) or "auto" (sysfs, --device form only) |
yes (create/set) |
--ceil |
Burst ceiling ≥ --rate; defaults to --rate when omitted |
no |
--prio |
HTB scheduling priority [0,7]; 0 is highest |
no (default 0) |
--default |
Default class for unmatched traffic (--device form only) |
yes (--device) |
--force |
Replace an existing device or class config | no |
--iface |
Interface to sample (watch); required — e.g. enp0s1 (egress) or lxc1a2b3c (ingress veth). No auto-detection |
watch |
--interval |
Sampling interval for watch (e.g. 1s, 500ms); default 2s |
no |
--count |
Number of watch samples to print then exit; 0 = run until interrupted |
no |
Configure secure access using Teleport agents.
Install the Teleport node agent for secure SSH access to the host:
# Install with required token and proxy address
sudo solo-provisioner teleport node install \
--token=<join-token> \
--proxy=proxy.teleport.example.com:443
# With error handling
sudo solo-provisioner teleport node install \
--token=<join-token> \
--proxy=proxy.teleport.example.com \
--stop-on-errorRequired Flags:
| Flag | Description |
|---|---|
--token |
Join token for Teleport agent |
--proxy |
Teleport proxy address (host:port) |
Install the Teleport Kubernetes cluster agent for secure kubectl access:
# Install with values file
sudo solo-provisioner teleport cluster install \
--values=/path/to/teleport-values.yaml
# With specific version
sudo solo-provisioner teleport cluster install \
--values=/path/to/teleport-values.yaml \
--version=16.0.0Required Flags:
| Flag | Description |
|---|---|
--values |
Path to Teleport Helm values file |
Optional Flags:
| Flag | Description |
|---|---|
--version |
Teleport Helm chart version |
Remove the Teleport node agent, stopping the systemd service and cleaning up binaries and configuration:
sudo solo-provisioner teleport node uninstallRemove the Teleport Kubernetes cluster agent Helm release:
sudo solo-provisioner teleport cluster uninstallManage the External Secrets Operator (ESO), which syncs secrets from external stores into Kubernetes. Installing ESO is the prerequisite for syncing secrets used by other components (e.g. the grafana-alloy-secrets Secret consumed by alloy cluster install).
| Prerequisite | Description |
|---|---|
| Root privileges | The install command requires sudo |
| Reachable K8s cluster | The cluster must be reachable via the admin kubeconfig |
Install the external-secrets/external-secrets Helm chart into the cluster. The command is idempotent: if ESO is already installed in the target namespace, installation is skipped with a clear message. The chart version is pinned by the infrastructure catalog.
# Install with defaults (namespace: external-secrets)
sudo solo-provisioner eso operator install
# Install into a custom namespace
sudo solo-provisioner eso operator install --namespace my-esoAdditional Flags:
| Flag | Default | Description |
|---|---|---|
--namespace |
external-secrets |
Kubernetes namespace for the External Secrets Operator |
Uninstall the ESO Helm release. The command is idempotent: if ESO is not installed in the target namespace, uninstallation is skipped with a clear message.
Warning: uninstalling ESO removes its cluster-scoped CRDs, which deletes every
ExternalSecret/SecretStoreresource in the cluster (and the Kubernetes Secrets they sync). Do not run this while other components still rely on synced secrets.
# Uninstall from the default namespace (external-secrets)
sudo solo-provisioner eso operator uninstall
# Uninstall from a custom namespace
sudo solo-provisioner eso operator uninstall --namespace my-esoAdditional Flags:
| Flag | Default | Description |
|---|---|---|
--namespace |
external-secrets |
Kubernetes namespace for the External Secrets Operator |
Create (or update) an ExternalSecret that ESO reconciles into a Kubernetes Secret. The command uses server-side apply, so re-running it with the same --name/--namespace updates the existing ExternalSecret in place.
Prerequisite: a
ClusterSecretStorenamed by--storeand the target--namespacemust both already exist in the cluster (the store points to your secrets backend — Vault, AWS, GCP, …). Setting up theClusterSecretStoreis cluster-operator configuration and is out of scope for this command.
sudo solo-provisioner eso secret create \
--store=vault-store \
--name=grafana-alloy-secrets \
--namespace=grafana-alloy \
--set PROMETHEUS_PASSWORD_PRIMARY=secret/data/grafana/alloy/prod/prometheus/primary#password \
--set LOKI_PASSWORD_PRIMARY=secret/data/grafana/alloy/prod/loki/primary#passwordAdditional Flags:
| Flag | Default | Description |
|---|---|---|
--store |
— | Required. Name of the ClusterSecretStore resource to sync from |
--name |
— | Required. Name of the resulting Kubernetes Secret (and the ExternalSecret) |
--namespace |
— | Required. Namespace for both the ExternalSecret and the Kubernetes Secret |
--set |
— | Repeatable. Map a Secret key to a store path: KEY=store/path[#field]. At least one required |
--refresh-interval |
1h |
How often ESO re-syncs the secret from the store |
Manage Grafana Alloy observability stack for metrics and logs.
When installing Alloy with remote endpoints (--add-prometheus-remote or --add-loki-remote), ensure the following
prerequisites are met:
| Prerequisite | Description |
|---|---|
| Running Kubernetes Cluster | A cluster must be set up via solo-provisioner block node install or solo-provisioner kube cluster install |
| K8s Secret | A Kubernetes Secret named grafana-alloy-secrets must exist in the grafana-alloy namespace with password keys for each configured remote (e.g., PROMETHEUS_PASSWORD_PRIMARY, LOKI_PASSWORD_PRIMARY) |
| Reachable Remote Endpoints | Prometheus/Loki URLs must be reachable from the cluster |
| Block Node (optional) | If using --monitor-block-node, the block node must be installed first |
Note: Without
--add-prometheus-remoteor--add-loki-remoteflags, Alloy installs without remotes and does not require the K8s secret.
# Step 1: Create the K8s secret with passwords for remote endpoints
kubectl create namespace grafana-alloy
kubectl create secret generic grafana-alloy-secrets \
--namespace=grafana-alloy \
--from-literal=PROMETHEUS_PASSWORD_PRIMARY=<password> \
--from-literal=LOKI_PASSWORD_PRIMARY=<password>
# Step 2: Install Alloy with remotes
sudo solo-provisioner alloy cluster install \
--cluster-name=mainnet-block-01 \
--profile=mainnet \
--add-prometheus-remote=name=primary,url=https://prom1.example.com/api/v1/write,username=user1 \
--add-loki-remote=name=primary,url=https://loki1.example.com/loki/api/v1/push,username=user1 \
--monitor-block-node
# Install Alloy without remotes (no secret needed)
sudo solo-provisioner alloy cluster install \
--cluster-name=mainnet-block-01Available Flags:
| Flag | Description |
|---|---|
--cluster-name |
Cluster name for metrics/logs labels |
--profile, -p |
Deployment profile (local, perfnet, testnet, previewnet, mainnet); sets the environment label for the ops profile. Optional; if omitted, the value from --config is used |
--monitor-block-node |
Enable Block Node specific monitoring |
--add-prometheus-remote |
Add a Prometheus remote (format: name=<name>,url=<url>,username=<username>[,labelProfile=eng|ops]). Repeatable. Default: eng |
--add-loki-remote |
Add a Loki remote (format: name=<name>,url=<url>,username=<username>[,labelProfile=eng|ops]). Repeatable. Default: eng |
--prometheus-url |
Prometheus remote write URL (deprecated: use --add-prometheus-remote) |
--prometheus-username |
Prometheus authentication username (deprecated) |
--loki-url |
Loki remote write URL (deprecated: use --add-loki-remote) |
--loki-username |
Loki authentication username (deprecated) |
--stop-on-error |
Stop execution on first error (default behavior when no execution-mode flag is set) |
--rollback-on-error |
Rollback executed steps on error |
--continue-on-error |
Continue executing steps even if some steps fail |
Note:
--stop-on-error,--rollback-on-error, and--continue-on-errorare mutually exclusive and apply to bothalloy cluster installandalloy cluster uninstall. When Alloy fails to install because the Kubernetes cluster is not reachable, install the cluster first withsolo-provisioner kube cluster install— Alloy deploys into an existing cluster and does not create one.
Note: Passwords must be pre-created in a K8s Secret named
grafana-alloy-secretsin thegrafana-alloynamespace. The secret can be created manually, via ESO, Terraform, or any other mechanism.
The --add-prometheus-remote and --add-loki-remote flags use the format
name=<name>,url=<url>,username=<username>[,labelProfile=<profile>]:
- name: Unique identifier for the remote (e.g.,
primary,backup,grafana-cloud) - url: The remote write endpoint URL
- username: Authentication username (password is read from the K8s Secret)
- labelProfile (optional): Label profile to auto-inject additional labels (default:
eng, which adds onlycluster). See Label Profiles below
K8s Secret Keys (for multiple remotes):
Each remote requires a corresponding password key in the grafana-alloy-secrets K8s Secret. The key name is derived
from the remote type and name:
- Prometheus:
PROMETHEUS_PASSWORD_<REMOTE_NAME> - Loki:
LOKI_PASSWORD_<REMOTE_NAME>
Example for a cluster with primary and backup remotes, create the secret with:
kubectl create namespace grafana-alloy
kubectl create secret generic grafana-alloy-secrets \
--namespace=grafana-alloy \
--from-literal=PROMETHEUS_PASSWORD_PRIMARY=<password> \
--from-literal=PROMETHEUS_PASSWORD_BACKUP=<password> \
--from-literal=LOKI_PASSWORD_PRIMARY=<password> \
--from-literal=LOKI_PASSWORD_BACKUP=<password>The alloy cluster install command is declarative - it replaces the entire remote configuration with what you
specify. To manage endpoints:
Add a new remote: Include all existing remotes plus the new one:
# If you had 'primary', and want to add 'backup':
sudo solo-provisioner alloy cluster install \
--cluster-name=mainnet-block-01 \
--add-prometheus-remote=name=primary,url=https://prom1.example.com/api/v1/write,username=user1 \
--add-prometheus-remote=name=backup,url=https://prom2.example.com/api/v1/write,username=user2 \
--add-loki-remote=name=primary,url=https://loki1.example.com/loki/api/v1/push,username=user1Remove a remote: Simply omit it from the command:
# Remove 'backup', keep only 'primary':
sudo solo-provisioner alloy cluster install \
--cluster-name=mainnet-block-01 \
--add-prometheus-remote=name=primary,url=https://prom1.example.com/api/v1/write,username=user1 \
--add-loki-remote=name=primary,url=https://loki1.example.com/loki/api/v1/push,username=user1Modify a remote URL: Specify the same name with the new URL:
# Change 'primary' Prometheus URL:
sudo solo-provisioner alloy cluster install \
--cluster-name=mainnet-block-01 \
--add-prometheus-remote=name=primary,url=https://new-prom.example.com/api/v1/write,username=user1 \
--add-loki-remote=name=primary,url=https://loki1.example.com/loki/api/v1/push,username=user1Remove all remotes (install without remotes):
sudo solo-provisioner alloy cluster install \
--cluster-name=mainnet-block-01Important: Each run replaces the previous remote configuration. Always specify all the remotes you want to keep.
Label profiles auto-inject additional labels into every metric and log stream. The optional labelProfile key on any
remote activates a profile.
Available profiles:
| Profile | Labels Added |
|---|---|
eng (default) |
cluster |
ops |
cluster, environment, instance, instance_type, inventory_name, ip (optional) |
Example — install with the ops label profile:
sudo solo-provisioner alloy cluster install \
--cluster-name=lfh02-previewnet-blocknode \
--add-prometheus-remote=name=primary,url=https://prom.example.com/api/v1/write,username=user1,labelProfile=ops \
--add-loki-remote=name=primary,url=https://loki.example.com/loki/api/v1/push,username=user1,labelProfile=ops \
--monitor-block-nodeWith --cluster-name=lfh02-previewnet-blocknode and --profile=previewnet, the ops profile derives:
| Label | Value | Source |
|---|---|---|
cluster |
lfh02-previewnet-blocknode |
Always set (from --cluster-name) |
environment |
previewnet |
From --profile (deploy profile) |
instance |
lfh02-previewnet-blocknode |
Full cluster name; overrides the auto-scraped IP:port |
instance_type |
lfh |
Alphabetic prefix of the first segment of cluster name |
inventory_name |
lfh02-previewnet-blocknode |
Full cluster name |
ip |
<ip> |
Optional; set when an IP address label is available for the node |
Note: If
labelProfileis omitted for a given remote, that remote uses the defaultengprofile (only theclusterlabel). Each remote can specify its ownlabelProfile.
sudo solo-provisioner alloy cluster uninstallManage the solo-provisioner-daemon systemd service that coordinates consensus-node upgrade handoffs as well as other
automation requirements.
| Prerequisite | Description |
|---|---|
| Root privileges | All daemon service commands require sudo |
| Reachable K8s cluster | The cluster must be reachable via the admin kubeconfig |
Bootstrap daemon.yaml, provision K8s RBAC, generate the daemon kubeconfig, and
start the systemd service in one step.
# Interactive install — prompts for components, cn-node-id, and cn-orbit when daemon.yaml is absent
sudo solo-provisioner daemon service install
# Enable consensus-node only (non-interactive / CI)
sudo solo-provisioner daemon service install \
--components=consensus-node --cn-node-id=0.0.3 --cn-orbit=hedera-network
# Override the CN upgrade staging directory
sudo solo-provisioner daemon service install \
--components=consensus-node --cn-node-id=0.0.3 --cn-orbit=hedera-network \
--cn-upgrade-dir=/custom/path/data/upgrade/current
# Copy a pre-built daemon.yaml into place, then run the workflow
sudo solo-provisioner daemon service install --from-config=/path/to/daemon.yamlAdditional Flags
| Flag | Default | Description |
|---|---|---|
--components |
(prompted) | Comma-separated list of components to enable: consensus-node, block-node |
--cn-node-id |
(prompted) | Hedera node identifier for the consensus node (e.g. 0.0.3) |
--cn-orbit |
(prompted) | Kubernetes namespace (orbit) where consensus-node NetworkUpgradeExecute CRs are watched |
--cn-upgrade-dir |
/opt/hgcapp/services-hedera/HapiApp2.0/data/upgrade/current |
Path to the consensus-node upgrade staging directory |
--bn-orbit |
(prompted) | Kubernetes namespace (orbit) for the block-node component (supported in a future release) |
--from-config |
(none) | Path to an existing daemon.yaml to copy into /opt/solo/weaver/config/daemon.yaml |
Config bootstrap logic: If
daemon.yamlalready exists its values are used. Individual fields can still be overridden with the component-scoped flags above. In interactive mode the prompts are pre-filled with any existing values so pressing Enter accepts them unchanged.Adding or removing components: Run
daemon service uninstallfirst, then re-rundaemon service installwith the updated--componentslist. At least one component must be selected — RBAC and kubeconfigs are only provisioned for the chosen components.
sudo solo-provisioner daemon service uninstallPrints the full /status response (per-component monitor state, connectivity errors, and prerequisite
probe failures). status is an alias for check.
sudo solo-provisioner daemon service check
# or, equivalently:
sudo solo-provisioner daemon service statussudo solo-provisioner daemon service start
sudo solo-provisioner daemon service stopDrive the consensus-node migration soak watcher that runs inside solo-provisioner-daemon. These
commands talk to the running daemon over its Unix socket; the daemon must already be installed and
running (see Daemon Service Commands). Soak lifecycle lives under
consensus migration soak, separate from the daemon service tree (which is scoped to daemon lifecycle
only).
sudo solo-provisioner consensus migration soak start \
--node-id=0.0.3 \
--cutover-ts=2025-09-01T00:00:00Z \
--migration-plan=/path/to/migration-plan.yamlRequired Flags:
| Flag | Description |
|---|---|
--node-id |
Consensus node ID |
--cutover-ts |
Cutover timestamp in RFC-3339 format (e.g. 2025-09-01T00:00:00Z) |
--migration-plan |
Path to the migration plan file on the host |
# Stop and delete state (clean stop — daemon will NOT auto-resume)
sudo solo-provisioner consensus migration soak stop
# Stop but preserve elapsed soak time (daemon WILL auto-resume on next restart)
sudo solo-provisioner consensus migration soak stop --keep-stateAdditional Flags:
| Flag | Description | Default |
|---|---|---|
--keep-state |
Preserve cutover-state.jsonl so the daemon resumes the soak on its next restart |
false |
sudo solo-provisioner consensus migration soak status# Default human-readable text output
solo-provisioner version
# JSON output
solo-provisioner version --output=json
# Short flag
solo-provisioner -vSolo Provisioner supports YAML configuration files with the --config flag:
# config.yaml
log:
level: debug # Log level: debug, info, warn, error
consoleLogging: true # Enable console output
fileLogging: false # Enable file logging
blockNode:
namespace: "block-node"
release: "block-node"
chart: "oci://ghcr.io/hiero-ledger/hiero-block-node/block-node-server"
version: "0.22.1"
storage:
basePath: "/mnt/fast-storage"
archivePath: "" # Optional: defaults to basePath/archive
livePath: "" # Optional: defaults to basePath/live
logPath: "" # Optional: defaults to basePath/log
liveSize: "10Gi"
archiveSize: "100Gi"
logSize: "5Gi"
alloy:
monitorBlockNode: true
clusterName: "mainnet-block-01"
prometheusRemotes:
- name: "primary"
url: "https://prometheus.example.com/api/v1/write"
username: "metrics"
labelProfile: "ops" # Optional: auto-inject additional labels
lokiRemotes:
- name: "primary"
url: "https://loki.example.com/loki/api/v1/push"
username: "logs"
labelProfile: "ops" # Optional: auto-inject additional labels
teleport:
version: "16.0.0"
valuesFile: "/path/to/teleport-values.yaml"
nodeAgentToken: "" # Set via flag for security
nodeAgentProxyAddr: "proxy.teleport.example.com:443"
proxy:
enabled: false # Set to true to route traffic through a proxy
url: "127.0.0.1:3128" # Proxy address as host:port
sslCertFile: "/etc/ssl/certs/ca-certificates.crt"
containerRegistryProxy: "localhost:5050"Solo Provisioner uses this precedence order (highest to lowest):
- Command-line flags
- Environment variables (when using
--config) - Configuration file
- Built-in defaults
Solo Provisioner supports routing all network traffic through an HTTP/HTTPS proxy. This is useful for:
- Caching: Speed up repeated deployments by caching binary downloads and container images through a local proxy
- Security: Route traffic through a corporate proxy for auditing, filtering, or compliance requirements
- Air-gapped environments: Use a proxy to reach external registries from restricted networks
To enable proxy support, add a proxy section to your config file:
proxy:
enabled: true
url: "127.0.0.1:3128"
sslCertFile: "/etc/ssl/certs/ca-certificates.crt"
containerRegistryProxy: "localhost:5050"| Field | Description |
|---|---|
enabled |
Enable proxy mode |
url |
Proxy address as host:port (sets both HTTP_PROXY and HTTPS_PROXY) |
noProxy |
Comma-separated hosts/CIDRs to bypass proxy (defaults to localhost and private networks) |
sslCertFile |
CA certificate bundle path for TLS verification (sets SSL_CERT_FILE) |
containerRegistryProxy |
Container image pull-through cache as host:port (configures CRI-O registry mirror) |
When proxy is enabled, Solo Provisioner sets the appropriate environment variables so that all HTTP clients and Helm
operations automatically route through the proxy. The sslCertFile allows trusting custom CA certificates (e.g., for
MITM proxy inspection) without disabling TLS verification.
Environment variables can override configuration file values. They require a config file to be provided via --config
flag.
Format: SOLO_PROVISIONER_<SECTION>_<FIELD> (uppercase, underscores for nested fields)
# Override block node storage base path
export SOLO_PROVISIONER_BLOCKNODE_STORAGE_BASEPATH=/data/block-node
# Override block node namespace
export SOLO_PROVISIONER_BLOCKNODE_NAMESPACE=my-block-node
# Then run with a config file
sudo solo-provisioner block node install --profile=mainnet --config=/etc/solo-provisioner/config.yaml# Step 1: Deploy the block node (includes preflight checks and K8s setup)
sudo solo-provisioner block node install \
--profile=mainnet \
--config=/etc/solo-provisioner/config.yaml \
--values=/etc/solo-provisioner/block-node-values.yaml
# Step 2: (Optional) Set up secure SSH access
sudo solo-provisioner teleport node install \
--token=$TELEPORT_JOIN_TOKEN \
--proxy=teleport.hedera.com:443
# Step 3: (Optional) Set up secure kubectl access
sudo solo-provisioner teleport cluster install \
--values=/etc/solo-provisioner/teleport-kube-values.yaml
# Step 4: (Optional) Set up monitoring
sudo solo-provisioner alloy cluster install \
--monitor-block-node \
--cluster-name=mainnet-block-01 \
--add-prometheus-remote=name=primary,url=https://metrics.hedera.internal/write,username=block-metrics \
--add-loki-remote=name=primary,url=https://loki.hedera.internal/loki/api/v1/push,username=block-logs# Quick local setup for development
sudo solo-provisioner block node install --profile=local
# Verify deployment
kubectl get pods -n block-node# Step 1: Prepare new values file with updated config
# Step 2: Perform upgrade
sudo solo-provisioner block node upgrade \
--profile=mainnet \
--values=/etc/solo-provisioner/block-node-values-v2.yaml \
--chart-version=0.24.0
# Step 3: Verify
kubectl get pods -n block-node# Remove Teleport agents (if installed)
sudo solo-provisioner teleport cluster uninstall
sudo solo-provisioner teleport node uninstall
# Remove Alloy monitoring
sudo solo-provisioner alloy cluster uninstall
# Remove Kubernetes cluster (removes block node)
sudo solo-provisioner kube cluster uninstall
# Uninstall Solo Provisioner itself (--yes is required)
sudo solo-provisioner uninstall --yes1. Permission Denied
# Most commands require root privileges
sudo solo-provisioner block node install --profile=local2. Profile Not Specified
# Error: profile flag is required
# Solution: Always specify --profile
sudo solo-provisioner block node check --profile=mainnet3. Invalid Storage Path
# Error: invalid base path
# Ensure path exists and has correct permissions
sudo mkdir -p /mnt/storage
sudo solo-provisioner block node install --profile=mainnet --base-path=/mnt/storage4. Helm Chart Issues
# Check specific chart version availability
# Use explicit version if needed
sudo solo-provisioner block node install \
--profile=mainnet \
--chart-version=0.22.1# General help
solo-provisioner --help
# Command-specific help
solo-provisioner block --help
solo-provisioner block node --help
solo-provisioner block node install --helpEnable debug logging in your config file:
log:
level: debug
consoleLogging: true# INSTALLATION
# Download from: https://github.com/hashgraph/solo-weaver/releases
sudo ./solo-provisioner install
# BLOCK NODE
sudo solo-provisioner block node check --profile=<profile>
sudo solo-provisioner block node install --profile=<profile> [--values=<file>] [--plugin-preset=<preset>]
sudo solo-provisioner block node upgrade --profile=<profile> [--values=<file>] [--with-reset]
sudo solo-provisioner block node reconfigure --profile=<profile> [--values=<file>] [--no-restart]
sudo solo-provisioner block node reset --profile=<profile>
sudo solo-provisioner block node uninstall --profile=<profile> [--with-reset]
# KUBERNETES
sudo solo-provisioner kube cluster install
sudo solo-provisioner kube cluster uninstall
# TELEPORT
sudo solo-provisioner teleport node install --token=<token> --proxy=<addr>
sudo solo-provisioner teleport node uninstall
sudo solo-provisioner teleport cluster install --values=<file>
sudo solo-provisioner teleport cluster uninstall
# EXTERNAL SECRETS OPERATOR (ESO)
sudo solo-provisioner eso operator install [--namespace=<ns>]
sudo solo-provisioner eso operator uninstall [--namespace=<ns>]
sudo solo-provisioner eso secret create --store=<name> --name=<secret> --namespace=<ns> --set KEY=store/path[#field] [--refresh-interval=<interval>]
# ALLOY
sudo solo-provisioner alloy cluster install [--monitor-block-node] [--cluster-name=<name>]
sudo solo-provisioner alloy cluster uninstall
# DAEMON
sudo solo-provisioner daemon service install [--components=<list>] [--cn-node-id=<id>] [--cn-orbit=<ns>] [--cn-upgrade-dir=<path>]
sudo solo-provisioner daemon service install --from-config=<path>
sudo solo-provisioner daemon service uninstall
sudo solo-provisioner daemon service check # alias: status
sudo solo-provisioner daemon service start
sudo solo-provisioner daemon service stop
# CONSENSUS MIGRATION SOAK
sudo solo-provisioner consensus migration soak start --node-id=<id> --cutover-ts=<RFC-3339> --migration-plan=<path>
sudo solo-provisioner consensus migration soak stop [--keep-state]
sudo solo-provisioner consensus migration soak status
# UTILITIES
solo-provisioner version [--output=text|json]
solo-provisioner --helpDocument Version: 1.3.0 | Last Updated: June 2026