Skip to content

osquery extensions delivery - #1096

Merged
javuto merged 1 commit into
developfrom
osquery-extensions
Oct 18, 2026
Merged

javuto merged 1 commit into
developfrom
osquery-extensions

Conversation

@javuto

@javuto javuto commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

osquery extensions delivery

Companion to the osctrld change that installs extensions safely (verified, root-owned, with process checks, rollback and per-extension status). Builds on per-node flags overrides (#1093).

Problem

osquery extensions add tables osquery doesn't ship, such as the trailofbits/osquery-extensions collection, and command execution, the typical investigation tool. osctrl had no way to distribute them. osctrld already called POST /{env}/osctrld-extensions, but osctrl didn't serve it.

What changes

Library: pkg/extensions

  • Extensions: name (^[A-Za-z0-9_-]{1,64}$, unique ignoring case), description, version, and a command execution marker.
  • Files: one per platform/architecture: darwin {amd64, arm64, universal}, linux {amd64, arm64}, windows {amd64}.
    • Up to 64 MB each, with SHA-256 and size computed by osctrl on upload.
    • Stored in the database as 1 MiB chunks shared by content hash. The column maps to longblob/bytea/blob; no 64 MB buffers anywhere.
    • Truncated uploads are detected and stored nowhere.
  • Assignments:
    • Ordinary extensions per environment.
    • Command-execution extensions per host, with optional expiry.
    • This split is enforced at assignment time and again when files are served, whatever rows exist.
  • Effective set per host: the environment's extensions plus the host's unexpired command-execution assignments, picked by platform and architecture. Darwin falls back to universal; extensions without a file for the platform are skipped.
  • Per-extension status reported by osctrld: ok, rejected, kept (a removal failed) and removed.
  • Lingering: a host is marked lingering when a command-execution extension reports kept and the host has no unexpired assignment for it.
    • Removing the assignment keeps the kept record.
    • Deleting the extension, or turning its command-execution marker off, is refused while any host reports kept.
  • Deletion guards: an extension can't be deleted, or have its marker flipped, while it's assigned.

osctrl-tls (under osctrld.enabled, authenticated with the environment secret)

  • List: POST /{env}/osctrld-extensions with {secret, uuid?, platform, arch, status?} returns [{name, sha256, size, url}].
    • The UUID is used only when it's a node of that environment; the node row's UUID is used for the set and the status record.
    • Old osctrld clients, which send no platform/arch, get [].
  • Download: POST /{env}/osctrld-extension with {secret, sha256}.
    • Streams the file with Content-Length.
    • Only serves files assigned in that environment, by content hash.
  • Route registration moved into registerRoutes(), with a test that registers every route together. The originally planned /{env}/osctrld-extension/{sha256} panicked at startup against /{env}/{secret}/osquery-config.

osctrl-api

  • Library (super admins only; denials audited):
    • GET|POST /api/v1/extensions
    • PATCH|DELETE /api/v1/extensions/{id}
    • PUT|DELETE /api/v1/extensions/{id}/files/{platform}/{arch}
  • Upload: multipart field file, streamed with MultipartReader.
    • The body cap is 65 MiB; a file over 64 MB gets 413, an empty file 400.
    • A 10-minute read deadline applies.
    • The filename is sanitized, and the description and version are capped.
  • Environment (environment admins):
    • GET|PUT|DELETE /api/v1/environments/extensions/{env}[/{id}].
    • Only ordinary extensions are offered and accepted; command-execution ones get 409.
  • Node:
    • GET /api/v1/nodes/{env}/node/{uuid}/extensions, for environment admins: the effective set, assignments, statuses and lingering rows. The command-execution library is included for super admins.
    • PUT|DELETE /api/v1/nodes/{env}/node/{uuid}/extensions/{id}, super admins only.
      • The body must be exactly {"expires_at": <future RFC3339> | null}. A missing or misspelled key is refused rather than silently granting "never expires".
      • A removal that removes nothing returns 404 and isn't audited.
  • Audit: every upload, file change, extension create/update/delete and assignment change, with the environment set where applicable.
  • Node deletion (API and CLI database mode) removes the node's assignments and statuses.
  • OpenAPI is regenerated.

SPA

  • Admin › Extensions (super admins):
    • library table: command-execution badge, files with size and SHA-256, where assigned;
    • create and edit;
    • upload or replace per platform, with a client-side 64 MB check;
    • delete disabled while assigned.
  • Environment › Configuration › Extensions: assign or remove ordinary extensions.
  • Node › Configuration › Extensions: the effective set with its source, availability for the host's platform, and the last status as host-reported plain text.
    • Super admins get Allow command execution, behind a confirmation with the "I understand … I know what I'm doing" checkbox, with optional expiry and remove.
  • Node banner:
    • "Command execution is allowed … via {extension}", only while osctrld reports it running;
    • "… assigned … but is not running" otherwise;
    • the expired wording;
    • "Command execution via {extension} was removed, but osctrld could not remove it from this host: {error}" for lingering hosts, which takes precedence.

Deployment and docs

  • nginx /api/ client_max_body_size raised from 64m to 65m in deploy/nginx/frontend.conf, deploy/docker/conf/nginx/frontend-dev.conf, deploy/cicd/nginx/frontend.conf and deploy/cicd/nginx/frontend-tls.conf.
  • ARCHITECTURE.md and README updated.

Security and operational impact

  • Who can do what:
    • Only super admins can put a binary into the library.
    • Only super admins can allow command execution, and only on single hosts.
    • Environment admins can assign ordinary extensions to their own environment only.
    • All checks are server-side, with CSRF on mutations; denials and changes are audited.
  • Integrity: osctrld installs only files whose size and SHA-256 match osctrl's record, fetched from its own configured osctrl.
  • Downloads are scoped to the environment and content hash, not per host. Any osctrld holding the environment secret and a file's hash can fetch it. Using it still needs root on that host to load it, and each host's list is per host.
  • Revoked command execution stays visible: a command-execution extension a host failed to remove keeps showing on the node page until osctrld reports it removed.
  • Delivery: only hosts running osctrld in service mode get extensions, on its next sync (default 60 minutes).
  • Schema: new tables via AutoMigrate (extensions, extension_files, extension_chunks, env_extensions, node_extensions, node_extension_statuses) with explicit idx_ext_* index names. No changes to existing tables.
  • Deployments using provision.sh: the direct API vhost inherits nginx's global 20 MB body limit (documented). Uploads over 20 MB through it fail until client_max_body_size is raised there.
  • Mixed versions:
    • Older osctrld clients get an empty list.
    • Older osctrld clients report a failed removal as rejected, which isn't flagged as lingering.
  • Known limits:
    • A host's kept record that never reports again blocks deleting that extension until the node is deleted.
    • Expiry is changed by re-running Allow.

Validation

  • go test ./... -count=1: 57 packages pass
  • go vet ./...; gofmt -l pkg cmd (empty); golangci-lint run ./...: 0 issues
  • make openapi-check: clean
  • cd frontend && npm test: 560 tests pass; npm run check: clean
  • New tests, each seen failing first:
    • pkg/extensions:
      • name validation, including case-insensitive uniqueness;
      • chunking, shared chunks and cleanup;
      • exactly 64 MiB accepted and 64 MiB + 1 refused, empty and truncated uploads refused;
      • assignment split, also enforced at serve time with rows inserted directly;
      • effective set: expiry, universal fallback, skipped platforms;
      • InEnvSet;
      • status validation, truncation and lingering states;
      • delete and marker-flip guards.
    • cmd/tls:
      • list for environment and node, unknown/foreign/malformed UUIDs, empty list for old clients, bad platform, wrong secret;
      • streaming download, replaced or foreign files (404), bad SHA, wrong secret, client disconnect;
      • full route registration.
    • cmd/api and cmd/cli:
      • super-admin gating with audited denials;
      • streaming upload limits;
      • 409 guards;
      • environment-admin scoping, cross-environment 404;
      • strict expiry body, 404 for removals that remove nothing;
      • lingering in the node view, including the expired case and no false alarm after a quick revoke;
      • node deletion through the API and the CLI.
    • Frontend:
      • the library page (gating, upload FormData, 64 MB check, delete disabled while assigned);
      • the environment tab;
      • the node view: super-admin-only allow flow with the checkbox, expires_at sent, plain-text host errors;
      • every banner variant, including lingering over expired;
      • no requests for non-admins.
  • Not run: a live host loading an uploaded extension, Windows on a real host, Playwright.

@javuto javuto added ✨ enhancement New feature or request osctrld osctrld related issues labels Oct 17, 2026
@javuto
javuto merged commit 8f6e8b7 into develop Oct 18, 2026
8 checks passed
@javuto
javuto deleted the osquery-extensions branch October 18, 2026 06:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ enhancement New feature or request osctrld osctrld related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant