Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c0c6f61
feat(wallet ls): wallet addresses display as base64 format (#116)
mrnkslv Apr 30, 2026
5b2d963
feat: use highload wallet for adding nominators (#124)
mrnkslv May 4, 2026
99ed8f6
feat: merge elections crate into service crate as a module (#126)
mrnkslv May 4, 2026
86228fb
feat: add tests for entity CRUD REST API endpoints (#136)
mrnkslv May 13, 2026
8e11f39
Feature/sma 85 migrate vote commands to rest api (#132)
mrnkslv May 14, 2026
81fae59
feat: Configure auto-deploy and auto-topup features (#125)
mrnkslv May 14, 2026
e5ad45f
Fix nodectl service unresponsive when a node's control-server is unre…
Keshoid May 15, 2026
72c32e2
Feature/sma 55 add cli commands for adaptive staking parameters (#128)
mrnkslv May 15, 2026
4fc2d85
Merge branch 'master' into chore/merge-master-v0.5.0
Keshoid May 15, 2026
b591451
feat:Deploy TON Core pool via stub-with-SETCODE init message (#138)
mrnkslv May 15, 2026
247c75b
Merge pull request #144 from RSquad/chore/merge-master-v0.5.0
Keshoid May 15, 2026
058ae6b
feat(elections): use static ADNL address by default (#145)
Keshoid May 18, 2026
71698f0
feat:process TONCore pool withdraw requests (#139)
mrnkslv May 18, 2026
8e48a2e
fix: serialize TaskController lifecycle ops to prevent orphaned elect…
Keshoid May 18, 2026
bf4704b
chore(nodectl): release v0.5.0 (#147)
Keshoid May 18, 2026
84fd343
Merge master to nodectl v0.5.0 (#149)
Keshoid May 18, 2026
0d6e91f
Fix/toncore get pool data empty dicts (#151)
mrnkslv May 18, 2026
5616532
ci(nodectl): install openssl on windows runner for release build (#153)
Keshoid May 19, 2026
1747e5b
docs(nodectl): HashiCorp Vault backend + chart SA flexibility (#156)
Keshoid May 19, 2026
0fc33ee
feat(nodectl): in-pod vault migration via 'nodectl key migrate' (#157)
Keshoid May 19, 2026
7a23e60
fix(nodectl): bound ton-http-api per-endpoint wait so daemon cannot h…
Keshoid May 20, 2026
e3a7562
fix(nodectl): send process_withdraw_requests with 1 TON and limit 10 …
mrnkslv May 20, 2026
d891b5e
Secrets-vault migration fixes for nodectl v0.5.0 (#166)
Keshoid May 21, 2026
42a412a
Merge branch 'master' into feat/merge-master-nodectl-v0.5.0-3
Keshoid May 21, 2026
bc53b6f
Merge pull request #169 from RSquad/feat/merge-master-nodectl-v0.5.0-3
Keshoid May 21, 2026
36f491e
docs(vault): consolidate HashiCorp setup across charts and lib (#171)
Keshoid May 21, 2026
3a7e02f
refactor(adnl): merge AdnlClient::timeout_connect into connect (#172)
Keshoid May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/nodectl-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ jobs:
sudo apt-get update
sudo apt-get install -y pkg-config libssl-dev

- name: Install OpenSSL (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
vcpkg install openssl:x64-windows-static-md
"OPENSSL_DIR=$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows-static-md" `
| Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"OPENSSL_STATIC=1" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8

- name: Build
working-directory: ${{ env.WORKING_DIR }}
run: cargo build --release --package nodectl --target ${{ matrix.target }}
Expand Down
13 changes: 13 additions & 0 deletions helm/nodectl/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to the nodectl Helm chart will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
Versions follow the Helm chart release tags (e.g. `helm/nodectl/v0.1.0`).

## [0.3.0] - 2026-05-19

appVersion: `v0.5.0`

### Added

- Attach an existing `ServiceAccount` to the Pod by setting `serviceAccount.name` while keeping `serviceAccount.enabled=false`. Useful when the SA is managed outside the Helm release (e.g. bound to a HashiCorp Vault role).
- Documentation: HashiCorp Vault backend in `docs/setup.md` and the file → HashiCorp migration runbook in `docs/copy-file-to-hashicorp.md`.

### Changed

- Default image updated to nodectl `v0.5.0`

## [0.2.1] - 2026-04-21

appVersion: `v0.4.0`
Expand Down
4 changes: 2 additions & 2 deletions helm/nodectl/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: nodectl
description: TON Node Control Tool — validator elections, voting, and monitoring
type: application
version: 0.2.1
appVersion: "v0.4.0"
version: 0.3.0
appVersion: "v0.5.0"

sources:
- https://github.com/rsquad/ton-rust-node
10 changes: 7 additions & 3 deletions helm/nodectl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ Total master wallet funding needed: `N * 1 TON` (wallets) + `N * 1 TON` (pools)

### Vault

Vault stores private keys (wallet keys, control client keys). Currently only the file-based backend is documented:
Vault stores all nodectl secrets — wallet keys, the master wallet key,
control client keys, REST API user password hashes, and the JWT signing key.
Two backends are supported:

| Backend | URL format | Use case |
|---------|-----------|----------|
| File-based | `file:///nodectl/data/vault.json?master_key=<hex>` | All setups |
| File-based | `file:///nodectl/data/vault.json?master_key=<hex>` | Single-cluster deployments, simplest setup |
| HashiCorp Vault | `hashicorp://<addr>?auth=k8s&role=<role>&...` or `?api_key=<token>&...` | Multi-tenant infra, shared key management, centralised audit |

Vault is configured via the **`VAULT_URL` environment variable**, not in `config.json`. The Helm chart passes this from a K8s Secret or plain value.
Vault is configured via the **`VAULT_URL` environment variable**, not in `config.json`. The Helm chart passes this from a K8s Secret or plain value. See [docs/setup.md — Create a vault secret](docs/setup.md#create-a-vault-secret) for both backends and [docs/copy-file-to-hashicorp.md](docs/copy-file-to-hashicorp.md) for migrating an existing deployment from file to HashiCorp.

> **Tip:** Add `IPC_LOCK` capability to the container security context if you want file-based vault to use `mlock()` for memory protection. Not required — the service works without it.

Expand Down Expand Up @@ -304,6 +307,7 @@ On first deploy, the init container prepares the PVC:
| Elections and stake policies | [docs/elections.md](docs/elections.md) |
| Chart maintainer guide | [docs/maintaining.md](docs/maintaining.md) |
| First elections with Rust node | [docs/first-elections.md](docs/first-elections.md) |
| Migrate vault: file → HashiCorp | [docs/copy-file-to-hashicorp.md](docs/copy-file-to-hashicorp.md) |

## Useful commands

Expand Down
268 changes: 268 additions & 0 deletions helm/nodectl/docs/copy-file-to-hashicorp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# Migrating nodectl secrets from file storage to HashiCorp Vault

This runbook describes how to migrate a running nodectl deployment's secrets
from the file-based vault on the PVC to a HashiCorp Vault backend, using the
built-in `nodectl key migrate` command. The source `vault.json` never leaves
the Pod, and no extra binary is required.

> **Supported direction:** `nodectl key migrate` only migrates from a **file
> vault** (`file://`) to a **HashiCorp vault** (`hashicorp://`). Any other
> combination of source/destination is rejected up front. Non-extractable
> secrets are migrated alongside extractable ones — the original `extractable`
> flag is preserved on the destination, so wallet keys that were created
> non-extractable on the file vault remain non-extractable in HashiCorp.

## What is stored in the vault

nodectl keeps the following secret types behind `VAULT_URL`:

- **Wallet keys** — one per configured wallet (created manually with
`nodectl key add`/`nodectl key import`).
- **Master wallet key** — **auto-generated by the service on first start** if
it is missing. The on-chain master wallet address depends on this key.
- **Control client keys** — authenticate nodectl to each validator's Control
Server (imported manually).
- **User password hashes** — created by `nodectl auth add`; gate the REST API.
- **JWT signing key** — **auto-generated by the service on first start** if it
is missing. Signs the tokens issued by `/auth/login`.

The two auto-generated entries are the high-risk items during a migration: if
they are absent on the destination when the Pod restarts, the service
regenerates them, which (a) changes the on-chain master wallet address and
(b) invalidates every JWT already issued to API users. Both are generated by
the service on first start regardless of any user login, so make sure the
service has been started at least once against the source vault before you
migrate.

## When to run

Unlike the validator node — where new validator keys are generated inside the
elections window — nodectl secrets do not rotate around elections. The
migration can be performed at any time, but to avoid races with concurrent
writes:

- Do not run `nodectl key add`, `nodectl key import`, `nodectl key rm`, or
`nodectl auth add` during the procedure.
- Confirm the master wallet key and JWT signing key are already present on
the source vault (`nodectl key ls` lists them as `master-wallet-secret` and
`auth.jwt-signing-key`).

The Pod is restarted once at the end to pick up the new `VAULT_URL`.

## Procedure

Assumptions:

- A HashiCorp Vault cluster is reachable from the nodectl Pod.
- Vault is configured with one of the supported auth methods: `token` or
`k8s` (see [secrets-vault README](../../../src/secrets-vault/README.md)
for URL parameters).
- For `auth=k8s`, the Pod runs under a ServiceAccount that is mapped to a
Vault role (see [setup.md — HashiCorp Vault backend](setup.md#hashicorp-vault-backend)).

### 1. Enter the Pod

```bash
kubectl exec -it deploy/my-nodectl -- sh
```

The remaining steps in §2–§6 run inside the Pod. `nodectl` and the migration
subcommand are part of the image — no extra binary is needed.

### 2. Confirm the current `VAULT_URL` and list existing keys

The current `VAULT_URL` must start with `file://`, for example:

```
file:///nodectl/data/vault.json?master_key=<MASTER_KEY_HEX>
```

```bash
echo "$VAULT_URL"
nodectl key ls
```

`nodectl key ls` shows every secret in the vault — including blobs such as
`auth.users.<name>` (password hashes) and `auth.jwt-signing-key`. Note the
count and the IDs you expect to see on the destination after the migration.

### 3. Point `FROM_VAULT_URL` at the current file storage

`nodectl key migrate` reads the source URL from `FROM_VAULT_URL` and the
destination URL from `VAULT_URL`.

```bash
export FROM_VAULT_URL="$VAULT_URL"
echo "$FROM_VAULT_URL" # must start with file://
```

### 4. Point `VAULT_URL` at the new HashiCorp Vault

Set `VAULT_URL` to the destination URL. The exact query parameters depend on
the auth method and on how the engines are mounted in your Vault.

Example with `auth=k8s` (typical in Kubernetes):

```bash
export VAULT_URL='hashicorp://http://node-vault.node-vault:8200?auth=k8s&auth_mount=kubernetes&role=nodectl-app&transit_mount=ton-transit&transit_prefix=nodectl&kv_mount=ton&kv_prefix=nodectl'
```

Example with a static token:

```bash
export VAULT_URL='hashicorp://https://vault.example.com:8200?api_key=hvs.xxx&transit_mount=ton-transit&transit_prefix=nodectl&kv_mount=ton&kv_prefix=nodectl'
```

See [secrets-vault README](../../../src/secrets-vault/README.md#hashicorp-vault-backend-hashicorp)
for the full list of query parameters.

> **Note:** the exec session's `VAULT_URL` change only affects this shell and
> the migration command below — the running nodectl service still uses the
> file-storage URL injected by the chart and keeps serving traffic. We make
> the switch permanent in §6.

### 5. Run the migration

Preview first:

```bash
nodectl key migrate --dry-run
```

When the plan looks correct, run for real:

```bash
nodectl key migrate
```

Example output of a real run (truncated):

```text
Migrate:
from: file://...
to: hashicorp://...

[1/N] READ master-wallet-secret
algo=Ed25519 payload=KeyPair extractable=yes expires=never tags=1
WRITE master-wallet-secret mode=NewOnly
OK master-wallet-secret (3ms)
...
────────────────────────────────────────────────────────────
total: N copied: N skipped: 0 failed: 0 elapsed: 18ms

✓ Migration completed
```

With `--dry-run`, the banner and per-record lines indicate no writes happen:

```text
Migrate:
from: file://...
to: hashicorp://...
mode: DRY RUN (no writes)

[1/N] READ master-wallet-secret
algo=Ed25519 payload=KeyPair extractable=yes expires=never tags=1
WRITE master-wallet-secret mode=NewOnly
DRY (dry-run, no write performed)
...
```

Each record is logged with a `READ` line, a `WRITE` line, then `OK` (or
`DRY` under `--dry-run`) on success. The final summary prints
`total / copied / skipped / failed / elapsed`. The command exits non-zero if
any record failed.

Flags:

| Flag | Default | Notes |
|------|---------|-------|
| `--on-conflict <fail\|skip\|overwrite>` | `fail` | What to do when the destination already has the same secret id |
| `--dry-run` | off | Print the plan without writing |
| `--continue-on-error` | off | Keep going on per-secret write/conflict errors instead of aborting (read errors still abort) |

The source vault is read-only — if migration fails partway, `vault.json` on
the PVC is untouched and the service keeps working with the old `VAULT_URL`.

### 6. Verify the destination

With `VAULT_URL` still pointing at HashiCorp in your exec shell, list the
destination:

```bash
nodectl key ls
```

Confirm the count matches what step 2 reported and that all expected IDs are
present (`master-wallet-secret`, every `wallet-*`, every
`control-client-secret*`, `auth.users.<name>` for each REST API user, and
`auth.jwt-signing-key`).

### 7. Persist `VAULT_URL` and restart the Pod

The shell-level `export VAULT_URL=...` from §4 affects only your current exec
session. The Pod still reads the file-storage URL from its Deployment env
(which the chart fills from `vault.secretName` or `vault.url`).

Leave the Pod shell:

```bash
exit
```

Then update Helm values so `VAULT_URL` permanently points at HashiCorp. Two
equivalent paths:

**Option A — update the Secret and `helm upgrade`** (recommended; matches how
the chart was installed):

```bash
kubectl create secret generic nodectl-vault \
--from-literal=VAULT_URL='hashicorp://http://node-vault.node-vault:8200?auth=k8s&auth_mount=kubernetes&role=nodectl-app&transit_mount=ton-transit&transit_prefix=nodectl&kv_mount=ton&kv_prefix=nodectl' \
--dry-run=client -o yaml | kubectl apply -f -

helm upgrade my-nodectl oci://ghcr.io/rsquad/ton-rust-node/helm/nodectl \
--reuse-values \
--set vault.secretName=nodectl-vault
```

**Option B — set `vault.url` directly via Helm values** (only if you were
already using `vault.url`, not `vault.secretName`):

```bash
helm upgrade my-nodectl oci://ghcr.io/rsquad/ton-rust-node/helm/nodectl \
--reuse-values \
--set vault.url='hashicorp://...'
```

Then restart the Pod so it picks up the new value:

```bash
kubectl rollout restart deploy/my-nodectl
```

After the Pod comes back up, confirm the service is healthy:

```bash
kubectl logs deploy/my-nodectl -f | grep -i "vault\|opened\|master wallet"
kubectl exec deploy/my-nodectl -- nodectl key ls
```

The key list must match what was on the source vault. Wallets and pools
should continue to operate against the same on-chain addresses — the master
wallet address only depends on the master wallet keypair, which was copied
as-is. Existing API users keep working with their current credentials, and
previously issued JWTs remain valid until they expire because the signing
key was carried over too.

## Rollback

If verification fails or nodectl misbehaves after restart:

1. Revert `VAULT_URL` in the Secret (or `vault.url` value) to the original
`file://` URL and `helm upgrade --reuse-values`.
2. `kubectl rollout restart deploy/my-nodectl`.

`nodectl key migrate` only reads from the source vault — `vault.json` on the
PVC was not modified by the procedure, so rollback restores the previous
working state exactly.
Loading