When a pending baseline change shows up in op review,
package ownership answers "does this look like an install." Change-source correlation
answers the other question: was there a change ticket, maintenance window, or approved
deploy that explains it? It queries whichever external ticket systems you've configured and
shows every match found — purely informational, never a gate. A pending change is still
only ever accepted via an explicit, signed op approve.
Request Tracker ships as the first reference source, under the same
Etminan Plugin API notification channels use.
verifier/src/change_source.rs opens, verifies, and executes a
separately-vetted script — it never implements a ticket system's API in Rust itself, and
it never executes anything not on an explicit, operator-maintained allowlist. Same
security model, same reason, as notification channels: this project has no in-process
HTTP client at all.
environment variables are the master switches: ETMINAN_CHANGE_SOURCES
(which sources run) and ETMINAN_CHANGE_SOURCE_PLUGINS_CONF
(where the allowlist lives). Both unset means the feature never executes anything —
off by default, purely additive when enabled.
Builds a TicketSQL search over a time window centered on the earliest
uncorrelated pending measurement's first-seen time, widened
±ETMINAN_RT_WINDOW_HOURS (default 24h) on both sides —
matching either a custom field or a plain Subject search
against the host id — and returns every matching ticket as JSON.
Needs: curl, jq, a date(1)
supporting GNU's -d
The plugin sends ETMINAN_RT_API_TOKEN in a plain
Authorization header on every request, and refuses to run
at all against an http:// base URL rather than send that
token in cleartext — a hard refusal (exit 2), not a warning.
etminan-verifier plugins install request-tracker is designed to
do both steps below at once — it verifies a catalog manifest's Ed25519 signature against a
compiled-in trust anchor before trusting anything it lists, then downloads the
script, hashes it, and writes the allowlist line itself. The sha256 pin still happens
either way; catalog install just adds a second, cryptographic layer in front of it instead
of it being the only thing standing between you and running someone else's script.
Not yet usable in practice: the command and its verification are shipped in
etminan-verifier, but nothing publishes a real, signed catalog
manifest at its default URL yet — see the plugin
directory for current status. Until that exists, this is the only real path:
Add it as one line in
/etc/etminan-verifier/change-source-plugins.conf (copy
from deploy/change-source-plugins.conf.example):
A stale or wrong hash means the verifier refuses to run the plugin at
all — a clear error, never a silent bypass. Recompute it every time you touch the
file. (This is exactly what plugins install does for you
when the source is in the catalog.)
In /etc/etminan-verifier/verifier.env — these are read
by the plugin, not the verifier binary:
ETMINAN_CHANGE_SOURCES is the master switch — unset or
empty, no plugin ever executes. Everything else only matters once a source's name is
both listed here and allowlisted in the previous step. Restart is never needed
for a config-only change: etminan-verifier is invoked
fresh per op check/run, not a
long-lived daemon reading env vars once at startup.
RT's TicketSQL date-literal format and your instance's actual custom field name are the two things this integration cannot know in advance — confirm them once before relying on it:
Paste that into RT's own Search → Tickets → Advanced and confirm it returns what you expect — a timezone mismatch or a wrong custom-field name shows up here, not as a silent zero-match during a real review. Then:
Unlike notify plugins (JSON on stdin, since finding text is free-form), a change-source plugin's whole job is querying an external system using the host and time it's given — a couple of scalar values as flags is natural, and lets a plugin be tested by hand trivially.
[
{
"id": "RT#4821",
"summary": "Patch Tuesday rollout — web fleet",
"status": "resolved",
"url": "https://rt.example.com/Ticket/Display.html?id=4821"
}
]
| Field | What it is |
|---|---|
| id | Whatever identifier makes sense for the source (RT ticket number
prefixed RT#, a ServiceNow change number, etc.) — free-form,
just needs to mean something to the operator reading it. |
| summary | Short human-readable description of the ticket/change. |
| status | The ticket's own status string (resolved,
approved, ...) — passed through as-is, not interpreted. |
| url | Optional deep link to the ticket. Currently stored end to end but
not yet rendered anywhere in op review's
output — send it anyway; it's wired up on the storage side already, just not surfaced to
the operator yet. |
Leave source out of the plugin's own output entirely — the
verifier sets it from the plugin's allowlisted name, never from anything the plugin prints,
so a plugin can't spoof which source a match came from. Exit non-zero on any real failure;
don't print partial or best-guess JSON.
If nothing matched, or correlation hasn't run yet for this host, one of these appears instead of the match line:
If more than one configured source (or, in principle, more than one RT ticket) matches, every one is listed:
None of this changes what op approve/op reject do — it's context for the operator making that call, never an
input to it.
FreeITSM (source) is a real,
free/open-source ITSM tool, shipped as this project's second reference
plugin (deploy/change-sources/freeitsm.sh)
— built directly against FreeITSM's actual REST API v1 source (its
/changes list filters and response shape,
its {"data":...}/{"error":...} envelope, and the UI's own
change-detail deep-link URL pattern), not a guessed generic ITSM
shape. Same external-plugin-process model as Request Tracker — no
verifier changes needed.
Requires an API key (System → API) with at least the
changes.read permission, sent as
Authorization: Bearer fitsm_....
Correlation matches on a change's title only — FreeITSM's
search doesn't expose a custom-field match the way RT's
CF.{Name} does — so name changes to
include the affected host_id, e.g. "web-01 kernel patch rollout".
ETMINAN_CHANGE_SOURCES=freeitsm # or "request-tracker,freeitsm" for both
ETMINAN_FREEITSM_BASE_URL=https://itsm.example.com
ETMINAN_FREEITSM_API_TOKEN=fitsm_abcdef0123456789...
ETMINAN_FREEITSM_COMPANY_ID=1 # optional, multi-tenant installs only
ETMINAN_FREEITSM_WINDOW_HOURS=24 # default shown
A real limitation of the underlying API, not this plugin: GET /changes only offers a lower-bound
modified_since filter, no matching upper
bound, so the window's far edge is applied client-side against the
newest 100 matches — disclosed in the plugin script's own header
comment, not hidden. Not yet verified against a live FreeITSM
instance — same "PLACEHOLDER" caveat this page's Request Tracker
plugin carries for TicketSQL specifics; confirm the exact permission
set and modified_since semantics before
trusting this in production. ETMINAN_FREEITSM_DEBUG_QUERY=1 prints the
constructed request to stderr for the same hand-verification purpose
as ETMINAN_RT_DEBUG_QUERY.
ServiceNow, Jira, an internal change-management tool — any system not shipped is the same contract as Request Tracker, and needs no verifier rebuild.
Accept --host <id> --anchor <RFC3339 timestamp>,
read whatever the source needs from its own environment (pick a distinct
ETMINAN_<X>_* prefix so it doesn't collide with
another configured source), print the JSON array contract above on stdout, exit
non-zero on any real failure.
Same two steps as Request Tracker: install to
/etc/etminan-verifier/change-sources/, add its path and
sha256sum to
change-source-plugins.conf, add its name to
ETMINAN_CHANGE_SOURCES (comma-separated — every configured
source runs, and every match any of them finds is kept, not just the first). A brand
new source starts out here, hand-hashed — getting it into
the certified catalog so
plugins install <name> can do this for the next
person is a separate, later step, not a prerequisite.
plugins verifyConfirms the allowlist entry and pinned hash both check out. Nothing else changes —
ChangeRecord, the storage, the perform_check wiring, and op review's rendering are all already source-agnostic.
Full security model (why root-owned, why content-hash pinned, what's re-verified on every single run) lives in the Etminan Plugin API — shared with notification channels, since both categories are the same underlying mechanism.