Ekho-Labs / infrastructure-tf-kubernetes  ·  Pull Request #92  ·  task15/matrix-metadata-only → main

Metadata-only Matrix credential staging

One OnePasswordItem reference is added to the Prometheus Stack module so a Kubernetes Secret can be materialized later. Two contracts sit behind it: the source is pinned to a path and nothing else, and the resulting Secret's key names are checked by a separate values-free contract bound to that Secret's UID and resourceVersion. No credential value enters Terraform, no Matrix receiver is built, and nothing here asserts that the credentials work.

Open · not merged Hold — no merge, no apply Metadata only Semantic gate = Task 16 5 files · +359 / −2
1
Resource created
0
Deletes
0
Replace paths
5
Secret key names
0
Matrix receivers
Base
14ebd76300348d087dd1e0d05165d9c43253edc5
Head (frozen)
7bd591f7d15aac0cba05ab338d86d23069ec1fc1
Tree (head = merge preview)
606f6801517d0e2f29c63dcbed9ecd15b50352e6
Merge preview commit
976fa949d02ee686d2245a3cc7b39a55c28d4165

What this change is

Task 15 stages a credential reference. It writes a pointer to a 1Password vault item into the cluster and stops there. Everything that would prove the credential is usable — and everything that would actually deliver an alert — belongs to later, separately gated tasks.

What the PR does
  • Adds exactly one Terraform resource: a OnePasswordItem custom resource in namespace kube-prometheus-stack.
  • Points it at a single vault item path. The manifest carries a path and nothing else.
  • Adds a source validator that pins the manifest to an exact literal, requires spec to be exactly {itemPath}, and rejects four concrete mutations.
  • Adds a separate key-only contract that binds a materialized Secret by UID and resourceVersion, asserts the five key names, and structurally forbids any value field.
  • Adds a narrowly scoped exemption so the existing no-delivery guard tolerates the new resource's own name.
What the PR does not establish
  • It does not show the credentials are valid. Nothing authenticates against Matrix anywhere in this change.
  • It does not show Matrix delivery is active. No Matrix receiver, route, or webhook exists, and every alert still routes to the built-in null sink.
  • It does not yet confirm the real Secret's key names. The contract that checks them has run only against synthetic self-tests, because no Secret exists to inspect.
  • It was not applied. No merge, no terraform apply, no Secret read, no workflow dispatch.

The staged object

The entire resource added to tf/modules/prometheus-stack/main.tf, unchanged across both revisions of this pull request. The validator asserts the manifest is byte-for-byte this, has exactly the four keys apiVersion, kind, metadata, spec, and — as a separate assertion over the parsed configuration — that spec's key set is exactly {itemPath}, so no field-mapping block can be smuggled in.

tf/modules/prometheus-stack/main.tf  ·  lines 9–21+14
resource "kubernetes_manifest" "matrix_alertmanager_receiver" {
  manifest = {
    apiVersion = "onepassword.com/v1"
    kind       = "OnePasswordItem"
    metadata = {
      name      = "matrix-alertmanager-receiver"
      namespace = "kube-prometheus-stack"
    }
    spec = {
      itemPath = "vaults/kubernetes-vault/items/alertmanager_matrix_receiver"
    }
  }
}
Terraform address

module.prometheus_stack
.kubernetes_manifest
.matrix_alertmanager_receiver

Vault item path

vaults/kubernetes-vault
/items/alertmanager_matrix_receiver

Absent by assertion

No fields block, no value, no depends_on, no lifecycle. The validator requires the resource body to hold the single key manifest, and a negative case proves that adding spec.fields is rejected.

The five-key map

Five 1Password field labels are intended to materialize as five identically named keys on the resulting Secret. A dedicated contract checks those names against real post-apply evidence — never against their values.

1Password field labelKubernetes Secret key
1MATRIX_USER_IDMATRIX_USER_ID
2MATRIX_ACCESS_TOKENMATRIX_ACCESS_TOKEN
3MATRIX_ALERT_ROOMMATRIX_ALERT_ROOM
4ALERTMANAGER_BASIC_USERNAMEALERTMANAGER_BASIC_USERNAME
5ALERTMANAGER_BASIC_PASSWORDALERTMANAGER_BASIC_PASSWORD

Change map — staging, materialization, and the gate

Solid lanes are created or enforced by this PR. Dashed lanes are runtime behaviour or future work that this PR deliberately does not perform. Hover a lane to isolate it.

Terraform — this pull requestIn scope

Declares the pointer and nothing else. The manifest carries a path, not a value, so there is no credential for Terraform to render or persist; the reviewed plan artifact is redacted to structure only.

main.tf
kubernetes_manifest resource, metadata only
metadata validator
Pins the manifest to an exact literal and spec to {itemPath}; 4 negative cases
speculative plan
1 create · 0 delete · 0 replace
apply would create the custom resource — not performed in this PR
Kubernetes runtime — OnePassword Connect operatorOut of scope · runtime

Behaviour of the already-installed operator once the custom resource exists. This is where values first appear, entirely outside Terraform's view.

OnePasswordItem CR
matrix-alertmanager-receiver in kube-prometheus-stack
operator resolves itemPath
Reads the vault item through 1Password Connect
Secret materialized
Keys derived from the item's field labels
the resulting Secret is described as evidence — names, UID and resourceVersion only
Key-only Secret contractIn this PR · awaiting real evidence

Added by this change and bound to one exact Secret revision. It judges names, never values — and it is built so that a value cannot even be submitted to it.

bind UID + resourceVersion
Both required non-empty; pins the verdict to one Secret revision
assert 5 sorted key names
Exactly the expected set; a missing or extra key fails
forbid any value field
extra="forbid"; evidence carrying data is rejected outright
Trust boundary — staging ends here

Everything above concerns shape: a path, a namespace, a set of key names, a Secret revision. Even a fully satisfied key-only contract proves only that the right names arrived — never that the values behind them are correct, current, or accepted by anything. No semantic claim crosses this line.

Task 16 — the authoritative semantic gateNot in this PR

The sole place where the credentials are actually judged. It binds its verdict to the specific Secret it inspected, by UID and resourceVersion, so a later rotation cannot silently inherit a stale pass.

bind Secret UID + resourceVersion
Verdict is pinned to one exact Secret revision
whoami
Identity check against Matrix
join
Room membership check
send
End-to-end delivery check
only a passing Task 16 gate authorises receiver work
Task 17 — receiver workloadNot built · actively blocked

Zero Matrix delivery resources exist today. Alertmanager still declares a single built-in null receiver and routes every alert to it, which is the standing no-delivery safeguard. The Task 9 contract fails the build if core tries to add real delivery, and that guard is unchanged outside one path-scoped exemption.

Matrix receiver
Blocked: webhook keys are forbidden
route
Blocked: no matching route may exist
receiver Deployment / Service
Blocked: sensitive resource types

Plan outcome

The reviewed artifact is a redacted plan manifest — structure without values. Its schema is asserted, not assumed.

Observed — the only changed address
# module.prometheus_stack
#   .kubernetes_manifest
#   .matrix_alertmanager_receiver
actions       = ["create"]
delete_count  = 0
replace_paths = []
  • Exactly one non-no-op resource change is permitted; more than one fails the contract.
  • The validator accepts only ["create"] or ["update"]. The observed plan is create.
  • Any delete action or any replacement path fails closed — one of the three negative test cases proves it.
Redaction — how values stay out

The manifest may contain exactly three top-level keys, and each resource change exactly nine. The absent keys are the point:

# allowed per change
address, module_address, mode, type, name,
actions, replace_paths,
before_sensitive, after_sensitive

# absent by assertion — this is the redaction
before, after

Because before and after are rejected outright, the reviewed artifact structurally cannot carry a value, rather than merely happening not to.

Trusted plan evidence — bound to the frozen head
Workflow run
32178432037 · attempt 1 · success
Artifact
9339927303
terraform-speculative-plan-7bd591f7…
tfplan sha256
12d567ea66f90900efcea0074fdd65289a2dcf29a012761f097032960b899d75
plan manifest sha256
5221b470126d4fafab540ff7973534e2db5ec4b2576f7983efbd860dfe4f61d5
Remote run
run-difbTWYh4s9k25sW
Artifact expires
2026-08-19T19:47:35Z

File by file

Five files, all inside tf/modules/prometheus-stack. Nothing else in the repository is touched. Only the first carries infrastructure; the rest are contracts and CI wiring.

tf/modules/prometheus-stack/main.tf+14−0Modified

The staged resource itself, added above the existing Grafana password resource. It is the only Matrix-related resource in a 444-line module whose other twenty resources are Grafana, Prometheus, Pushgateway, ingresses and dashboard ConfigMaps.

Why here: the Secret must land in the same namespace as the Alertmanager that will eventually consume it, so the reference belongs to the Prometheus Stack module rather than a separate one.

tf/modules/prometheus-stack/tests/task15_metadata_validator.py+199−0Added

A standalone source validator. It parses main.tf with python-hcl2, requires the staging resource to be the sole one of its name, and compares the manifest against an expected literal — so any drift in path, name, namespace, or shape fails.

It then runs four negative cases, each a deep copy mutated and required to be rejected: a wrong itemPath, an added depends_on, an added spec.fields block, and a plan whose action is delete. Testing that the guard refuses bad input is what gives the positive assertion its weight.

What changed in the second round: the earlier FIELD_MAPPING constant and the assertion over it were removed outright. In their place the validator now asserts something it can actually observe — that spec's key set is exactly {itemPath}. Key names moved to the contract below, where they are compared against real evidence instead of against themselves.

tf/modules/prometheus-stack/tests/task15_secret_metadata_contract.py+139−0Added

The key-only materialization contract, and the substantive addition of this round. It models the evidence as frozen Pydantic types: an OnePasswordItemMetadata that must match the exact apiVersion, name, namespace and item path, and a SecretMetadata carrying name, namespace, a non-empty uid, a non-empty resource_version, and the tuple of key names.

Values are excluded structurally rather than by convention. Every model is declared extra="forbid" and none has a field for secret data, so evidence containing a data block fails validation instead of being silently dropped — and one of the four negative cases exists purely to prove that.

Note on invocation: the self-tests run unconditionally, but the real comparison happens only when an evidence file is passed as an argument. At this head none has been, because nothing has been applied.

tf/modules/prometheus-stack/tests/task9_contract/policy.py+5−0Modified

The existing no-delivery guard scans every walked location for the tokens matrix and /alerts/, plus the five Task 15 field names and the item-name token alertmanager_matrix_receiver. The new resource necessarily trips it: its Terraform name contains matrix and its itemPath contains alertmanager_matrix_receiver.

The fix is a path-scoped skip, not a token removal — and it suppresses only forbidden-string scanning. The sensitive-resource-type and manifest-kind checks still run over the new resource, which passes them on its own merits because OnePasswordItem is not a sensitive kind. Webhook-key detection and the alternate-config-source checks are likewise untouched, though it is worth being precise that those read the Helm values and the rendered Alertmanager config rather than the Terraform resource, so they never covered this resource to begin with.

tf/modules/prometheus-stack/tests/workflow_contract_check.py+2−2Modified

CI plumbing, not Task 15 semantics. Two pinned expectations in the workflow contract move: the apply gate now also accepts the remote run status planned_and_saved alongside planned, cost_estimated and policy_checked; and the expected actions/upload-artifact commit pin is bumped to a new SHA.

Why it appears here: the contract test asserts the workflow file byte-for-byte, so an unrelated workflow change cannot merge without this file moving with it. Worth separating mentally from the credential-staging story when reviewing.

Weak links and reviewer notes

The one substantive finding from the first review round was resolved in code rather than argued away. One new finding replaces it, surfaced by checking what the pipeline actually executes. The rest are seams a reviewer should hold in mind — each a consequence of Task 15 deliberately deferring meaning to Task 16.

MediumNone of these contracts are executed by CI+
WhatThe workflow at this head contains no reference to task15, task9, pytest, uv, or any .py path. The jobs that ran on this pull request are Terraform format, init, validate and a Trivy config scan — plus a kubeconform step that explicitly reports itself as skipped, since no static manifests can be rendered without credentials in fork-safe CI — followed by plan, manifest projection, hashing, artifact upload and evidence publication. No Python contract executes at any point.
ConsequenceThe metadata validator, the key-only Secret contract, the Task 9 no-delivery policy and the workflow contract check all protect only when someone runs them by hand. A future change could break the pinned manifest, add a spec.fields block, or widen the guard, and every check on the pull request would still report green.
EvidenceStep lists for run 32178432037, cross-checked against the workflow file at head 7bd591f7.
JudgementNot a defect in the contracts themselves, which are carefully built and genuinely strict. It is a gap between what they assert and what the pipeline enforces. Read checks pass on this pull request as "Terraform is well-formed and the plan is clean" — not as "the Task 15 contracts were verified".
FixEither wire the validators into a job so they gate the merge, or state plainly that they are reviewer-run tools so no one mistakes a green tick for contract enforcement.
LowThe key-only contract has never seen a real Secret+
WhatThe contract's real comparison runs only when an evidence file is supplied as an argument. Executed bare, as it is here, it exercises a synthetic fixture and four negative cases. Nothing has been applied, so no Secret exists to describe.
ConsequenceAt this head the contract is defined and self-tested, not satisfied. It demonstrates that it would reject a missing key, an extra key, a missing UID, or attached values — but it has not yet accepted anything real.
Blast radiusNone today, by construction. It becomes meaningful only once an apply produces a Secret and evidence is generated from it.
JudgementCorrect for a staging change under hold. The residual risk is procedural rather than technical: the contract only earns its keep if the post-apply step actually feeds it real evidence rather than skipping straight to Task 16.
LowThe guard exemption is a substring match on the location path+
WhatThe skip triggers when the constant .kubernetes_manifest.matrix_alertmanager_receiver appears anywhere in a walked location, so every nested location under that resource is exempt from forbidden-token scanning.
Why boundedThe metadata validator independently pins that same resource to an exact literal manifest with exactly four keys, and now additionally pins spec to exactly {itemPath}. A token the Task 9 guard ignores would still have to survive both equality checks.
Coupling riskThe checks are load-bearing together. Relaxing the pinning assertions silently widens the Task 9 exemption, and the connection is not obvious from either file alone.
SuggestionA comment in policy.py pointing at the pinning assertions would keep the pair from drifting apart.
LowThe no-Secret rule guards the Terraform surface, not the cluster surface+
Whatresource_failures blocks core from declaring a kind: Secret manifest, and OnePasswordItem is deliberately absent from the sensitive-kind set — so the new resource passes that check on its own merits, without needing the exemption.
NuanceA Secret is still created in-cluster, by the operator, as the intended outcome. The guard's promise is that Terraform does not author a Secret, not that no Secret comes into existence.
JudgementCorrect as designed and consistent with the metadata-only framing; worth stating plainly so the guard is not read as a stronger claim than it makes.
InfoResolved since the first revision: the self-referential key assertion is gone+
PreviouslyThe first revision carried a FIELD_MAPPING constant of five identical pairs and asserted that its left column equalled its right column. Both sides derived from the same literal, so the check was true by construction and could never fail — it validated a constant, not the configuration.
NowThe constant and the assertion were deleted. The validator instead asserts that spec's key set is exactly {itemPath} — a genuine statement about the parsed configuration — and a new negative case proves that adding spec.fields is rejected.
Key namesThey moved to a separate contract that compares them against supplied evidence describing a real Secret, bound to that Secret's UID and resourceVersion, with values structurally excluded. The claim each file makes is now a claim it can actually support.
CorroborationThe correction was not made in isolation: an intermediate release-packet refresh recorded the self-comparison as an explicit BLOCKED decision — noting that no independent Terraform or rendered-plan evidence proved the five labels — before the second commit landed.
InfoAn unrelated CI-contract change rides along in the diff+
Whatworkflow_contract_check.py moves two pinned expectations: the apply gate additionally accepts the remote run status planned_and_saved, and the expected actions/upload-artifact pin is bumped to a new commit SHA.
WhyThat test asserts the workflow file byte-for-byte, so a workflow change elsewhere forces this file to move with it. It is CI plumbing and carries no Task 15 semantics.
Reviewer noteJudge it on its own terms rather than as part of the credential-staging story — in particular the action pin, which is a supply-chain decision independent of this PR's subject.
InfoTwo staging records exist, and only the newer one binds this head+
The recordsThe original binding, task-15-metadata-staging-binding.json, names the first head 052418a0 with that round's artifact and digests. A later post-correction packet, task-15-release-refresh-20260818T194740Z.json, binds the current head 7bd591f7, tree 606f6801, run 32178432037, artifact 9339927303 and both digests. Evidence for this head does exist on disk.
Read the right oneLocal-validation claims carried in the original binding — the terraform test tally, the trivy deferral — describe the earlier revision and do not carry forward. The newer packet is the one that covers this head; it also records the key-only contract explicitly, with runtime_semantic_claim set to false.
On its stale flagThat newer packet marks this visualization stale, noting it references a previous head and digest packet. The note was written before the refreshed page was deployed, and the page you are reading is that refresh — built from head 7bd591f7 and the matching digest packet. The flag records a moment in time, not a defect in what is now published.
Artifact windowThe current artifact 9339927303 expires 2026-08-19T19:47:35Z. If the hold outlasts it, the digests can no longer be re-verified against a retrievable artifact and a fresh speculative plan is required.
InfoMerging introduces no content change, and the plan shape did not move+
WhatHead commit 7bd591f7 and merge-preview commit 976fa949 resolve to the same tree, 606f6801.
Across revisionsThe redacted plan manifest digest 5221b470… is unchanged from the first revision even though the head moved, which is independent confirmation that the second round touched test code only and left the planned infrastructure untouched.
ConsequenceThe reviewed tree and the tree that would land are identical, so the plan taken at the head remains representative of the merge result for as long as the base does not move.

Status

Hold — review pending, no merge and no apply REVIEW_PENDING_NO_MERGE_OR_APPLY

Every automated check that ran on the frozen head passed — Apply and Main Plan were skipped by design, not failed — and the change is tightly scoped to what Task 15 authorises. The hold is not a symptom of a failure; it is the design. Task 16 remains the sole gate that may assert anything about the credentials themselves, and no step in this pull request attempts that assertion.

Read the row below carefully. The green ticks cover Terraform and the plan; the contracts this change adds are not among them.

PASSSecurity Check — fork guard
PASSTerraform Static Checks — fmt, init, validate, Trivy
PASSTerraform Plan — plan, manifest, hash, upload, evidence
SKIPTerraform Apply — by design
SKIPTerraform Main Plan — by design
SKIPkubeconform — declared, not silently passed
NOT RUNTask 15 contracts — no CI job invokes them