60 Commits
Author SHA1 Message Date
mrhid6 09888825a8 feat(monitors): heartbeat token, ping recording and overdue verdict 2026-09-17 08:20:16 +00:00
mrhid6 64dbad1d20 refactor(monitors): extract applyTransition and add passive monitor model fields 2026-09-17 08:14:24 +00:00
mrhid6 934501f4ec fix(auth): store passkey backup flags so synced passkeys verify
Chart Release / chart (push) Successful in 19s
Server Deploy / deploy (push) Successful in 3m4s
go-webauthn refuses an assertion whose Backup Eligible flag differs from
the stored credential's. The flag was never stored, so it compared against
false and every synced passkey (iCloud Keychain, Google Password Manager,
1Password) failed with "Backup Eligible flag inconsistency" - in
passwordless sign-in, second-factor sign-in and step-up alike.

Registration now stores BackupEligible and BackupState. Rows registered
before this have no baseline, so their first verified assertion adopts the
signed flag and records it; a recorded value always stands, so a genuine
change is still refused.
2026-09-16 15:03:54 +00:00
mrhid6 25541345a8 feat(mfa): TOTP secrets, recovery codes and factor lookup 2026-09-16 08:30:55 +00:00
mrhid6 b78a9b3832 feat(mfa): user_mfa and webauthn_credentials collections 2026-09-16 08:27:54 +00:00
mrhid6 fbcf436ef6 feat: show Ubuntu phased updates apart and leave them out of pending counts
Chart Release / chart (push) Successful in 29s
Server Deploy / deploy (push) Successful in 5m9s
apt lists phased updates as upgradable while an upgrade defers them until
Ubuntu selects the host, so a freshly patched server kept reporting pending
updates. The agent now flags them; the server stores the flag and leaves them
out of patch run counts, and the server page shows them in their own section.
2026-09-15 14:55:30 +00:00
mrhid6 3f2d20868e fix(patching): final review fixes
Chart Release / chart (push) Successful in 19s
Server Deploy / deploy (push) Successful in 6m12s
- no dispatch in the last 15 minutes of a window; no-result timeout from dispatch time
- per-server output moves to patch_run_outputs (16MB document limit)
- reboot proven by a changed boot time; RebootTimeout 45m, ResultGrace 20m
- window update and delete are server-scoped against the policies using them
- scheduler puts the claim back on an error after it, so the next tick retries
- cancelled runs with failures alert; MCP apply_updates audits per server
- apply-updates 503 body documented; openapi regenerated
- web: cleared numeric fields no longer save as 0; Run now asks for confirmation
2026-09-15 13:49:28 +00:00
mrhid6 8f1ea6d5a0 feat: patch models, scoped collections and indexes; pin vantage-shared v0.5.0 2026-09-15 08:27:04 +00:00
mrhid6 6ee203f5e9 chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
Chart Release / chart (push) Successful in 20s
Server Deploy / deploy (push) Failing after 1m52s
2026-09-10 09:18:55 +00:00
mrhid6 3bdbf33f90 fix: scope monitor runner and workflow targets to the caller's fleet
GET /api/monitors and GET /api/monitors/:id returned Monitor.Runner
unfiltered; for an agent-pushed monitor that field is literally a server
ID, so a restricted token learned which out-of-scope server a monitor
runs on directly, not merely that one exists. services.RedactMonitorRunner
replaces Runner with models.RunnerRestricted when it names a server
outside the caller's scope, resolved once via the new
services.VisibleServerIDs rather than per monitor. The monitor itself is
still returned — a restricted operator may legitimately need to see that
it exists and is up or down — only the runner field goes neutral; omitting
the monitor entirely was considered and rejected as more surprising than
one field changing. Runner "server" (control-plane-run) is never
touched. The MCP list_monitors/get_monitor_status projections never had a
Runner field to begin with, so REST and the tool surface already agreed;
a comment now records why.

GET /api/workflows and GET /api/workflows/:id returned
Workflow.TargetServerIDs unfiltered — directly naming out-of-scope
servers, worse than a count. services.FilterVisibleServerIDs narrows the
list to what VisibleServerIDs admits and reports hidden (no count) when
at least one target was dropped; WorkflowResponse wraps *models.Workflow
with a scoped TargetServerIDs and a TargetsRestricted flag. TargetTags is
left untouched — the tag vocabulary is already ruled acceptable to
expose. The MCP list_workflows/get_workflow tools get the identical
treatment: list_workflows' target count is now based on the filtered ID
list, and get_workflow's workflowDetail carries the same
TargetsRestricted flag, so a model that sees a filtered target list and
then has run_workflow refuse the same workflow for out-of-scope targets
is not left concluding the refusal invented a problem the list never
mentioned.

All four routes recorded in serverScopedRoutes as true; none is
boot-enforced, for the same substring-filter reason as the key routes
added in the previous round.
2026-09-09 08:03:26 +00:00
mrhid6 f9df426e6c feat: allow an API token to be restricted to servers by tag 2026-09-08 13:34:58 +00:00
mrhid6 f9dec9b230 refactor: move the public host out to vantage-site and vantage-docs
site/ and sitesvc/ become web/ and server/ in vantage-site; docsite/
becomes the root of vantage-docs. Their images move with them, to
vantage/vantage-site/{web,server} and vantage/vantage-docs.

Nothing here imported any of them, and sitesvc turned out to read no
database at all, so both cuts are clean. docker-compose.site.yml is
deleted rather than emptied: every service it held now ships with the
repository that builds it, and deploy/docker/docker-compose.yml is once
again exactly a self-hosted install.

Corrects four comments that named sitesvc for work it no longer does.
2026-09-08 08:41:11 +00:00
mrhid6 3bf80a117b refactor: rename shared module to gitea.hostxtra.co.uk/vantage/vantage-shared 2026-09-08 07:34:53 +00:00
mrhid6 1c15961309 feat: status page schema, licence feature and indexes 2026-08-24 14:00:00 +00:00
mrhid6 22b99ff895 feat: Monitor grath zoom 2026-08-24 10:58:50 +00:00
mrhid6 2fab784ba7 feat: Monitor groups and chart information 2026-08-24 10:30:23 +00:00
mrhid6 aa1c8e4aa1 feat: Hide secrets on api and channels 2026-08-14 12:23:36 +00:00
mrhid6 b51e87477e feat: Report whether a managed host is waiting on a reboot 2026-08-13 10:39:27 +00:00
mrhid6 72e5228351 feat: Add the API token service
Mint, resolve, list and revoke, with the effective role capped at the
owner's and recomputed per request rather than frozen at creation.

Deleting a user deletes their tokens in the same call, so offboarding is
one action. Revoking somebody else's token answers not-found rather than
forbidden, since a 403 confirms the credential exists.

Also re-exports shared.APITokenMaxDays into server/internal/models,
following the existing ValidRole wrapper pattern, since the token
service needs it and it was never re-exported.
2026-08-12 14:28:10 +00:00
mrhid6 6ad65a1242 feat: Add the api_tokens collection and its indexes
The unique index on token_hash is what makes authentication an indexed
lookup rather than a scan, so this builder is fatal on failure like
EnsureAuthIndexes rather than warning like the secrets one.

Registered in ScopedCollections so instance purge reaches it.
2026-08-12 14:16:12 +00:00
mrhid6 0838d1d735 feat: models and indexes for the workload registry 2026-08-07 08:45:00 +01:00
mrhid6 3a6d24fe0e feat: models and indexes for package inventory and CVE findings
Adds server_packages, vuln_findings and vuln_alert_rules to
ScopedCollections rather than to a separate deletion list. purgeInstance
derives its collection list from that registry, so instance deletion
follows automatically and there is no second copy to drift.
2026-08-06 11:59:00 +01:00
mrhid6 d0e1cc4ad6 feat: cron arithmetic and persisted workflow schedules 2026-08-04 13:51:10 +01:00
mrhid6 d1b3cd2f74 feat: target workflow runs by tag selector 2026-08-04 13:36:43 +01:00
mrhid6 13cd41d202 feat: validate server tags and add the model field 2026-08-04 13:30:06 +01:00
mrhid6 1f08e90009 feat: Removed email alert settings 2026-08-03 14:40:15 +01:00
mrhid6 dde47de145 feat: auth provider REST API and public provider discovery 2026-08-03 10:51:35 +01:00
mrhid6 45f7c0c393 feat: add AuthProvider model and identity provider presets 2026-08-03 10:30:21 +01:00
mrhid6 dabe6fe3aa feat: Updated package path to match repo 2026-07-28 10:01:40 +01:00
mrhid6andClaude Opus 5 17bcf4b5b9 feat(shared): auth_source constants and hq_user_id on User
Nothing writes them yet. They land now so phases 2 and 3 do not require a
second rebuild of every service that consumes the shared module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:52:15 +01:00
mrhid6 c3e363eccc refactor(server): finish the Org to Instance rename
Private identifiers plan 0b's naming map missed, plus the OrgOIDC model type.
No wire format, database field or route changes.
2026-07-24 15:07:06 +01:00
mrhid6 4f041d2f4b refactor(server): rename Org to Instance
Adds migration 0004_org_to_instance, the ScopedCollections list, the
AssertNoScopedCollectionMissed boot check, and moves EnsureAuthIndexes into
its own file.

Two ordering constraints the rename exposed, both now enforced and commented:

- 0004 must run BEFORE EnsureAuthIndexes. The index builder creates
  instances.slug, which would create an empty instances collection and make
  0004 refuse to rename orgs onto an existing target.
- Migrations 0001 to 0003 run BEFORE 0004 and still read and write org_id, so
  they use a private legacyOrg struct rather than shared/models.
2026-07-24 13:58:41 +01:00
mrhid6 76d3111a72 refactor(server): use shared models, provision and indexes
Also removes the duplicate Slugify in stepscan.go; workflow step slugs now
use the shared definition too.
2026-07-24 13:42:44 +01:00
mrhid6 1a6cf03c03 feat: Removed comments 2026-07-24 09:51:30 +01:00
mrhid6 77a92787fb fix: repair migration collection names and cross-cutting scoping gaps
Findings from the final whole-branch review.

- scopedCollections named "audit" and "channels", but the code writes to
  audit_logs and notification_channels. On upgrade from single-tenant,
  legacy audit events and channels would never get org_id, becoming
  invisible to org-filtered reads while channels silently stopped firing
  — and the detection loop counted the wrong names, so the 0001 marker
  could be written having migrated nothing. Names fixed, plus migration
  0003 so an incorrectly-migrated instance converges with a fresh one.
- EnsureAuthIndexes failure is now fatal. GetUserByEmail is unscoped and
  the OIDC cross-org guard compares against whichever duplicate Mongo
  returns first, so users.email uniqueness is a security invariant, and a
  legacy collection with duplicate emails is the realistic upgrade case.
- Evict the per-org OIDC provider cache on save; rotating away from a
  compromised IdP previously had no effect until restart.
- Build the oauth2 config per request instead of mutating a shared cached
  pointer outside the mutex, which raced on RedirectURL between
  concurrent logins for the same org.
- Stamp org_id on console_sessions, incidents and monitor_rollups, the
  last collections with no tenant column. 0003 derives their org from the
  owning server/monitor rather than defaulting, so one org's console
  history and incident timeline cannot merge into another's.
- Seed default steps when an org is created, not only at boot.
- Reject an empty session OrgID at the middleware.
- Derive the app root label from APP_ROOT_LABEL instead of hardcoding
  "vantage", which silently disabled the host guard off that domain.
- Stop caching negative slug lookups, so a new org's subdomain resolves
  immediately.
2026-07-22 10:44:17 +01:00
mrhid6 aa31cd8a10 fix: validate roles, guard last owner, scope bootstrap status
Security review of e70b2f0. The UI gating was correctly backed by
RequireRole everywhere; these are the missing validation gaps behind it.

- UpdateUserRole and createOrgUser accepted any role string verbatim, so
  an admin could self-promote to owner, create an owner outright, or set
  a junk role that silently stripped a user's access. Roles are now
  whitelisted, only an owner may grant or remove the owner role, and an
  actor cannot change their own.
- Neither demote nor delete guarded the last owner, so an org could reach
  zero owners. Both now refuse when no owner would remain, returning 409.
  Self-delete rejected.
- CountUsers counted across all orgs, so a locked-out org could never
  re-bootstrap once another tenant existed, and the unauthenticated
  bootstrap-status endpoint reported instance-wide state. It now answers
  per-org on an org host, falling back to global only on the apex.
- HandleMe repeats the middleware's host/org check; it sits outside the
  middleware so it can still return its own 401.
- Post-bootstrap now sends the new owner to their org host's login page.
  The session cookie is deliberately scoped to the exact host, so the old
  redirect landed them unauthenticated.
- AuthProvider renders an error state instead of mounting the shell with
  a null user when /auth/me fails for a reason other than 401.
- api.ts unwraps {"error": ...} so these messages render as text.
2026-07-22 10:26:21 +01:00
mrhid6 e5363a64ee fix(server): per-org settings and ESO read token
The settings collection was a single global document, so every org
shared one SMTP config, alert config, retention policy and ESO read
token. GetSecretGroupDecryptedAny then flattened every org's secrets
for a group into one map, meaning any tenant's token read every
tenant's secrets.

- settings gains org_id; GetSettings/SaveSettings/RotateSecretsReadToken/
  GetWorkflowLogRetentionDays all take orgID
- VerifySecretsReadToken replaced by ResolveSecretsReadToken, which
  resolves the org from the presented token's hash; the ESO endpoint
  derives its org from the token rather than a session, since it is
  called machine-to-machine
- GetSecretGroupDecryptedAny deleted in favour of the org-scoped variant
- settings and token-rotation routes now require owner/admin
- offline sweep and log retention resolve org per server / per run
- migration 0002 stamps the legacy settings doc with the default org

Note: /api/settings now 403s for members; the web settings page needs a
matching role check.
2026-07-22 09:35:47 +01:00
mrhid6 a8771a6e4d style(models): gofmt org_id field alignment 2026-07-21 16:24:16 +01:00
mrhid6 502045d3af feat(models): add org/user/org_oidc models and org_id on scoped collections 2026-07-21 16:23:14 +01:00
mrhid6 0464c540b2 feat: edit monitors + notification channels; HTTP monitor insecure-TLS option 2026-07-21 14:55:41 +01:00
mrhid6 8f3a27100f feat(server): multi-channel monitor notifications 2026-07-21 14:22:55 +01:00
mrhid6 e019493087 feat(server): monitor model + checker package 2026-07-21 14:10:46 +01:00
mrhid6 03e2c3c50d feat(proto): add ReportInventory RPC and inventory model 2026-07-21 14:04:28 +01:00
mrhid6 434f14ae3a feat(server): inline step ref + workflow validation 2026-07-21 10:15:52 +01:00
mrhid6 d905c99d32 feat(server): stream step logs to files, drop log bodies from run docs 2026-07-20 15:18:35 +01:00
mrhid6 85e1baf59a feat(server): workflow log-writer registry, retention setting, sweeper 2026-07-20 15:18:35 +01:00
mrhid6 05cd8e154b feat(workflows): step input params model + cascade step delete 2026-07-20 14:34:50 +01:00
mrhid6 47690c58d9 fix: Fixed workflow id 2026-07-20 12:49:43 +01:00
mrhid6 4872a26786 feat(models): add workflow, step, and run models 2026-07-20 11:31:37 +01:00
mrhid6 a9d602d021 feat: harden console sessions + complete protocol support
- single-use session tokens (atomic ConsumeSessionToken) + user-bound tunnel (actor must match session opener)
- wire VNC end-to-end (stash/consume password, connect+tunnel, frontend password field)
- passphrase-protected SSH keys: passphrase_enc on Key model, capture on upload, decrypt + pass to guacd
2026-07-17 12:19:07 +01:00