Skip to content
Merged

3.3.1 #986

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ddb9aef
update schains ls test
dmytrotkk Apr 21, 2026
df24536
Merge pull request #974 from skalenetwork/update-schains-ls-test
dmytrotkk Apr 21, 2026
144b7cd
Fix skale health sgx command
badrogger Jul 20, 2026
1b0cb94
Merge pull request #976 from skalenetwork/fix-health-sgx
badrogger Jul 20, 2026
6a9646e
Update nftables
badrogger Sep 1, 2026
9215f79
Fix for existing nodes
badrogger Sep 1, 2026
a239ddb
Small improvement
badrogger Sep 1, 2026
abc8af9
Fix passive node init
badrogger Sep 1, 2026
fb77a0c
Small improvements
badrogger Sep 2, 2026
200d83b
Improve exception handling
badrogger Sep 8, 2026
ef28f02
Extract user rules to seprate chain
badrogger Sep 9, 2026
8c083b9
Fix for ssh port
badrogger Sep 10, 2026
1eebf76
Small improvements
badrogger Sep 11, 2026
c969d32
Improve exception handling
badrogger Sep 11, 2026
58c0abb
Fix setup
badrogger Sep 11, 2026
959be02
Remove MONITORING_PORTS functionality
badrogger Sep 11, 2026
8cc24cf
Bump version
badrogger Sep 11, 2026
ed032a4
Improve cleanup
badrogger Sep 11, 2026
5aa6b97
Fix monitoring container removal
badrogger Sep 11, 2026
7baaa6d
Remove redundant env option
badrogger Sep 14, 2026
f6f26de
Merge pull request #979 from skalenetwork/update-nftables
badrogger Sep 14, 2026
743b4ce
update skale py version
dmytrotkk Sep 15, 2026
56eada8
Merge pull request #980 from skalenetwork/bump-skale-py
dmytrotkk Sep 15, 2026
5918f48
Merge pull request #981 from skalenetwork/develop
badrogger Sep 16, 2026
ddaa946
Bump version
badrogger Sep 16, 2026
feb00ef
Merge pull request #982 from skalenetwork/bump-version
badrogger Sep 17, 2026
ff61de7
Fix publish
badrogger Sep 17, 2026
e4e03fa
Merge pull request #983 from skalenetwork/fix-publish
badrogger Sep 17, 2026
ffce261
Wrap sgx api into node-cli
badrogger Sep 22, 2026
b8462e8
Add sgx options call
badrogger Sep 23, 2026
905cc59
Add sgx api tests
badrogger Sep 23, 2026
b577c7d
Rename skale sgx status to skale sgx cert-status
badrogger Sep 23, 2026
9ee6157
Merge pull request #984 from skalenetwork/sgx-api
badrogger Sep 23, 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
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.VERSION }}
target_commitish: ${{ github.sha }}
name: Release ${{ env.VERSION }}
draft: false
prerelease: ${{ steps.release_info.outputs.prerelease }}
Expand Down
92 changes: 89 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ SKALE Node CLI, part of the SKALE suite of validator tools, is the command line
4. [sChain commands (Standard)](#schain-commands-standard)
5. [Health commands (Standard)](#health-commands-standard)
6. [SSL commands (Standard)](#ssl-commands-standard)
7. [Logs commands (Standard)](#logs-commands-standard)
7. [SGX commands (Standard)](#sgx-commands-standard)
8. [Logs commands (Standard)](#logs-commands-standard)
3. [Passive Node Usage (`skale` - Passive Build)](#passive-node-usage-skale---passive-build)
1. [Top level commands (Passive)](#top-level-commands-passive)
2. [Passive node commands](#passive-node-commands)
Expand All @@ -32,8 +33,9 @@ SKALE Node CLI, part of the SKALE suite of validator tools, is the command line
5. [Fair Wallet commands](#fair-wallet-commands)
6. [Fair Logs commands](#fair-logs-commands)
7. [Fair SSL commands](#fair-ssl-commands)
8. [Fair Staking commands](#fair-staking-commands)
9. [Passive Fair Node commands](#passive-fair-node-commands)
8. [Fair SGX commands](#fair-sgx-commands)
9. [Fair Staking commands](#fair-staking-commands)
10. [Passive Fair Node commands](#passive-fair-node-commands)
5. [Exit codes](#exit-codes)
6. [Development](#development)

Expand Down Expand Up @@ -119,6 +121,29 @@ Options:

> Prefix: `skale node`

#### Configure firewall

Firewall setup does not automatically open monitoring ports 9100 and 8080.
Reconfiguration removes their legacy allow rules from the managed base chain
and saves the updated rules for reboot. `MONITORING_CONTAINERS` controls the
containers only; the firewall's `--monitoring` option has been removed.
Explicit rules in `/etc/nft.conf.d/skale/user.conf` remain under operator control.

SSH allow rules use all listening ports reported by `sshd -T`, including ports
configured through included files and `ListenAddress`. If detection fails, the
command stops before enabling the default-drop policy.

For a port configured through sshd command-line options or socket activation,
set the `SSH_PORT` environment variable explicitly when running commands that
configure the firewall (including node init and update):

```shell
sudo SSH_PORT=2222 skale node configure-firewall
```

The override replaces automatic detection and accepts one port from 1 to 65535.
It must be passed in the command environment, not only in the node settings file.

#### Node information

Get base info about the standard SKALE node.
Expand Down Expand Up @@ -476,6 +501,48 @@ Options:
* `--port/-p` - Port to start healthcheck server (default: `4536`).
* `--no-client` - Skip client connection (only make sure server started without errors).

### SGX commands (Standard)

> Prefix: `skale sgx`

Manage the client certificate that node services use to authenticate to the SGX wallet.
The files live in `~/.skale/node_data/sgx_certs` and are read by the SKALE containers.
These commands work directly with those files and the SGX server; they do not go through
the node API.

#### SGX certificate status

Show the certificate files, the certificate details and its expiry.

```shell
skale sgx status [--json] [--check]
```

Options:

* `--json` - Show data in JSON format.
* `--check` - Also verify that the SGX server accepts the certificate.

#### Renew SGX certificate

Issue a new client certificate from the SGX server and install it. The current
certificate stays in place until the new one is signed and verified against the server.
The previous files are copied to `~/.skale/node_data/sgx_certs_backup/<timestamp>`.
If the SGX server requires manual approval of signing requests, the command prints the
request hash and waits until it is approved. Node services pick up the new certificate
on their next SGX request; no restart is needed. `skale health sgx` confirms afterwards
that node services reach the SGX server.

```shell
skale sgx renew [--yes] [--timeout <SECONDS>] [--skip-verify]
```

Options:

* `--yes` - Do not ask for confirmation.
* `--timeout` - Seconds to wait for the SGX server to sign the request (default: `600`).
* `--skip-verify` - Install the certificate without testing it against the SGX server first.

### Logs commands (Standard)

> Prefix: `skale logs`
Expand Down Expand Up @@ -1096,6 +1163,25 @@ Options:
* `--no-client` - Skip client connection for openssl check.
* `--no-wss` - Skip WSS server starting for skaled check.

### Fair SGX commands

> Prefix: `fair sgx`

Manage the client certificate that node services use to authenticate to the SGX wallet.
See [SGX commands (Standard)](#sgx-commands-standard) for details; the behaviour is the same.

#### Fair SGX Status

```shell
fair sgx status [--json] [--check]
```

#### Fair SGX Renew

```shell
fair sgx renew [--yes] [--timeout <SECONDS>] [--skip-verify]
```

### Fair Staking commands

> Prefix: `fair staking`
Expand Down
5 changes: 2 additions & 3 deletions node_cli/cli/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,16 +239,15 @@ def check(network):


@node.command(help='Reconfigure nftables rules')
@click.option('--monitoring', is_flag=True)
@click.option(
'--yes',
is_flag=True,
callback=abort_if_false,
expose_value=False,
prompt='Are you sure you want to reconfigure firewall rules?',
)
def configure_firewall(monitoring):
configure_firewall_rules(enable_monitoring=monitoring)
def configure_firewall():
configure_firewall_rules()


@node.command(help='Show node version information')
Expand Down
191 changes: 191 additions & 0 deletions node_cli/cli/sgx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# This file is part of node-cli
#
# Copyright (C) 2026 SKALE Labs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import json

import click
from terminaltables import SingleTable

from node_cli.configs.sgx import SGX_SIGN_TIMEOUT
from node_cli.core.sgx import (
SgxCertificateError,
check_certificate,
get_certificate_status,
get_server_options,
renew_certificate,
)
from node_cli.utils.decorators import check_inited, check_user
from node_cli.utils.exit_codes import CLIExitCodes
from node_cli.utils.helper import abort_if_false, error_exit
from node_cli.utils.settings import get_sgx_url
from node_cli.utils.texts import safe_load_texts

G_TEXTS = safe_load_texts()
TEXTS = G_TEXTS['sgx']


@click.group()
def sgx_cli():
pass


@sgx_cli.group('sgx', help=TEXTS['help'])
def sgx():
pass


@sgx.command('options', help=TEXTS['options']['help'])
@click.option('--json', 'json_format', is_flag=True, help=G_TEXTS['common']['json'])
@check_inited
@check_user
def options(json_format: bool) -> None:
_configured_sgx_url()
status, payload = get_server_options()
if status != 'ok':
error_exit(payload, exit_code=CLIExitCodes.BAD_API_RESPONSE)
if json_format:
print(json.dumps(payload))
else:
rows = [['SGX option', 'Value']]
for group, values in payload.items():
entries = (
[(f'{group}.{key}', value) for key, value in values.items()]
if isinstance(values, dict)
else [(group, values)]
)
rows.extend(
[key, value if isinstance(value, str) else json.dumps(value)]
for key, value in entries
)
print(SingleTable(rows).table)


@sgx.command('cert-status', help=TEXTS['status']['help'])
@click.option('--json', 'json_format', is_flag=True, help=G_TEXTS['common']['json'])
@click.option('--check', is_flag=True, help=TEXTS['status']['check'])
def cert_status(json_format: bool, check: bool) -> None:
try:
info = get_certificate_status()
except SgxCertificateError as err:
error_exit(str(err), exit_code=CLIExitCodes.OPERATION_EXECUTION_ERROR)
check_error = None
if check:
try:
info['server_version'] = check_certificate(_configured_sgx_url())
except SgxCertificateError as err:
check_error = str(err)
if json_format:
if check_error:
info['check_error'] = check_error
print(json.dumps(info))
else:
print_certificate_status(info)
if check_error:
error_exit(check_error, exit_code=CLIExitCodes.OPERATION_EXECUTION_ERROR)


@sgx.command('renew', help=TEXTS['renew']['help'])
@click.option(
'--yes',
is_flag=True,
callback=abort_if_false,
expose_value=False,
prompt=TEXTS['renew']['prompt'],
)
@click.option(
'--timeout',
type=int,
default=SGX_SIGN_TIMEOUT,
show_default=True,
help=TEXTS['renew']['timeout'],
)
@click.option('--skip-verify', is_flag=True, help=TEXTS['renew']['skip_verify'])
@check_inited
@check_user
def renew(timeout: int, skip_verify: bool) -> None:
sgx_url = _configured_sgx_url()
try:
result = renew_certificate(sgx_url, timeout=timeout, verify=not skip_verify, log=print)
except SgxCertificateError as err:
error_exit(str(err), exit_code=CLIExitCodes.OPERATION_EXECUTION_ERROR)
print_certificate_status(result)
if result['backup']:
print(TEXTS['renew']['backup'].format(path=result['backup']))
print(TEXTS['renew']['done'])


def _configured_sgx_url() -> str:
try:
sgx_url = get_sgx_url()
except Exception as err: # settings files are missing or invalid
error_exit(f'Cannot read node settings: {err}', exit_code=CLIExitCodes.NODE_STATE_ERROR)
if not sgx_url:
error_exit(TEXTS['no_sgx'], exit_code=CLIExitCodes.NODE_STATE_ERROR)
return sgx_url


def print_certificate_status(info: dict) -> None:
present = info['present']
rows = [
['SGX client certificate', ''],
['Directory', info['directory']],
['Private key', _presence(present['key'])],
['Signing request', _presence(present['csr'])],
['Certificate', _presence(present['crt'])],
]
if 'subject' in info:
rows.extend(
[
['Subject CN', info['subject']],
['Issuer CN', info['issuer']],
['Valid from', info['not_valid_before']],
['Valid until', info['not_valid_after']],
['Days left', str(info['days_left'])],
['SHA-256', info['fingerprint_sha256']],
['Key matches', _yes_no(info['key_matches'])],
]
)
if info.get('server_version'):
rows.append(['SGX server', f'accepted the certificate, version {info["server_version"]}'])
print(SingleTable(rows).table)
for notice in _notices(info):
print(notice)


def _notices(info: dict) -> list[str]:
notices = []
if not info['complete']:
notices.append(TEXTS['status']['missing'])
elif info.get('expired'):
notices.append(TEXTS['status']['expired'])
elif info.get('expires_soon'):
notices.append(TEXTS['status']['expires_soon'].format(days=info['days_left']))
if info.get('key_matches') is False:
notices.append(TEXTS['status']['key_mismatch'])
return notices


def _presence(present: bool) -> str:
return 'present' if present else 'missing'


def _yes_no(value: bool | None) -> str:
if value is None:
return 'unknown'
return 'yes' if value else 'no'
2 changes: 2 additions & 0 deletions node_cli/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@

SKALE_DIR = os.path.join(G_CONF_HOME, '.skale')
SKALE_TMP_DIR = os.path.join(SKALE_DIR, '.tmp')
AUTH_DIR = Path(SKALE_DIR) / 'auth'
ADMIN_API_TOKEN_PATH = AUTH_DIR / 'admin-api.token'

NODE_DATA_PATH = os.path.join(SKALE_DIR, 'node_data')
SCHAIN_NODE_DATA_PATH = os.path.join(NODE_DATA_PATH, 'schains')
Expand Down
3 changes: 2 additions & 1 deletion node_cli/configs/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
'set-domain-name',
'update-safe',
],
'health': ['containers', 'schains', 'sgx'],
'health': ['containers', 'schains'],
'info': ['sgx', 'sgx-options'],
'schains': ['config', 'list', 'dkg-statuses', 'firewall-rules', 'repair', 'get'],
'ssl': ['status', 'upload'],
'wallet': ['info', 'send-eth'],
Expand Down
Loading
Loading