Skip to content

feat(backend): pin SSH host keys and DSM's self-signed certificate - #147

Merged
EraPartner merged 2 commits into
mainfrom
claude/project-thread-ze36w4
Oct 28, 2026
Merged

EraPartner merged 2 commits into
mainfrom
claude/project-thread-ze36w4

Conversation

@EraPartner

@EraPartner EraPartner commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Requested by EraPartner · project thread

Follow-up to #145. The user approved both items that #145 listed under "Not changed".

Changes

Area Before After
SSH (Mac mini, Pi direct, Pi legacy relay) Any host key was accepted, so a machine that took over the host's address received Watchman's login. New optional fields sshHostKey (Mac mini, Pi) and macMiniSshHostKey (Pi relay) take the SHA256:… fingerprint that ssh-keygen -lf prints. When a fingerprint is set, the pool and the one-shot executor check the host key during the handshake, before any credentials are sent. A mismatch is reported as UNAUTHORIZED and the message names both fingerprints. Changing the pin opens a new pooled connection. An empty field keeps the current behaviour.
Synology DSM over https:// DSM's default self-signed certificate failed with a bare "http request failed". New optional field dsmCertSha256. When it is set, DSM requests go through the existing pinned client (createPinnedClient, the same one Hue uses). The client checks the certificate in the TLS handshake, before the DSM password is sent. Without a pin, an untrusted certificate now gives an error that says to set the fingerprint.
Config — Validation rejects malformed fingerprints. The new fields appear in the Settings form with help text.

Docs: docs/integrations/{macmini,raspberry-pi,synology}.md now include the steps to get each fingerprint.

Decisions (standing rule: pick the recommended option)

  • The pin is optional; there is no trust-on-first-use. Trust-on-first-use would need stored state and a reset flow. An empty pin keeps the current behaviour, so no existing setup breaks.
  • Synology uses a certificate pin, not an allowSelfSigned toggle. A toggle would turn off verification, and anything on the network could then receive the DSM password.
  • ServiceInfra gets an injectable pinnedHttp factory, defaulting to createPinnedClient, so the client choice can be tested.

Read-only rule

No new calls to monitored services. The PR only adds checks to connections that already exist.

Checks

Nothing ran in the cloud (npm is unreachable), so CI is the first run. Regression tests are in hostKey.test.ts, sshPool.test.ts, MacMiniService.test.ts, PiStatsCollector.test.ts, dsmClient.test.ts, registerServices.test.ts and fieldMetadata.test.ts. hostKey.ts was run directly with Node 22, and its fingerprint for the test key matched a separate Python computation (sha256, then base64 without padding). The match with ssh-keygen output was not checked here because ssh-keygen isn't installed in the cloud.

🤖 Generated with Claude Code

https://claude.ai/code/session_018QJtaL6RxEK3gXc6YDZdPD

SSH (Mac mini, Raspberry Pi direct and legacy relay) accepted any host
key, so a machine taking over the host's address received Watchman's
login. New optional fields sshHostKey / macMiniSshHostKey take the
fingerprint ssh-keygen -lf prints ("SHA256:..."). When set, the pool and
the one-shot executor verify the key during the handshake, before any
credentials are sent, and a mismatch is UNAUTHORIZED naming both
fingerprints. Changing the pin opens a new pooled connection. Empty
keeps today's behaviour.

Synology DSM over https:// with DSM's default self-signed certificate
failed with a bare "http request failed". New optional dsmCertSha256
routes DSM requests through the existing pinned client (checked in the
TLS handshake, before the DSM password is sent). Without a pin, an
untrusted-certificate failure now says to set the fingerprint.

Config validation rejects malformed fingerprints. Docs: macmini,
raspberry-pi, synology.
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 87.29% 1772 / 2030
🔵 Statements 84.91% 1937 / 2281
🔵 Functions 79.29% 494 / 623
🔵 Branches 74.68% 1593 / 2133
File CoverageNo changed files found.
Generated in workflow #384 for commit fb4a3d5 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Coverage Report for backend

Status Category Percentage Covered / Total
🟢 Lines 93.42% (🎯 80%) 2869 / 3071
🟢 Statements 90.51% (🎯 80%) 3148 / 3478
🟢 Functions 86.2% (🎯 80%) 600 / 696
🟢 Branches 79.88% (🎯 72%) 2148 / 2689
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/backend/src/domain/services/macMini/MacMiniService.ts 96.52% 78.53% 100% 97.74% 207-209, 222, 258, 371, 421
apps/backend/src/domain/services/raspberryPi/PiStatsCollector.ts 94.01% 82.82% 95% 97.81% 152, 234, 236, 237, 239, 240, 241, 243-244, 283
apps/backend/src/infra/ssh/hostKey.ts 100% 100% 100% 100%
apps/backend/src/infra/ssh/sshExecutor.ts 100% 100% 100% 100%
apps/backend/src/infra/ssh/sshPool.ts 91.53% 79.71% 100% 93.57% 70, 89-90, 138-143, 146, 217, 268, 282
apps/backend/src/infra/synology/dsmClient.ts 98.41% 90.56% 90% 100% 46
Generated in workflow #384 for commit fb4a3d5 by the Vitest Coverage Report Action

@EraPartner
EraPartner marked this pull request as ready for review October 28, 2026 09:53
@EraPartner
EraPartner enabled auto-merge (squash) October 28, 2026 09:53
@EraPartner
EraPartner disabled auto-merge October 28, 2026 11:44
@EraPartner
EraPartner merged commit 33e188c into main Oct 28, 2026
18 checks passed
@EraPartner
EraPartner deleted the claude/project-thread-ze36w4 branch October 28, 2026 11:44
EraPartner pushed a commit that referenced this pull request Oct 28, 2026
Rebased onto #145/#147/#148: document SSH host-key pinning (Mac Mini,
Pi), the DSM certificate fingerprint and DSM-only health, the now-used
Bitcoin rawtx ZMQ endpoint, Roon pairing state in DATA_DIR, and the
new error messages and login backoffs.
EraPartner added a commit that referenced this pull request Oct 28, 2026
…144)

* docs: add per-service setup guides with least-privilege credentials

Add docs/guides/service-setup/ with an index and one guide for each of
the 13 service kinds: what to enable on the service, which read-only or
least-privilege credentials to create, what to enter in Watchman, and
troubleshooting keyed to the error messages in the code.

Link the guides from the integration pages and indexes, and fix doc
drift found while checking against the code: AdGuard no longer claims
toggle control, the Homebridge health probe path, Bitcoin's Tor claim,
and the unused zmqRawtxEndpoint.

* docs: drop Test connection caveat, fix useServiceHealth hook doc

#146 made Test connection report an unreachable service as Failed, so
the setup guides no longer need the known-issue callout.

docs/components/use-service-health.md described a protection-toggle
mutation hook and a cache-clear hook that don't exist; rewrite it from
apps/frontend/src/hooks/useServiceHealth.ts (read-only, ADR-032).

* docs: update setup guides for the service bug fixes on main

Rebased onto #145/#147/#148: document SSH host-key pinning (Mac Mini,
Pi), the DSM certificate fingerprint and DSM-only health, the now-used
Bitcoin rawtx ZMQ endpoint, Roon pairing state in DATA_DIR, and the
new error messages and login backoffs.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants