Commit Graph
537 Commits
Author SHA1 Message Date
naiba a9d14a0dfa chore(frontend-templates): add Nezha-Pixel template 2026-08-01 20:21:57 +08:00
naiba f9e7cfa54b chore(frontend): update bundled template versions 2026-08-01 20:21:57 +08:00
9ec6164f58 fix(security): harden server and service deletion lifecycle (#1220)
* test: TDD regression tests for GHSA-jx78-55p5-rwv5 stream quota enforcement

* Apply remaining changes

* fix: update action SHA allowlist and test assertions to match dependabot bump

* fix: close GHSA-jx78-55p5-rwv5 incomplete fix of GHSA-qjpp-gffx-2wm9

Finding 1 (Moderate): nil-guard reporterServer in delayCheck and notifyCheck.
ServerShared has its own lock independent of serviceResponseDataStoreLock, so
m := ServerShared.GetList() taken inside the worker can return a nil entry for
the reporter if the server was concurrently deleted. Previously this caused an
unrecovered SIGSEGV in the worker goroutine (and in the gRPC layer with no
recovery interceptor), taking down the whole instance.

Finding 2 (Low): nil-guard ss.services[id] in ServiceSentinel.Delete().
A caller-supplied id that is absent from the registry caused
ss.services[id].CronJobID to panic, aborting the Delete loop and leaving every
subsequent valid id as a zombie service (DB row deleted, in-memory entry kept,
cron probe still running).

Regression tests added for both findings following the existing
servicesentinel_lifecycle_test.go patterns.

* Apply remaining changes

* chore: replace commit hashes with version tags in test.yml

* fix(server): serialize authoritative lifecycle changes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(service): bind reports to reporter lifecycle

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(rpc): reject results from stale task streams

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(agentcompat): allow version-tagged actions

* fix(agentcompat): allow literal checkout refs

* refactor(agentcompat): remove SHA resolver policy

* test(agentcompat): remove resolver SHA fixtures

* test(agentcompat): remove mutable ref fixtures

* test(agentcompat): use tagged actions in secure fixtures

* test(agentcompat): update credential fixtures for tags

* test(agentcompat): update reusable action fixtures

* test(agentcompat): update artifact redaction fixtures

* test(agentcompat): finish artifact fixture tag migration

* test(agentcompat): update workflow validation fixtures

* test(agentcompat): update dependency workflow fixture

* ci(agentcompat): stop pinning cross-repository revisions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: naiba <hi@nai.ba>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-01 15:45:20 +08:00
naibaandnaiba/CloudCode 5d7e8b58af chore(frontend): update bundled template versions
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 13:11:43 +00:00
naibaandnaiba/CloudCode c893aa5786 fix(service): serialize reports with service lifecycle
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-21 13:11:05 +00:00
naibaandnaiba/CloudCode 391f622c75 test(agentcompat): cover SQLite hold arbitration
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:37:59 +00:00
naibaandnaiba/CloudCode 52c766d27c test(rpc): cover reentrant state observers
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:36:50 +00:00
naibaandnaiba/CloudCode 3f6de265a0 fix(mcp): harden dashboard dispatch lifecycle
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:32:16 +00:00
naibaandnaiba/CloudCode 0c6eb416a5 feat(agentcompat): add SQLite hold scoped attribution
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:29:21 +00:00
naibaandnaiba/CloudCode 26e92da33b feat(agentcompat): add scoped IO stream capabilities
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:27:33 +00:00
naibaandnaiba/CloudCode c756ef9385 fix(rpc): make IO stream lifecycle race-safe
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:26:19 +00:00
naibaandnaiba/CloudCode 8b47ff141f feat(agentcompat): add shared runtime contracts
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-20 04:25:03 +00:00
naibaandnaiba/CloudCode b01ff45ef4 chore(frontend-templates): bump user-dist v2.4.1 and aobobo-dist v1.4.6
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-12 06:27:58 +00:00
naibaandnaiba/CloudCode 1477138dad chore(frontend-templates): bump admin-dist v2.2.5, user-dist v2.3.2 and aobobo-dist v1.4.5
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-07-04 11:08:03 +00:00
naibaandnaiba/CloudCode fd31e5f1b0 chore(frontend-templates): add aobobo template
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-06-28 01:09:23 +00:00
naibaandnaiba/CloudCode d750fa035d chore(frontend-templates): bump user-dist v2.3.1
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-06-20 11:01:54 +00:00
naiba 39d398066d fix(api): redact third-party credentials in ddns/notification list
GET /api/v1/ddns and /api/v1/notification returned full objects with
plaintext credentials (Cloudflare/TencentCloud secrets, webhook URLs
with embedded bot tokens, Authorization headers). Redact these fields
in the list responses.

Since the frontend edit form repopulates from the list endpoint, the
update handlers now treat an empty submitted credential as "no change"
and preserve the stored value, preventing accidental secret wipes.

Ref: GHSA-ww5p-j6cj-6mqq
2026-06-20 01:54:50 +00:00
naiba d556c3216f chore(frontend-templates): bump user-dist v2.3.0 and nazhua-dist v1.1.0 2026-06-20 00:40:38 +00:00
naiba 8cd2da79b8 chore(frontend-templates): bump admin-dist v2.2.4 2026-06-20 00:39:47 +00:00
hamster1963 7a61da098b fix(service): filter service stats based on user permissions 2026-06-20 00:45:53 +08:00
naiba 576e6773c0 fix(release): bump user-dist to v2.1.2 (v2.1.1 never released upstream)
The v2.2.2 Release workflow failed: fetch-frontends.sh fetched user-dist
from hamster1963/nezha-dash-v2 at v2.1.1, but upstream went v2.1.0 -> v2.1.2
and never tagged v2.1.1, so the dist.zip download 404'd and unzip aborted the
build. Point at the existing v2.1.2 asset.
2026-06-07 16:23:24 +00:00
naiba b12b1709d9 test(alert): pin sample-slice memory-boundedness invariant
Add TestCheckStatus_SampleMemoryBounded asserting that, across 100k ticks,
the per-(alert,server) sample slice length and capacity stay bounded by the
rule's retention window and never grow with elapsed time.
2026-06-07 16:14:31 +00:00
naiba e756b4540c fix(alert): keep sample history by rule-defined retention window
Trim alert samples using AlertRule.RetentionWindow() (max rule duration)
instead of Check()'s verdict max, which is 0 while a rule's window is still
filling. The old max<=0 trim wiped history every tick, so Duration>=2 general
rules never accumulated enough samples and never fired a notification.

Add model + end-to-end singleton regression tests covering sample
accumulation, the retention-window contract, and the actual notify path.
2026-06-07 16:11:43 +00:00
naiba 66de244c2e feat(oauth2): add dashboard_host setting for callback URL
Decouple OAuth2 callback host from agent install host. When dashboard_host
is empty, the request Host is passed through; otherwise non-reserved hosts
are pinned to dashboard_host. Added to reserved-host allowlist for NAT.
2026-06-06 05:03:12 +00:00
naiba c595728609 chore(frontend-templates): bump admin-dist v2.2.2 and user-dist v2.1.1 2026-06-06 04:55:31 +00:00
naiba 6e9510d26a chore(frontend-templates): bump admin-dist v2.2.0 2026-06-05 02:42:58 +00:00
naiba 5fd91c44b0 test(rpc): cover both-key-styles present in agent auth metadata
After #1197/#244 a new agent emits both hyphenated and underscore credential
metadata, so a new dashboard receives both at once. Add tests for the everyday
both-present case and pin that the hyphenated key takes precedence over the
underscore alias that Caddy v2.11.4 strips.
2026-06-05 02:42:58 +00:00
naiba 0759f5c801 fix(alert): prevent divide-by-zero panic and unbounded growth on zero-duration rule
A general alert rule with Duration:0 (accepted by the API, no minimum
validation) drove fail*100/total with total==0, panicking with an integer
divide-by-zero inside checkStatus, which has no recover and would take down the
whole alert goroutine — a config-reachable DoS by any user able to create alert
rules. Skip duration<=0 rules in Check via continue (not boundCheck, which would
pollute hasPassedRule and skip sibling valid rules). Also trim the sample slice
when max<=0 in alertsentinel, otherwise it appended every tick without ever
trimming. Adds regression tests for zero-only and mixed rule sets.
2026-06-05 02:42:49 +00:00
naiba 36240f5888 fix(rpc): cap concurrent IO streams per user and per server
GHSA-jg62-j5h6-8mpq: the terminal and file-manager endpoints created unbounded
IO streams; an authenticated member could open thousands, each spawning
goroutines, a 1MiB buffer and an agent-side PTY, exhausting dashboard and agent
resources. CreateStream now enforces a per-user (20) and per-server (40) cap in
the existing ioStreamMutex critical section, using the stream map as the single
source of truth. Dashboard-internal streams (uid==0: NAT, server transfer, MCP
transfer) skip the per-user cap but still count per-server. Adds caps,
exemption, slot-release and no-leak regression tests.
2026-06-05 02:42:38 +00:00
zhdsmy 4289d30357 fix(auth): accept hyphenated agent metadata (#1197) 2026-06-05 09:06:38 +08:00
naiba c3c361ad66 fix(service): flip CopyStats public filter to HideForGuest
Completes the EnableShowInService->HideForGuest rename; the public stats
filter was missed in the previous commit, leaving service/singleton
referencing the removed field and breaking the build.
2026-06-02 03:32:10 +00:00
naiba e47b5b99f8 chore(frontend-templates): bump admin-dist v2.1.2 and user-dist v2.1.0 2026-06-02 03:31:36 +00:00
naiba 9b5199ac6c chore(frontend-templates): bump admin-frontend to v2.1.1 2026-05-31 15:14:48 +00:00
naibaandcloudcode 34ab8a31bc chore(frontend-templates): bump admin-frontend to v2.1.0
Embed the admin-frontend v2.1.0 release (PAT UI, CSRF handling, reserved
hosts) so the upcoming backend v2.1.0 ships the matching frontend.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 10:26:41 +00:00
naibaandcloudcode 7432efde04 chore(frontend-templates): bump user-frontend to v2.0.4
Upstream hamster1963/nezha-dash-v2 v2.0.4 reviewed commit-by-commit for
supply-chain risk (dependency bumps only, no new packages/postinstall
scripts/network calls/obfuscation) and mirrored to nezhahq/user-frontend-backup.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 09:49:57 +00:00
naibaandcloudcode d4837dfc7a fix(security): reserve dashboard hosts from NAT and re-check DDNS profile ownership at worker time
GHSA-x6fg-52vr-hj4w: NAT is a commonHandler any authenticated member can
create, and newHTTPandGRPCMux matches r.Host against NAT before dispatching
the dashboard/gRPC handlers, so a member could register the dashboard's own
host as a NAT Domain and hijack global routing. Add IsReservedDashboardHost
(InstallHost/ListenHost plus an operator-declared ReservedHosts list for
reverse-proxy deployments), reject reserved hosts on create/update, and drop
pre-planted records when building the startup cache.

GHSA-39g2-8x68-pmx8: bind-time CheckPermission let a member pre-bind a DDNS
profile ID that the victim would only create later. GetDDNSProvidersFromProfiles
now re-validates ownership by server owner UID and skips foreign-owned
profiles (UserID==0 is treated as a migration artifact, not an admin grant).

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-31 02:04:35 +00:00
naibaandcloudcode e8dabf5bc6 feat(auth): add PAT auth, scoped REST/MCP access, CSRF, and tenant isolation
Introduce Personal Access Tokens (nzp_*) as a stateless auth path alongside
JWT, gated per-endpoint by a scope middleware (nezha:{resource}:{verb}) with
fail-closed empty-scope defaults and a server-id whitelist. Self-management
endpoints (profile, api-tokens, oauth2 bind, refresh-token) explicitly reject
PATs to block privilege-escalation chains. A revoke registry tears down active
long-lived connections (terminal, fm, ws, transfer, mcp) the moment a PAT is
deleted, with a tombstone closing the revoke->register race.

Add an MCP endpoint that proxies tool calls (exec, fs read/write/delete,
transfer) to agents over gRPC, guarded by origin/DNS-rebinding checks, a
per-token rate limiter, audit logging, and a kill switch. Serialize all
sends through the IOStream wrapper to honour grpc-go's concurrency contract.

Add CSRF double-submit protection on unsafe cookie-authenticated methods,
exempting authenticated PAT requests by context identity (not a forgeable
Authorization header). Apply visibility/whitelist filtering consistently
across list, get-by-id, and mutate paths to enforce tenant isolation.

Migrate legacy mcp:* scopes: rewrite read/exec to nezha:* equivalents and
drop dangerous write/delete/wildcard grants.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-30 15:56:44 +00:00
029695344c chore(frontend-templates): bump admin-frontend to v2.0.9
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 07:12:31 +00:00
naibaandcloudcode e05700c2f0 feat(jwt): server-side session table with keyId + obfuscated uid claims
Replace the {user_id, ip} claim pair with {keyId, uid}:
- keyId is a 32-byte random id that points to a row in the new
  jwt_sessions table holding the real user id, bound IP, UA hash,
  TokenVersion and expiry.
- uid is the user id encoded through pkg/idcodec; mismatch between
  claim uid and session.UserID trips WAF block on the caller IP.
- identityHandler now rejects unknown/revoked/expired sessions, IP
  drift and stale TokenVersion. Refresh updates session.ExpiresAt.

User.TokenVersion bumps on password change and revokes outstanding
sessions, so a leaked JWT secret alone is no longer enough to forge
a token. JWTSession rows are GC'd every 10 minutes (expired + grace
or revoked >24h). OAuth2 callback shares the same issue path.

Includes regression tests for happy path, mismatched claim uid,
revoked session, TokenVersion bump, IP drift and unknown keyId.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-26 03:51:05 +00:00
naiba d202ecf8db chore(frontend-templates): bump admin-frontend to v2.0.8 2026-05-25 11:42:39 +00:00
naiba b83d93fdc9 fix(server-transfer): break loader sort ties by transferID
NewServerTransferClass merged Verified and acked-rollback rows and sorted
them by AckedAt only. On Windows time.Now() granularity is ~15.6ms, so
MarkVerified and an immediately-following MarkRevertDelivered routinely
share a timestamp. The stable sort then left the Verified candidate
ahead of the rollback that is actually on disk and the agent was locked
out on the next restart. Add transferID as a deterministic tiebreaker so
the later rotation always wins.
2026-05-25 10:36:43 +00:00
naiba 6b88cdb012 feat: server transfer rotation 2026-05-25 10:17:34 +00:00
naibaandnaiba/CloudCode 8d21062c3b fix(rpc): authorize agent task results
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-21 02:02:02 +00:00
naibaandnaiba/CloudCode 0f7af0bcb2 fix(rpc): bind io streams to target agents
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-21 02:01:54 +00:00
naibaandnaiba/CloudCode 1efe2bf053 fix(rpc): allow global agent secret across server owners
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-19 01:58:53 +00:00
naibaandnaiba/CloudCode 7c493e8f0b fix(rpc): verify gRPC client UUID belongs to the agent secret owner
Auth.Check resolved client_uuid → server.ID via UUIDToID without
checking that the resolved server's UserID matched the user that the
secret was bound to. An agent presenting one user's secret could
target a different user's server UUID and impersonate it — poisoning
monitoring state, triggering alerts, or quietly receiving tasks the
real owner expected.

Add authorizeAgentForUUID: if the UUID is unknown we still allow new
registration bound to the secret owner; if the UUID is known but
points to someone else's server we reject. The rejection message
"client UUID does not belong to the agent secret owner" also helps
operators trace which user's secret has leaked.

Note: this changes runtime behaviour after batch-move/server — the
new owner must reconfigure agents with their own secret. That is the
correct contract; the previous behaviour was a cross-user reporting
bug.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:17:37 +00:00
naibaandnaiba/CloudCode 280e34977f fix(rpc): enforce magic ff05ff05 on IOStream init
The inline magic check expressed the *invalid* form as
\`byte0 != 0xff && byte1 != 0x05 && byte2 != 0xff && byte3 == 0x05\`,
relying on && to detect a four-byte mismatch. Because && short-circuits,
any payload whose byte0 happened to be 0xff was treated as a valid magic
even if the remaining bytes did not match — almost every random payload
slipped through and only the stream-UUID layer above stood between a
caller with a valid agent secret and a live IOStream session.

Extract the check into isValidIOStreamMagic stated positively (all four
bytes must match) so short-circuit reasoning cannot reintroduce the bug.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:17:18 +00:00
naibaandnaiba/CloudCode 36297699f5 fix(rpc): bind io_stream sessions to creator to prevent terminal/fm hijack
createTerminal and createFM correctly check server ownership before
issuing a stream UUID, but terminalStream and fmStream only verified
that the UUID existed. Any authenticated user holding a valid stream
UUID could attach to it, gaining the original creator's live shell or
file-manager session — and the UUID is exposed via URL path (referer
leaks, access logs, browser history, frontend error reporters).

Bind the creator user ID into ioStreamContext at CreateStream time,
expose StreamOwnership and IsStreamAuthorizedForUser, and check
ownership in terminalStream/fmStream before the WebSocket upgrade so a
rejected attempt does not tear down the legitimate stream via defer.

NAT streams are also routed through CreateStream(_, 0); they are not
reachable from /ws/terminal or /ws/file so a sentinel user ID is fine.

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-18 15:16:40 +00:00
naibaandnaiba/CloudCode 854e2edf17 chore: bump admin frontend to v2.0.7
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00
naibaandnaiba/CloudCode 7061651587 test: cover multi-user permission boundaries
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-05-17 10:24:19 +08:00