GitHub
10/17/2026, 11:47 PMPOST /{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 re…
jmpsec/osctrlGitHub
10/18/2026, 6:10 AM