synology-nas-cli
Health Pass
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 39 GitHub stars
Code Warn
- network request — Outbound network request in nas/cli.py
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
nas: a zero-dependency Python CLI + MCP server for Synology DSM 7. One-command audit and fix for the C3RB3R / .L0CK3D ransomware campaign; hand it to your AI agent. Mirror; canonical repo and issues: gitlab.com/aa22396584/synology-nas-cli
synology-nas-cli
Audit a Synology NAS against the C3RB3R / .L0CK3D ransomware campaign with one command, fix what it finds with a second, and keep running DSM from the terminal afterwards.
English · 繁體中文 · 简体中文 · 日本語 · 한국어 · Deutsch · Español · Français
Canonical repository, releases and issues: https://gitlab.com/aa22396584/synology-nas-cli

nas is a zero-dependency Python CLI (standard library only, Python 3.8+, so it also runs on DSM itself) and stdio MCP server for the Synology DSM 7 Web API: status, health, logs, packages, volumes, Tailscale, DSM upgrades, scheduled health alerts pushed to a webhook, and an incident audit and remediation tool built during the October 2026 C3RB3R / .L0CK3D campaign against internet-exposed Synology NAS boxes (Synology-SA-24:20, SA-26:13, SA-26:14).
Not affiliated with Synology Inc. "Synology", "DSM" and "QuickConnect" are trademarks of Synology Inc.
- Hand it to your agent
- What it checks and fixes
- Install
- Pick your setup
- Configuration
- Credentials
- Backends
- Incident quick start
nas incident fix- MCP server and agent skills
- Alerts and webhooks
- Command reference summary
- What changes the NAS
- Tests
Hand it to your agent
Paste either prompt into Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Grok Bot, or any tool-using AI agent (Grok Bot setup, skills and alert routine: docs/GROK-BOT.md):
English prompt:
Use https://github.com/akari-sora/synology-nas-cli to audit and fix my Synology NAS at https://192.0.2.10:5001. Follow docs/AGENT-RUNBOOK.md.
繁體中文提示詞:
使用 https://github.com/akari-sora/synology-nas-cli 檢查並修復我的 Synology NAS(位址:https://192.0.2.10:5001),照著 docs/AGENT-RUNBOOK.md 的步驟執行。

What the agent does, step by step
- Install: Clones the repository (or installs the package) and verifies
./bin/nas --version. nas setup: Runsnas setup --json --no-input ...to write~/.config/synology-nas-cli/config.json(mode0600), signs in once to verify credentials, and pins DSM's TLS certificate SHA-256 fingerprint on first use.nas doctor: Runsnas doctor --jsonto verify configuration, credentials, origin reachability, TLS pin, login, DSM version against SA-26:13, 2-step verification status, administrator capabilities, and clock skew.nas incident: Runsnas incident --json(and optionally--probe --scan) for a read-only audit of DSM version, vulnerable packages, firewall, QuickConnect, UPnP router rules, public ports, external and failed sign-ins, AutoBlock, SMB1, built-inadmin, snapshot schedules, the unsupported-model WORM lock trap, ransom notes, and CVE-2024-10441 exploit attempts.- Plan: Runs
nas incident fix --dry-run --json --ransom-scanand shows every planned change (before -> after), the exact DSM Web API calls, and the corresponding DSM web UI path. nas incident fix --yes: Applies the approved plan in order (snapshot_worm→snapshots→packages→smb_min_protocol→quickconnect→router_port_forwarding→autoblock→admin_account→firewall, plus optional--rotate-passwordand--upgrade), reads every setting back to verify, and re-runs the audit.- Optional
--deepandalert-deploy: Runsnas incident --deep --json(one read-only root collector via Task Scheduler checking root-only logs, crontabs,/binbinaries, accounts, SSH keys, and/proc) andnas alert-deploy --json --test(10-minute health and security monitoring pushed to a webhook).
What you provide along the way
- NAS address and account (once):
- DSM URL (for example
https://192.0.2.10:5001, plus a VPN/Tailscale URL if used). - An administrator username and where its password comes from (
op://VAULT/ITEM/password, a password command, or the OS keychain). - Your LAN/VPN subnet CIDR(s) to keep allowed in the firewall (for example
192.0.2.0/24). - An optional shared folder path for temporary staging (for example
/docker/.nas-cli-deploy, used by--deepandalert-deploy).
- DSM URL (for example
- A 6-digit 2FA code (once, if 2-step verification is on):
- When
nas setupexits3with"field": "otp", provide the current 6-digit code;--trust-devicestores DSM's trusted-device token (0600) so subsequent commands do not need a code.
- When
- Plan confirmation:
- Review the dry-run plan before
nas incident fix --yesruns (and choose whether to include--rotate-passwordor--upgrade).
- Review the dry-run plan before
Full agent specification: docs/AGENT-RUNBOOK.md.
flowchart LR
A["nas incident<br/>read-only audit"] --> B["nas incident fix --dry-run<br/>plan: before → after"]
B --> C["nas incident fix --yes<br/>apply, read back, audit again"]
C --> D["nas alert-deploy<br/>check every 10 min → webhook"]
A -.->|files being encrypted| E["nas incident contain<br/>freeze the encryptor"]
E --> F["nas incident rescue<br/>memory capture → key search"]
What it checks and fixes
nas incident is read-only. Each item prints PASS, WARN, FAIL or SKIP with a one-line hint (--json outputs structured JSON; schema in schemas/incident.schema.json). Exit status is 1 when any item is FAIL.
| Item | What nas incident reports |
What nas incident fix changes |
|---|---|---|
dsm_version |
FAIL if the DSM build is older than the SA-26:13 fixed build for its branch, or on a branch with no SA-26:13 fix (DSM 7.1 and older); PASS if patched |
With --upgrade: downloads the newest official .pat for the model, runs PreCheck, installs, and reboots |
sa_26_14_packages |
Checks installed packages against Synology-SA-26:14 (Calendar, MailStation, PHP 7.3–8.4): FAIL if a vulnerable version is running, WARN if installed but stopped |
Stops running vulnerable packages (--only packages) |
firewall |
FAIL when the firewall is off and the NAS holds a public IP itself (e.g. dials PPPoE), WARN when off behind NAT, or FAIL/WARN when interfaces accept unmatched traffic or rules allow any source (source_ip: all) |
Enables the firewall, adds allow rules for this computer's /32 (as DSM logged it) plus IPv4 allowed_networks on LAN interfaces, sets every non-global interface policy to drop, and removes allow-any-source rules (--only firewall) |
quickconnect |
FAIL when QuickConnect or QuickConnect automatic router port forwarding is enabled |
Turns off QuickConnect and its UPnP router port forwarding (--only quickconnect) |
router_port_forwarding |
FAIL when DSM has enabled UPnP/NAT-PMP port-forwarding rules on the router |
Disables all router port-forwarding rules managed by DSM (--only router_port_forwarding) |
public_ports |
Only with --probe: opens TCP connections from this machine to DSM and file-service ports on the NAS's public IP (FAIL if any port answers) |
Covered by firewall, quickconnect, and router_port_forwarding |
external_signins |
FAIL when the connection log contains successful sign-ins from source addresses outside allowed_networks |
None (investigate unauthorized access; see docs/INCIDENT-C3RB3R.md) |
failed_signins |
WARN when failed sign-ins in the last --hours (default 24) exceed --failed-threshold (default 20) |
Mitigated by firewall and autoblock |
autoblock |
FAIL when AutoBlock is disabled |
Enables AutoBlock (10 attempts within 5 minutes, permanent block) and allow-lists this computer's IP plus IPv4 allowed_networks (and 127.0.0.1 if incident.autoblock_allow_loopback is true) (--only autoblock) |
smb_min_protocol |
FAIL when SMB is enabled and allows SMB1 (smb_min_protocol=0), WARN when SMB is off but minimum protocol is SMB1 |
Raises minimum SMB protocol to SMB2 (smb_min_protocol=1, smb_max_protocol=3) (--only smb_min_protocol) |
admin_account |
FAIL when the built-in admin account is enabled (expired: normal) |
Disables the built-in admin account via a temporary root task after checking that another enabled administrator exists and the current session is not admin (--only admin_account) |
snapshot_schedule:<share> |
Per shared folder: FAIL if no snapshot schedule exists, the schedule is disabled, or the newest scheduled snapshot is older than snapshot_max_age_hours (default 26 h) |
Creates a daily snapshot schedule (03:30 default) and a 14-day retention policy on shares that have no schedule yet; gated on a clean --ransom-scan in the same run or --i-know-snapshots-may-rotate (--only snapshots) |
snapshot_worm:<share> |
FAIL when the immutable-snapshot (WORM) lock is enabled on a model whose SYNO.DisasterRecovery.Retention info lacks worm (DSM accepts the API call and scheduled snapshots silently stop) |
Turns off the unsupported WORM lock on affected shares so scheduled snapshots resume; gated like snapshots (--only snapshot_worm) |
ransomware_files |
Only with --scan: searches shares via File Station for *.L0CK3D, *.LOCK3D, and read-me3.txt (FAIL if any match is found) |
None (incident response required; see docs/INCIDENT-C3RB3R.md) |
exploit_attempts |
Scans the DSM connection log for CVE-2024-10441 sign-in injection markers (PLUGIN_VALUE_END, LD_PRELOAD=, LD_DEBUG_OUTPUT) and incident.ioc_ips (WARN when attempts are seen) |
None (check --deep to verify whether any attempt succeeded) |
exploit_attempts_logs, ioc_cron, ioc_binaries, ioc_users, ioc_ssh_keys, ioc_processes |
Only with --deep: runs one read-only root collector to inspect /var/log/synoplugin.log*, /var/log/synoscgi.log*, /var/log/auth.log*, crontabs (/etc/cron.d/synot*), /bin/libgld, /bin/qnetd, /bin/l0ck.enc, xdy5*, extra uid-0 or unknown accounts, authorized_keys, and /proc processes/connections (FAIL on known C3RB3R artifacts, WARN on unknown entries) |
None (incident response required; see docs/INCIDENT-C3RB3R.md) |
SKIP always states the reason: the item needs a flag (--probe, --scan), the API returned an error, or the NAS does not use that feature (no Btrfs/Snapshot Replication, no router configured in DSM).
Install
From a Git checkout (zero install)
git clone https://gitlab.com/aa22396584/synology-nas-cli.git synology-nas-cli
cd synology-nas-cli
./bin/nas --help # runs directly from the checkout (Python 3.8+ stdlib only)
ln -s "$PWD/bin/nas" ~/bin/nas # optional: put `nas` on PATH
With pip from a GitLab release or local checkout
pip install https://gitlab.com/aa22396584/synology-nas-cli/-/archive/v1.0.0/synology-nas-cli-v1.0.0.tar.gz
# or from a local checkout:
pip install -e . # or: pipx install .
nas --help
On Windows (with py)
git clone https://gitlab.com/aa22396584/synology-nas-cli.git synology-nas-cli
cd synology-nas-cli
py -3 bin\nas --help
# or install the console script:
py -3 -m pip install -e .
nas --help
Initialize and verify login
Interactive or agent-driven one-step setup:
nas setup --origin https://192.0.2.10:5001 --user ops --password-source op:op://VAULT/ITEM/password --allowed-network 192.0.2.0/24
nas doctor
Or create the config file from the template and edit it by hand:
nas config init # writes ~/.config/synology-nas-cli/config.json (mode 600)
$EDITOR ~/.config/synology-nas-cli/config.json
nas config show # effective config, commands masked
nas whoami # one login: account, admin flag, DSM version, credential source
Pick your setup
| Setup | Backend | Password from | Notes |
|---|---|---|---|
| macOS with 1Password | password |
op://VAULT/ITEM/password refs; op signed in through the 1Password app |
Approve op in the app the first time. 2FA codes from the same item (op item get --otp). |
| macOS without 1Password | password |
nas login --save (Keychain), or password_command = security find-generic-password -s synology-nas -a USER -w |
nas login also caches the session so later commands do not sign in again. |
| Linux server, cron, systemd timer | password |
1Password service account (OP_SERVICE_ACCOUNT_TOKEN or the token file) + op:// refs, or password_command (secret-tool lookup ..., pass show ...) |
No terminal: the prompt is off, so configure a non-interactive source. |
| Windows | password |
password_command with PowerShell (see Credentials), or NAS_USER + NAS_PASSWORD |
Store the credential with the CredentialManager PowerShell module (New-StoredCredential) and read it with Get-StoredCredential in password_command. |
| On the NAS itself | local |
none: runs as root through DSM Task Scheduler | nas alert-deploy sets this up. Uses DSM's own Python 3.8. |
| CI / headless with a service account | password |
OP_SERVICE_ACCOUNT_TOKEN (CI secret) + op:// refs |
Or NAS_PASSWORD_COMMAND reading the CI secret store. NAS_PASSWORD also works. |
Configuration
One JSON file: $NAS_CONFIG, else ~/.config/synology-nas-cli/config.json. Every key is optional. Unknown keys are an error, so typos do not silently fall back to defaults. Keys beginning with _ are comments. Environment variables override the file; command-line flags override both. nas config show prints the result with commands masked.
Example (config.example.json, what nas config init writes; the addresses are documentation examples):
{
"name": "my-nas",
"origins": ["https://192.0.2.10:5001", "https://my-nas.example-tailnet.ts.net:5001"],
"lan_ip": "192.0.2.10",
"snapshot_shares": ["photos", "documents"],
"credentials": {
"op_username": "op://VAULT/ITEM/username",
"op_password": "op://VAULT/ITEM/password"
},
"webhook": {"file": "~/.config/synology-nas-cli/webhook.json"},
"deploy": {"stage_dir": "/SHARE/.nas-cli-deploy"}
}
| Key | Default | Meaning |
|---|---|---|
name |
nas |
Label in webhook bodies and the DSM notification prefix ([name]). |
assume_yes |
false |
Accept every confirmation (same as --yes / NAS_YES=1; what that turns on: What changes the NAS). The MCP server does not read it. |
origins |
[] |
DSM URLs for --backend password, tried in order; the first that accepts a TCP connection (3 s) is used. Put the LAN address first, then a VPN/Tailscale one. |
chrome_origins |
= origins |
Which Chrome tab --backend chrome may use. |
connect_timeout |
3.0 |
Seconds for the TCP reachability check. |
tls.verify |
false |
true: check DSM's certificate against the system CAs (a real certificate for the origin's name). false (DSM ships a self-signed certificate): trust on first use, see TLS. |
tls.known_certs_file |
~/.config/synology-nas-cli/known-certs.json |
SHA-256 of the certificate each origin presented first (mode 600). nas trust lists it, nas trust --reset ORIGIN forgets one. |
lan_ip / public_ip |
"" |
Targets for exposure and incident --probe. lan_ip defaults to the host of the first origin. public_ip is only needed when the NAS does not dial PPPoE itself (behind a router: the router's WAN address). |
allowed_networks |
[] |
Sign-ins from these are expected (alert, incident); empty = every non-global address (RFC 1918, 100.64.0.0/10, loopback, link-local, IPv6 ULA). For incident fix these IPv4 networks plus this computer's address (as DSM logged it, /32) are what the firewall and AutoBlock allow; empty = this computer only. IPv6 entries are not written as firewall rules. Entries wider than a /8 (for example 0.0.0.0/0), and public ranges wider than a /24 (IPv6: /48), stop incident fix's firewall and AutoBlock changes with the entry named; private ranges of any size and public /24s or single addresses are fine. |
snapshot_shares |
[] |
Shares whose scheduled snapshots alert watches. incident checks every share when empty. |
snapshot_max_age_hours |
26 |
A scheduled snapshot older than this is "stale". |
latest_dsm |
"" |
Newest build you expect for this model (e.g. 7.4.1-90080); empty skips the dsm_behind alert. |
credentials.* |
see Credentials | source, op_username, op_password, op_otp, op_token_file, username, password_command, otp_command, store, store_service. |
session.cache_file |
~/.cache/synology-nas-cli/session.json |
Where nas login caches the DSM session. Nothing is cached unless you run nas login. |
session.device_token_file |
~/.config/synology-nas-cli/device-token.json |
Trusted-device token from a 2FA login with --trust-device. |
session.device_name, session.trust_device |
synology-nas-cli, false |
Device name shown in DSM; ask for a device token on every 2FA login. |
alert.state_file |
~/.local/state/synology-nas-cli/baseline.json |
Baseline between alert runs. |
alert.package_volume |
"" |
Report packages installed outside this volume (e.g. /volume1). |
alert.recordings |
off | {"path": "/share/folder", "local_path": "", "folders": ["cam1"], "max_age_min": 10, "note": ""}: warn when a folder stops receiving files (camera recordings and the like). Empty folders = off. |
webhook.* |
"" |
Where alert-deploy reads {"url", "header"}: op_item (op://VAULT/ITEM: website = URL, field op_header_field = header), file (JSON file), or command (prints that JSON). |
deploy.* |
see nas config show |
root_dir (/volume1/@nas-alert), stage_dir (a folder in a writable share), task_name, install_task_name, interval_min (10), dsm_webhook_name, dsm_webhook_prefix, dsm_webhook_template (Warning). |
upgrade.lang, upgrade.cache_dir |
enu, ~/.cache/synology-nas-cli/pat |
PreCheck note language; where .pat files are kept. |
incident.* |
14, 3, 30, 10, 5, false |
snapshot_retain_days (only set on shares that have no retention policy), snapshot_hour, snapshot_minute for schedules incident fix creates; autoblock_attempts, autoblock_within_mins it sets; ioc_ips (default 46.8.78.122 from public reports and 104.253.175.125, observed in the wild on 2026-10-28), ioc_strings (extra strings) for exploit_attempts and --deep; allowed_users (accounts --deep treats as known); autoblock_allow_loopback: also allow-list 127.0.0.1 in AutoBlock (Tailscale's DSM package in userspace mode delivers every tailnet connection as 127.0.0.1, so one blocked tailnet device would block all of them). |
Environment variables: NAS_CONFIG, NAS_BACKEND, NAS_YES, NAS_NO_INPUT, NAS_ORIGINS (comma-separated; older name NAS_LAN_ORIGIN), NAS_CHROME_ORIGINS (older NAS_ORIGIN), NAS_OP_ITEM (op://VAULT/ITEM), NAS_OP_TOKEN_FILE, NAS_USER, NAS_PASSWORD_COMMAND, NAS_PASSWORD, NAS_OTP_COMMAND, NAS_CREDENTIAL_SOURCE, NAS_SESSION_FILE, NAS_SID + NAS_SYNOTOKEN, OP_SERVICE_ACCOUNT_TOKEN.
Credentials
The password comes from the first configured source in this chain (credentials.source or NAS_CREDENTIAL_SOURCE can force one: op, command, keychain, env, prompt):
- 1Password (
op):credentials.op_password=op://VAULT/ITEM/FIELD(andop_username, or a literalusername).opauthenticates withOP_SERVICE_ACCOUNT_TOKEN; if that is unset andcredentials.op_token_file(default~/.config/op/service-account-token) exists, its content is used as the token; otherwiseopuses its own sign-in (1Password app integration orop signin).opis only run when anop://reference is configured.op readfails on item names with non-ASCII characters: use the item ID. - Command (
command):NAS_PASSWORD_COMMAND, elsecredentials.password_command, run with/bin/sh -c(cmd.exeon Windows); the first line of output is the password. Examples:- macOS Keychain:
security find-generic-password -s synology-nas -a USER -w - Linux Secret Service:
secret-tool lookup service synology-nas account USER - pass:
pass show nas/USER - Bitwarden CLI:
bw get password synology-nas(needsBW_SESSIONfrombw unlock) - Windows:
powershell -NoProfile -Command "(Get-StoredCredential -Target synology-nas).GetNetworkCredential().Password"with the CredentialManager module (Install-Module CredentialManager).cmdkey /generic:synology-nas /user:USER /passstores a credential but has no way to print the password again.
- macOS Keychain:
- OS keychain (
keychain): the entrynas login --savewrote. macOS Keychain throughsecurity(the password goes throughsecurity -ion stdin, not the command line) or the Linux Secret Service throughsecret-tool(password on stdin).credentials.store=auto(default),keychain,secret-tool, or""(off); service namecredentials.store_service.nas logout --forgetdeletes it. - Environment (
env):NAS_USER+NAS_PASSWORD. The value sits in the process environment (readable by processes of the same user, and byps eon some systems). - Prompt (
prompt):getpass, only when stdin is a terminal and nothing above is configured.
The username comes from NAS_USER / credentials.username, else op_username, else the keychain entry's account, else a prompt.
2-step verification. When DSM answers 403 the login is retried once with a code from credentials.otp_command / NAS_OTP_COMMAND (e.g. oathtool --totp -b SECRET), else op item get --otp when the password came from 1Password (add the DSM 2FA secret to the item as a one-time password), else a prompt. nas login --trust-device (or session.trust_device: true) asks DSM for a trusted-device token and stores it (mode 600); later logins send it and skip the code until DSM forgets the device.
Sessions. By default every command signs in and out (each sign-in adds a "signed in successfully" line to DSM's connection log). nas login signs in once and caches the SID and SynoToken in session.cache_file (mode 600); later commands reuse it, and when DSM rejects it (106 timeout, 107 duplicate login, 119 invalid SID) they sign in again and rewrite the cache. nas logout ends and deletes it. NAS_SID + NAS_SYNOTOKEN reuse a session you already have (for example copied from a browser): nothing signs in or out, and a rejected session is an error.
TLS
The password backend never uses HTTP(S)_PROXY, ALL_PROXY or the system proxy settings for DSM. With tls.verify: false (default) the SHA-256 of the certificate an origin presents on the first connection is stored in tls.known_certs_file (mode 600; nas: trusting the certificate of ... on first use on stderr) and every later connection must present the same certificate, otherwise the command stops with the TLS certificate of ORIGIN changed. After replacing the DSM certificate: nas trust --reset https://HOST:PORT. nas trust lists the stored fingerprints. With tls.verify: true the system CAs decide and nothing is pinned. A notice is printed when the 1Password token file is readable by other users.
AutoBlock. DSM blocks an address after 10 failed sign-ins within 5 minutes by default, often permanently. nas never retries a failed password. A wrong password in your source costs one attempt per command, so fix the source before running again. Code 407 means the address is already blocked: remove it from Control Panel > Security > Protection > Allow/Block List from another address. See docs/TROUBLESHOOTING.md for recovery paths.
Backends
| Backend | When | How it signs in | Notes |
|---|---|---|---|
password (default) |
From any computer that reaches DSM over HTTPS (LAN, VPN, Tailscale) | The credential chain above | Uses origins from the config. |
chrome |
macOS only, when you are already signed in to DSM in Google Chrome and do not want to store a password anywhere | Runs same-origin requests inside the DSM tab via AppleScript (Chrome: View > Developer > Allow JavaScript from Apple Events) | upgrade, alert-deploy and tailscale run with --backend password. |
local |
On the NAS itself as root (DSM Task Scheduler), e.g. what alert-deploy installs |
None: /usr/syno/bin/synowebapi --exec runs the API as the system |
Runs on the NAS as root. Tailscale status via the tailscale binary. |
Incident quick start
nas incident # read-only audit
nas incident --probe # also test the public address from this machine
nas incident --scan # also search every share for ransom notes (minutes)
nas incident --deep # also indicators of compromise via one read-only root script (needs deploy.stage_dir)
nas --json incident # machine-readable
nas incident fix --dry-run # the plan: every change (current -> new), the API calls, the DSM UI path
nas incident fix # same plan, asks before applying
nas incident fix --yes # apply without asking
nas incident fix --only firewall,smb_min_protocol
nas incident fix --skip admin_account
nas incident fix --ransom-scan --yes # a clean scan also allows the snapshot fixes
nas incident fix --upgrade --rotate-password --yes # opt-in extras
nas incident contain # list processes that look like an encryptor (signals nothing without --yes / NAS_YES=1)
nas incident contain --yes # SIGSTOP them (all threads) so their memory and keys stay intact
nas incident contain --yes --stop-sync # also stop Cloud Sync, Synology Drive, Hyper Backup, Container Manager
nas incident contain --yes --save-exe /volumeUSB1/usbshare/evidence # copy each executable (sha256 recorded)
nas incident contain --release # SIGCONT the frozen PIDs and start the packages --stop-sync stopped
nas incident rescue --yes --capture-dir /usbshare1/rescue # freeze, capture memory, recover key candidates
nas incident rescue --yes --capture-dir /usbshare1/rescue --sample /share/a.pdf.L0CK3D # test each candidate on a copy
nas incident rescue --yes --capture-dir /usbshare1/rescue --decrypt-from /share/docs --decrypt-to /Volumes/USB/out
While files are being encrypted, nas incident contain --yes is the first command: it freezes the encryptor (SIGSTOP, never killed) so the session keys stay in its memory, then prints the next steps.
- Candidates: known names (
libgld,qnetd,xdy5*,incident.ioc_process_names), executables in/tmp,/var/tmp,/dev/shm,/volume*/@tmp, processes with*.L0CK3D/read-me3.txtfiles open for writing, and high CPU outside Synology paths (containers excluded). A process whose executable is under a Synology path (/usr/syno,/var/packages,/bin, ...) needs a reason besides the open files; the scan lists it underskippedwith the--pidthat adds it. PID 1, kernel threads and the scanning script with its parents are never signalled, also with--pid. - The freeze signals exactly the processes the scan listed: same PID, start time (
/proc/PID/statfield 22) and executable; a PID reused in between is reported and skipped. --stop-sync: after the freeze, each of the packages Cloud Sync, Synology Drive, Hyper Backup and Container Manager (or Docker) that is running is stopped withSYNO.Core.Package.Control stop. Stopping Container Manager stops every container. The IDs it stopped are recorded incontain-packages.jsonnext to the session cache on this computer.--release:SIGCONTto the PIDs recorded on the NAS (incident.contain_state_file) and any--pid, thenSYNO.Core.Package.Control startfor each recorded package; the record is deleted once every package started. It starts packages, not single containers: a container comes back when its restart policy (auto-restart in Container Manager) starts it with the package.
nas incident rescue does the memory capture and key recovery with soimy/c3rb3r-rescue, fetched at run time at the commit in incident.rescue_commit:
contain: the freeze above (--no-containskips it).toolkit: clones the toolkit and checks the commit.--rescue-dir DIRuses an existing checkout whengit status --porcelainis empty and HEAD is that commit (--force-unpinned-toolkitfor any other).capture: as root on the NAS (temporary Task Scheduler task, or directly with--backend local), the toolkit'scapture_process_memory.py inspectandcapturefor each frozen PID into a new case folder under--capture-dir. The folder has to be on a USB disk or another volume than the affected data; volumes are compared by mount source in/proc/self/mountinfo(every Btrfs shared folder is its own subvolume with its own device number, so device numbers do not tell volumes apart);--force-same-volumecaptures onto the same volume. A--capture-dirwith a symlink in its path is refused. With--backend passwordthe capture tool goes throughdeploy.stage_dir(DSM answers 4803 to a task script that carries it inline) and runs (from stdin) only if its sha256 matches the uploaded file. The case folder is then handed to the signed-in DSM account (id -u,chown) so File Station can list and download it.pull: downloads the capture to the workdir on this computer after checking free space there (the capture's readable bytes plus 256 MiB); with--backend locala workdir on the NAS's system partition is refused.--keep-on-nasstops after the capture.venv: a venv (Pythonincident.rescue_min_python, 3.11, or--python) with the toolkit's requirements installed bypip --require-hashes: the sha256 of every PyPI file of numpy 2.3.5, cryptography 50.0.2, cffi 2.1.1 and pycparser 3.12 is kept innas/rescue.pyfor the pinned commit; other pins install without hash checks with--force-unpinned-toolkit. Then the toolkit's self-tests.keys:recover_process_key.pyon each capture; candidates are files with mode 400, never printed. A process that is not the reviewed encryptor stops here withValueError: Executable does not match the reviewed sample profile.sample(--sample NAS_FILE [--expected-sha256 HEX]): copies the file and tries each candidate on the copy.decrypt(--decrypt-from NAS_FOLDER --decrypt-to DIR): decrypts copies with the verified candidate (--force-unverified-key: the first candidate when no--sampleverified one).
--force turns on --force-same-volume, --force-unverified-key and --force-unpinned-toolkit together.
Each step is recorded in <workdir>/state.json (--workdir, default ~/.local/share/synology-nas-cli/rescue/<time>); the same command continues where it stopped. What it writes on the NAS:
- the case folder
--capture-dir/case-<time>-<random>/pid-N(kept; owned by the signed-in account; delete it when the capture is safe elsewhere). The capture tool runs from stdin (the verified bytes); no copy of it is written to the case folder. The chown/chmod walk goes through file descriptors opened withO_NOFOLLOWand leaves symlinks alone. In testing on a DSM shared folder, folders created with mode 0700 were listed as 0755 afterwards (files kept 0400); check who can read that shared folder. - with
--backend password: the uploaded tooldeploy.stage_dir/nas-rescue-tool-<random>.pyand the result filedeploy.stage_dir/nas-incident-rescue-<token>.json, both deleted after the run, and the temporary root tasknas-incident-rescue, deleted after the run. - the
containstep: the temporary tasknas-incident-containand its result file (deleted), and the freeze's state fileincident.contain_state_file(/root/.nas-incident-contain.json), removed bynas incident contain --release.
JSON: schemas/incident-rescue.schema.json.
If the audit shows ransomware_files FAIL or external_signins FAIL you are past hardening: follow docs/INCIDENT-C3RB3R.md (isolate first, keep evidence, restore from snapshots).
nas incident fix
Runs the audit, prints the plan, asks for confirmation (--yes, NAS_YES=1 or assume_yes: true answer yes; without a terminal and without those it exits 3 with Not applied: add --yes (or NAS_YES=1).), applies the changes in this order, reads each one back, then runs the audit again and prints before -> after. --dry-run prints the plan and the exact calls and sends no write. Exit status 1 when a change fails or the final audit still has a FAIL.
Order: snapshot_worm -> snapshots -> packages -> smb_min_protocol -> quickconnect -> router_port_forwarding -> autoblock -> admin_account -> firewall -> rotate_password (opt-in) -> upgrade (opt-in, reboots).
| Fix | What changes | DSM web UI (manual way) | Raw call (nas api ... --allow-write) |
|---|---|---|---|
snapshot_worm |
Immutable-snapshot lock off on shares where the model lacks immutable snapshots (gated like snapshots: the share's schedule starts taking snapshots again) |
Snapshot Replication > Snapshots > Settings (the option is hidden on unsupported models, so the API is the only way) / Snapshot Replication > 快照 > 設定 | SYNO.DisasterRecovery.Retention set_worm_lock --version 1 type=Share name=SHARE worm_lock_enable=false worm_lock_day=7 |
snapshots |
Create a daily schedule (incident.snapshot_hour:minute) on shares that have none, with retention by days (incident.snapshot_retain_days, 14) only where the share has no retention policy yet. Existing schedules, enabled or not, and existing retention are left alone. Runs after a clean --ransom-scan in the same run, or with --i-know-snapshots-may-rotate / --force: snapshots that start being taken apply retention, which deletes the oldest snapshots |
Snapshot Replication > Snapshots > select the share > Settings > Schedule / Retention Policy (Snapshot Replication > 快照 > 設定 > 排程 / 保留原則) | SYNO.Core.Share.Snapshot set_schedule --version 1 name=SHARE enable_snapshot_schedule=true 'schedule={...}' task_id=-1 then SYNO.DisasterRecovery.Retention set --version 1 name=SHARE type=Share policyType=64 retainDay=14 ... |
packages |
Stop running packages named in SA-26:14 (update or uninstall them yourself) | Package Center > Installed > package > Update or Stop (套件中心 > 已安裝) | SYNO.Core.Package.Control stop --version 1 id=PHP7.4 |
smb_min_protocol |
Minimum SMB protocol SMB2 (SMB1-only clients lose access) | Control Panel > File Services > SMB > Advanced Settings (控制台 > 檔案服務 > SMB > 進階設定) | SYNO.Core.FileServ.SMB set --version 3 smb_min_protocol=1 smb_max_protocol=3 |
quickconnect |
QuickConnect off, and its automatic router port forwarding off | Control Panel > External Access > QuickConnect (控制台 > 外部存取 > QuickConnect) | SYNO.Core.QuickConnect set --version 2 enabled=false; SYNO.Core.QuickConnect.Upnp set --version 1 enabled=false |
router_port_forwarding |
Disable (not delete) the router port-forwarding rules DSM manages over UPnP/NAT-PMP | Control Panel > External Access > Router Configuration (控制台 > 外部存取 > 路由器設定); also check the router's own page | SYNO.Core.PortForwarding.Rules save --version 1 'rules=[...enable:false...]' task_id_suffix=PF, poll save_status |
autoblock |
AutoBlock on (10 attempts / 5 min); allow-list this computer's address as DSM logged it, the IPv4 allowed_networks, and 127.0.0.1 only with incident.autoblock_allow_loopback |
Control Panel > Security > Protection > Auto Block; Allow/Block List (控制台 > 安全性 > 保護) | SYNO.Core.Security.AutoBlock set --version 1 enable=true attempts=10 within_mins=5 enable_expire=false expire_day=0; SYNO.Core.Security.AutoBlock.Rules create --version 1 type=allow block_days=0 overwrite=true 'ip=["127.0.0.1"]' |
admin_account |
Disable the built-in admin. Checks first whether the CLI is signed in as admin, which account a pasted NAS_SID session uses (SYNO.Core.Desktop.Initdata), and whether another enabled member of administrators exists; --force-admin (or --force) applies it whatever those checks say. SYNO.Core.User set answers 105 from a password session, so it runs as root in a temporary Task Scheduler task that is deleted afterwards |
Control Panel > User & Group > admin > Edit > Disable this account (控制台 > 使用者與群組) | root task script: /usr/syno/bin/synowebapi --exec api=SYNO.Core.User method=set version=1 name='"admin"' expired='"now"' (with --backend local: SYNO.Core.User set --version 1 name=admin expired=now) |
firewall |
Enable the firewall; allow rules (this computer's /32 as DSM logged it + IPv4 allowed_networks) on the LAN interfaces (global only when the profile has no LAN interface); every interface except global (eth*, pppoe, ppp0, vpn, wlan0, ...) drops unmatched traffic; rules that allow any source are removed |
Control Panel > Security > Firewall > Edit Rules (控制台 > 安全性 > 防火牆 > 編輯規則) | SYNO.Core.Security.Firewall.Profile set --version 1 'profile={...}' profile_applying=false, then SYNO.Core.Security.Firewall.Profile.Apply start --version 1 name=default profile_applying=false, poll status task_id=..., stop |
rotate_password (--rotate-password) |
New random 24-character password for the signed-in account. Written to the store first (1Password item via op item edit on stdin, keeping the old value in the item history; or the keychain entry, with the old value copied to <store_service>-previous), then changed in DSM; if DSM rejects it, the old password goes back into the store; if the DSM call is interrupted the store keeps the new one. Read back and a fresh login verified. Never printed. Writes to 1Password or the keychain entry; for a password from a command, env or prompt, change it in DSM and in that source |
Personal > Account > change password (個人 > 帳號) | SYNO.Core.NormalUser set --version 2 password=... old_password=... |
upgrade (--upgrade) |
Download the newest official .pat, upload, PreCheck, install; the NAS reboots |
Control Panel > Update & Restore > Manual DSM Update (控制台 > 更新與還原) | nas upgrade --yes |
Firewall lockout check. Before the firewall change the CLI checks, and lists in the plan: HTTP(S) proxies are configured for the origin; the origin is a loopback address (SSH tunnel, local proxy) or a public address; the newest successful sign-in of this account in DSM's connection log shows a loopback, IPv6 or public source; or that logged source differs from the local address this computer uses to reach the NAS (NAT, a proxy, or the sign-in came from another device). When a check does not pass, the change waits for --force-firewall (or --force), which applies it with the check result in the plan note. The allowed set contains the address DSM logged. After applying, it signs in again over the same origin from scratch; if that fails it prints rescue steps: another computer on the LAN, Synology Assistant / find.synology.com, Tailscale userspace mode (DSM sees 127.0.0.1, which the firewall did not filter in testing), and the RESET button (mode 1). IPv6 allow-list entries are not written as rules, so IPv6 sources are dropped. 100.64.0.0/10 (Tailscale) is also the range CGNAT ISPs use on the WAN side: on a NAS that dials PPPoE itself, a global allow rule for it would admit other subscribers, which is why the rules go on the LAN interfaces.
A NAS with its own public address (it dials PPPoE itself, or sits in a DMZ) has no router NAT in front of it: the DSM firewall is then the only layer between every DSM service and the internet. QuickConnect and UPnP port forwarding open DSM to the internet even behind NAT.
MCP server and agent skills
MCP server (nas mcp)
nas mcp runs a Model Context Protocol server over stdio (JSON-RPC 2.0; protocol versions 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05) exposing 15 tools: setup, doctor, whoami, status, health, net, services, logs, alert, incident_audit, incident_fix, pkg_list, volume_list, tailscale_status, and api_read.
claude mcp add synology-nas --scope user -- /path/to/synology-nas-cli/bin/nas mcp
claude mcp add synology-nas --scope user -- /path/to/synology-nas-cli/bin/nas mcp --auto-confirm
incident_fix applies its plan when a call has confirm: true, or has no confirm on a server started with --auto-confirm; confirm: false is always a dry run. NAS_YES and assume_yes do not change MCP calls. The setup tool fills fields the configuration does not have yet and takes op:, keychain and env credential sources; changing a value already in effect (config file, environment such as NAS_ORIGINS, or the server's --lan-origin) for origins, user, password_source, otp_source, tls, allowed_networks, stage_dir or webhook_file, and command: sources, go through nas setup on the command line (the tool answers exit 2 naming the field). Built-in defaults (tls pin, otp_source op) do not count as set; public allowed_networks entries are added on the command line. Fields such as user, descr, log text, cmdline and file names carry text chosen by whoever sends sign-ins to the NAS or runs processes on it.
Full tool list, schemas and configuration for Claude Code, Claude Desktop, Cursor and Codex: docs/MCP.md.
Agent skills installer
The skills/ folder contains skills/synology-nas/SKILL.md and skills/synology-ransomware-response/SKILL.md. Install them into existing skill directories (~/.claude/skills, ~/.cursor/skills, ~/.codex/skills, ~/.agents/skills, ~/.grok/skills, ~/.gemini/skills, ~/.config/opencode/skills, ~/.hermes/shared-skills):
./skills/install.sh # symlink skills into detected agent folders
./skills/install.sh --copy # copy instead of symlink (--dry-run, --uninstall also available)
On Windows PowerShell:
.\skills\install.ps1 # supports --copy, --dry-run, --uninstall
Alerts and webhooks
nas alert runs every health and security check once and exits 1 when something is at warning/critical level. Every finding is {level, code, kind, message}. With --push FILE it POSTs only what changed since the last successful push to {"url", "header"} from FILE: new conditions, a reminder every 24 h for warning/critical ones still present, every event, resolved keys, and one heartbeat a day. Codes, the body format and receiver examples are in docs/ALERTS.md.
Three ways to run it:
- On the NAS, set up for you:
nas alert-deploycopies the code into a root-only folder (deploy.root_dir), creates a root Task Scheduler task runningnas --backend local alert --pushevery 10 minutes, and points DSM's own notification webhook (Control Panel > Notification > Webhooks) at the same URL. - On the NAS, by hand: the same three pieces done in DSM's UI. Steps in
docs/ALERTS.md. - From another always-on machine: cron or launchd running
nas alert --push ...with thepasswordbackend. Seedocs/ALERTS.md.
Receivers: any HTTPS endpoint that takes a JSON POST with one header (generic JSON receiver), a chat-bot "routine" webhook that starts an agent run per POST (Grok-Bot-style), or no webhook at all: DSM's built-in notifications (email, Synology mobile push through the DS finder / Synology app) cover DSM's own warning/error events. All three are in docs/ALERTS.md.
Command reference summary
nas setup --origin URL --user USER --password-source SOURCE # write the config, sign in once, pin the certificate
nas doctor # config, credentials, origin, TLS, login, DSM version, 2FA, capabilities, clock
nas whoami # backend, origin, credential source, session, account, admin flag, DSM version
nas status # model, DSM version, uptime, SA-26:13 verdict
nas net # interfaces, PPPoE (public address), firewall, gateway
nas services # SSH / FTP / SMB / AFP / NFS / rsync / QuickConnect / AutoBlock / DSM ports
nas inventory # shares, packages (SA-26:14 flags), containers, users (2FA), scheduled tasks
nas health # disks (SMART, temperature), pools, volumes, UPS, CPU, memory
nas logs --since "2026-10-30 00:00" --top 5 # failed sign-ins by user/IP, external successes
nas ransom-scan # File Station search for *.L0CK3D, *.LOCK3D, read-me3.txt (one task per pattern)
nas exposure # TCP probe of LAN and public addresses from this machine
nas incident [--probe] [--scan] [--deep] [--hours 24] [--failed-threshold 20]
nas incident fix [--dry-run] [--yes] [--only LIST] [--skip LIST] [--share S] [--ransom-scan] [--upgrade] [--rotate-password]
nas upgrade --dry-run | nas upgrade | nas upgrade --yes | nas upgrade --wait-only
nas pkg list | start ID | stop ID | install ID [--volume /volume1] | uninstall ID [--remove-data] | default-volume [/volumeN]
nas volume list | volume atime volume_1 noatime
nas tailscale status | tailscale login-url [--force]
nas alert [--no-update] [--push webhook.json]
nas alert-deploy [--test] [--interval 10] [--webhook-item op://VAULT/ITEM] [--no-dsm-webhook]
nas login [--save] [--trust-device] [--no-cache] | nas logout [--forget]
nas config init [--force] | nas config show
nas trust | nas trust --reset https://HOST:PORT # TLS certificates trusted on first use
nas api SYNO.Core.System info --version 1 # any DSM API; writes need --allow-write
nas --json <command> # JSON output for everything
nas --no-input <command> # prompts answer with exit 3 (also NAS_NO_INPUT=1; automatic without a terminal)
nas --yes <command> # accept every confirmation (also NAS_YES=1, config assume_yes; see "What changes the NAS")
nas --force incident fix --yes # apply changes whose checks do not pass (--force-firewall, --force-admin, snapshot rotation, reinstall)
nas mcp [--auto-confirm] # MCP server on stdio (docs/MCP.md)
Documentation index
docs/AGENT-RUNBOOK.md— Step-by-step sequence for AI agents (setup→doctor→incident→incident fix→--deep→alert-deploy).docs/INCIDENT-C3RB3R.md— Public incident page for the 2026 C3RB3R /.L0CK3Dransomware campaign, advisories, timeline, IOCs, decision steps, recovery and prevention.docs/HARDENING.md— Synology DSM security hardening checklist with sourced explanations, English / 繁體中文 DSM UI paths, and matchingnascommands.docs/TROUBLESHOOTING.md— Solutions for firewall/AutoBlock/2FA lockouts, Mode 1 reset, reverse proxies and Tailscale127.0.0.1, unsupported WORM models, Google Drive 750 GB/day quota, SMB1 devices, DSM API error codes, "up to date" branch traps, and TLS pin resets.docs/MCP.md— MCP server tools, arguments, schemas, and client setup.docs/GROK-BOT.md— Grok Bot:nason the bot's cloud computer (Tailscale) or on a computer with the Grok Bot app, skills, the alert routine webhook, and a bot description.docs/ALERTS.md— Alert codes, webhook payload, receivers, and deployment (alert-deploy, manual DSM task, cron/launchd).docs/DSM-API-NOTES.md— DSM 7 Web API quirks and parameter encoding rules relied on bynas.docs/EXIT-CODES.md— CLI and MCP_meta.exit_codedefinitions (0–7,130).schemas/— JSON Schemas forincident,incident-fix,incident-contain,incident-rescue,alert,status,health, andwhoami.llms.txt— Entry-point index for LLM agents.
What changes the NAS
- Commands that change the NAS:
incident fix(asks, or--yes),incident contain(SIGSTOPwith--yes;--stop-syncstops packages;--release),incident rescue(asks, or--yes: freeze, temporary root tasks, the capture folder),upgrade(without--yesit still uploads the.patand runs PreCheck, then deletes the upload;--yesinstalls),pkg start|stop|install|uninstall|default-volume VOL,volume atime,tailscale login-url,alert-deploy,login --save/logout --forget(local keychain only),trust --reset(local file), andapiwith--allow-write.ransom-scan,incident --scanandincident fix --ransom-scancreate temporary File Station search tasks on the NAS (stopped and cleaned afterwards).nas apisends methods that are not reads with--allow-write,--yesorNAS_YES=1. --yes,NAS_YES=1and configassume_yes: trueanswer every confirmation of the CLI:incident fixapplies its plan (with--upgradeit installs DSM and the NAS reboots, a PreCheck with notes included; with--rotate-passwordit changes the password);upgradeinstalls after PreCheck and the NAS reboots;incident containfreezes the candidates (and with--stop-syncstops the packages);incident rescueruns its steps;apisends write methods as with--allow-write.nas mcptakes its answer from each call'sconfirmand from--auto-confirm, not from these.incident fix --dry-runprints every write call it would send. The plan always shows current -> new values.- Passwords, SIDs and SynoTokens are never printed.
nas apiprints raw DSM output, which can include serial numbers and account names; the other commands filter those. - Each password sign-in that fails counts toward AutoBlock; the CLI reports a failed sign-in once and stops.
alert-deployinstalls code that runs as root on the NAS in a root-only folder outside the shared folders.
Tests
tests/run_offline.sh # all offline tests with python3, and with Python 3.8 when `uv python find 3.8` works
tests/smoke_live.sh # optional: read-only commands against your NAS (uses your config)
License
MIT, Copyright (c) 2026 Akari Sora. See LICENSE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found