|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) |
| 3 | += A2ML Attestations: What They Are and Where They Help |
| 4 | +:toc: left |
| 5 | +:toclevels: 3 |
| 6 | +:icons: font |
| 7 | +:sectnums: |
| 8 | + |
| 9 | +== Preface |
| 10 | + |
| 11 | +A2ML (Attested Markup Language) has two faces. One is a document format — a |
| 12 | +Djot-like markup with typed guarantees. The other, and the one that matters |
| 13 | +here, is the _attestation envelope_: a content-addressable audit record that |
| 14 | +proves a decision was made, what the inputs were, and that nobody tampered |
| 15 | +with the record afterwards. |
| 16 | + |
| 17 | +This document tells the story of actually using a2ml attestations in a |
| 18 | +production gateway and automation router. Where they help, where they are |
| 19 | +overkill, and what the terminology means. |
| 20 | + |
| 21 | +For the a2ml format spec: `standards/a2ml/SPEC.adoc` + |
| 22 | +For K9-SVC contracts (which pair with a2ml): `docs/K9-SVC-EXPLAINED.adoc` |
| 23 | + |
| 24 | +== The Problem: "Who Decided That?" |
| 25 | + |
| 26 | +You are debugging a production incident. A request to `/api/v1/users/42` was |
| 27 | +denied with 403. The caller says they should have been allowed. You check the |
| 28 | +logs: |
| 29 | + |
| 30 | +---- |
| 31 | +[2026-02-28 14:23:07] DENY trust=untrusted exposure=authenticated path=/api/v1/users/42 |
| 32 | +---- |
| 33 | + |
| 34 | +Okay, so the caller was untrusted and the endpoint requires authentication. |
| 35 | +But the caller says they sent a valid API key. You check the headers... and |
| 36 | +discover the log does not record headers. You have the _verdict_ but not the |
| 37 | +_evidence_. |
| 38 | + |
| 39 | +Now multiply this by a hundred decisions per second across three services. When |
| 40 | +something goes wrong, you need to answer: |
| 41 | + |
| 42 | +1. What was the exact input? |
| 43 | +2. What policy was applied? |
| 44 | +3. What was the decision? |
| 45 | +4. Can I prove the log entry has not been modified? |
| 46 | + |
| 47 | +Without attestations, you are reconstructing history from incomplete, unsigned |
| 48 | +log lines. |
| 49 | + |
| 50 | +== Chapter 1: "I'll Just Log More" |
| 51 | + |
| 52 | +Your first attempt is to add more fields to the log: |
| 53 | + |
| 54 | +[source,elixir] |
| 55 | +---- |
| 56 | +Logger.info("access_decision", |
| 57 | + trust: trust_level, |
| 58 | + exposure: exposure_level, |
| 59 | + path: conn.request_path, |
| 60 | + method: conn.method, |
| 61 | + client_ip: client_ip, |
| 62 | + headers: sanitised_headers, |
| 63 | + decision: "deny", |
| 64 | + policy_version: current_policy_hash |
| 65 | +) |
| 66 | +---- |
| 67 | + |
| 68 | +This is better. You can now see the full context. But: |
| 69 | + |
| 70 | +* **Logs are mutable.** Someone with access to the log aggregator can edit or |
| 71 | + delete entries. You have no proof that the log at 14:23:07 is the same log |
| 72 | + that was written at 14:23:07. |
| 73 | +* **Logs are not content-addressable.** If you have two log lines with the same |
| 74 | + timestamp, you cannot determine which is authentic. |
| 75 | +* **Log fields drift.** When you change the log format, historical entries |
| 76 | + become inconsistent with current entries. There is no schema enforcement |
| 77 | + across log lines. |
| 78 | + |
| 79 | +=== What a2ml Attestations Add |
| 80 | + |
| 81 | +An a2ml attestation wraps the decision in a content-addressable envelope: |
| 82 | + |
| 83 | +[source,elixir] |
| 84 | +---- |
| 85 | +%{ |
| 86 | + version: "1.0", |
| 87 | + type: :access, |
| 88 | + issuer: "http-capability-gateway", |
| 89 | + issued_at: "2026-02-28T14:23:07.123Z", |
| 90 | + decision_hash: "a3f8b2c4d5e6...", # SHA-256 of the decision payload |
| 91 | + decision: %{ |
| 92 | + trust: :untrusted, |
| 93 | + exposure: :authenticated, |
| 94 | + path: "/api/v1/users/42", |
| 95 | + method: "GET", |
| 96 | + verdict: :deny, |
| 97 | + policy_hash: "7b9e1f2a3c..." |
| 98 | + } |
| 99 | +} |
| 100 | +---- |
| 101 | + |
| 102 | +The `decision_hash` is the SHA-256 of the `decision` map, canonically serialised. |
| 103 | +If anyone modifies any field of the decision, the hash no longer matches. You can |
| 104 | +verify an attestation at any time by recomputing the hash and comparing. |
| 105 | + |
| 106 | +This is not blockchain. There are no blocks, no consensus, no distributed |
| 107 | +ledger. It is a hash. Simple, fast, tamper-evident. |
| 108 | + |
| 109 | +== Chapter 2: "Do I Need This for Every Request?" |
| 110 | + |
| 111 | +You instrument the gateway to produce an attestation for every access decision. |
| 112 | +At 1000 requests per second, you are now producing 1000 attestation records per |
| 113 | +second. Each one has a SHA-256 hash computation, a JSON serialisation, and |
| 114 | +a write to the audit store (Redis, in this case). |
| 115 | + |
| 116 | +After a day you check Redis: 86.4 million attestation records. Your Redis |
| 117 | +instance is using 12GB of memory for audit data. |
| 118 | + |
| 119 | +**This is where a2ml attestations become expensive.** Every-request attestation |
| 120 | +is appropriate for high-security systems (financial transactions, healthcare |
| 121 | +data access, regulatory compliance). For a general-purpose API gateway, it is |
| 122 | +overkill. |
| 123 | + |
| 124 | +=== What You Learn |
| 125 | + |
| 126 | +Attest _decisions that matter_, not every request: |
| 127 | + |
| 128 | +[horizontal] |
| 129 | +Always attest:: Access denials (every 403 needs a provable reason), trust level |
| 130 | +changes (escalation or de-escalation), policy reloads (the old and new policy |
| 131 | +hashes), circuit breaker state transitions. |
| 132 | + |
| 133 | +Optionally attest:: Access grants to sensitive endpoints (configurable per |
| 134 | +route), routing decisions in the automation router (content-addressable plan). |
| 135 | + |
| 136 | +Skip:: Health checks, metrics scrapes, static asset serving, internal |
| 137 | +keep-alive pings. |
| 138 | + |
| 139 | +The automation router is different — there, every routing decision IS the |
| 140 | +product. You want an attestation for every plan because the plan determines |
| 141 | +which backend runs which infrastructure operation. A wrong routing decision |
| 142 | +can destroy a production environment. The cost of one SHA-256 per routing |
| 143 | +decision is negligible compared to the cost of an unauditable deployment |
| 144 | +mistake. |
| 145 | + |
| 146 | +== Chapter 3: "The Sensitive Data Problem" |
| 147 | + |
| 148 | +Your attestation for a routing decision includes the full decision payload. |
| 149 | +That payload includes the parameters the user sent. Those parameters include: |
| 150 | + |
| 151 | +[source,json] |
| 152 | +---- |
| 153 | +{ |
| 154 | + "backend": "terraform-apply", |
| 155 | + "target": "production", |
| 156 | + "params": { |
| 157 | + "aws_access_key_id": "AKIA...", |
| 158 | + "aws_secret_access_key": "wJalrXUtnFEMI..." |
| 159 | + } |
| 160 | +} |
| 161 | +---- |
| 162 | + |
| 163 | +You just wrote AWS credentials to your audit log. In a content-addressable |
| 164 | +record. With a hash that proves it is authentic. |
| 165 | + |
| 166 | +**This is a real security failure.** The attestation system, designed to improve |
| 167 | +security, has created a credential leak. |
| 168 | + |
| 169 | +=== What You Learn |
| 170 | + |
| 171 | +The attestation layer MUST redact sensitive parameters before hashing. The |
| 172 | +implementation maintains a list of sensitive key patterns: |
| 173 | + |
| 174 | +[source,elixir] |
| 175 | +---- |
| 176 | +@sensitive_keys ~w(password token secret key credential api_key |
| 177 | + access_key private_key) |
| 178 | +---- |
| 179 | + |
| 180 | +When building the attestation payload, any key matching these patterns gets its |
| 181 | +value replaced with `"[REDACTED]"`. The hash is computed over the _redacted_ |
| 182 | +payload, so: |
| 183 | + |
| 184 | +1. The hash still proves the _structure_ of the decision (which backend, which |
| 185 | + target, how many parameters). |
| 186 | +2. The hash does NOT prove the _values_ of sensitive parameters (because they |
| 187 | + are redacted before hashing). |
| 188 | +3. No credentials appear in the audit log. |
| 189 | + |
| 190 | +This is a deliberate trade-off: you lose the ability to prove "the exact |
| 191 | +credential that was used" in exchange for not storing credentials in the audit |
| 192 | +system. For almost every use case, the structure proof is sufficient. |
| 193 | + |
| 194 | +== Chapter 4: "Can I Use This in Git Forges?" |
| 195 | + |
| 196 | +You want to attest commits. Every time someone pushes to your git forge, you |
| 197 | +generate an a2ml attestation recording the commit hash, the author, and the |
| 198 | +branch. |
| 199 | + |
| 200 | +This works — but it duplicates what git already provides. A git commit IS a |
| 201 | +content-addressable record. The commit hash IS a SHA (SHA-1 or SHA-256 with |
| 202 | +newer git). The commit metadata already contains the author, timestamp, and |
| 203 | +parent references. |
| 204 | + |
| 205 | +**A2ML attestations add nothing over git's built-in commit hashing for recording |
| 206 | +"who committed what."** Git is already a content-addressable audit trail for |
| 207 | +source code changes. |
| 208 | + |
| 209 | +=== Where a2ml DOES Help in Git Forges |
| 210 | + |
| 211 | +What git does NOT record: |
| 212 | + |
| 213 | +* **Policy decisions.** When a CI pipeline decides to auto-merge a Dependabot PR, |
| 214 | + what policy was applied? Was the merge based on passing tests, review |
| 215 | + approval, or a timeout? Git records that the merge happened but not _why_. |
| 216 | +* **Access control decisions.** When a push is rejected by branch protection, what |
| 217 | + rule rejected it? Git returns "rejected" but the decision reasoning is |
| 218 | + ephemeral. |
| 219 | +* **Cross-repo propagation.** When you mirror a commit from GitHub to GitLab to |
| 220 | + Codeberg, how do you prove the mirrors are faithful? Git hashes prove content |
| 221 | + integrity per-commit, but a2ml can attest the _propagation decision_ ("mirror |
| 222 | + triggered at T, source hash X, destination hash Y, match: true"). |
| 223 | + |
| 224 | +The forge integration point is the _automation around git_, not git itself. |
| 225 | +Attesting CI/CD decisions, merge policies, and cross-forge propagation gives |
| 226 | +you an audit trail for the stuff that happens _between_ commits. |
| 227 | + |
| 228 | +== Chapter 5: The Terminology |
| 229 | + |
| 230 | +[cols="1,3"] |
| 231 | +|=== |
| 232 | +| Term | Plain English |
| 233 | + |
| 234 | +| **attestation** |
| 235 | +| A record that says "at time T, system S made decision D based on inputs I." |
| 236 | +The record carries a hash that proves it has not been modified. |
| 237 | + |
| 238 | +| **content-addressable** |
| 239 | +| The attestation's identity IS its content. The ID is the SHA-256 hash of the |
| 240 | +decision payload. Same inputs → same hash. Different inputs → different hash. |
| 241 | +No separate ID generator needed. |
| 242 | + |
| 243 | +| **decision_hash** |
| 244 | +| SHA-256 of the canonically serialised decision payload. This is the |
| 245 | +tamper-evidence mechanism. Recompute the hash from the stored decision; if it |
| 246 | +matches decision_hash, the record is authentic. |
| 247 | + |
| 248 | +| **policy_hash** |
| 249 | +| SHA-256 of the policy rules that were in effect when the decision was made. |
| 250 | +If you later discover the policy was wrong, you can find all decisions made |
| 251 | +under that policy by searching for its hash. |
| 252 | + |
| 253 | +| **envelope** |
| 254 | +| The wrapper around the decision: version, type, issuer, issued_at, |
| 255 | +decision_hash. The envelope metadata is NOT included in the decision_hash |
| 256 | +(the hash covers only the decision payload). This means you can add envelope |
| 257 | +metadata (like a verification timestamp) without invalidating the hash. |
| 258 | + |
| 259 | +| **attestation type** |
| 260 | +| What kind of decision was attested. Types: `:routing` (which backend handles |
| 261 | +this?), `:access` (allow or deny?), `:policy` (policy reload), `:trust` |
| 262 | +(trust level change). The type is in the envelope, not the hash. |
| 263 | + |
| 264 | +| **issuer** |
| 265 | +| Which system produced this attestation. In the gateway it is |
| 266 | +`"http-capability-gateway"`. In the router it is `"hybrid-automation-router"`. |
| 267 | +Used for provenance — you can trace an attestation back to its source. |
| 268 | + |
| 269 | +| **verify** |
| 270 | +| Recompute the hash from the stored decision payload and compare it to the |
| 271 | +stored decision_hash. If they match, the record has not been tampered with. |
| 272 | +If they do not match, either the decision was modified or the hash was |
| 273 | +corrupted. |
| 274 | + |
| 275 | +| **redaction** |
| 276 | +| Replacing sensitive values with `"[REDACTED]"` before computing the hash. |
| 277 | +The hash covers the redacted payload, so it proves structure but not sensitive |
| 278 | +values. This is a deliberate security trade-off. |
| 279 | +|=== |
| 280 | + |
| 281 | +== Chapter 6: How a2ml and K9-SVC Work Together |
| 282 | + |
| 283 | +A2ML attestations and K9-SVC contracts are complementary layers: |
| 284 | + |
| 285 | +[cols="1,2,2"] |
| 286 | +|=== |
| 287 | +| | a2ml Attestation | K9-SVC Contract |
| 288 | + |
| 289 | +| **Question answered** |
| 290 | +| "What happened and can I prove it?" |
| 291 | +| "Was it within spec and what do we do if not?" |
| 292 | + |
| 293 | +| **When it acts** |
| 294 | +| After a decision is made (records the outcome) |
| 295 | +| Before and after execution (enforces the SLA) |
| 296 | + |
| 297 | +| **What it produces** |
| 298 | +| A content-addressable audit record |
| 299 | +| A breach/success signal with enforcement action |
| 300 | + |
| 301 | +| **Failure mode** |
| 302 | +| If attestation fails, the request still succeeds (audit gap) |
| 303 | +| If contract breaches, the breach policy fires (degrade, alert, circuit-break) |
| 304 | + |
| 305 | +| **Storage** |
| 306 | +| Audit log (Redis, database, IPFS) |
| 307 | +| ETS table (in-memory, per-node) |
| 308 | +|=== |
| 309 | + |
| 310 | +In practice, the flow is: |
| 311 | + |
| 312 | +---- |
| 313 | +Request arrives |
| 314 | + │ |
| 315 | + ├─ SafeTrust evaluates access → a2ml attests the decision |
| 316 | + │ |
| 317 | + ├─ K9 contract pre-check (trust threshold, rate limit) |
| 318 | + │ |
| 319 | + ├─ Proxy to backend (timed) |
| 320 | + │ |
| 321 | + ├─ K9 contract post-check (latency SLA) → breach policy if exceeded |
| 322 | + │ |
| 323 | + └─ a2ml attests the full outcome (including K9 breach if any) |
| 324 | +---- |
| 325 | + |
| 326 | +The attestation records that K9 fired a breach policy. K9 does not know or |
| 327 | +care about attestations. They are independent systems that compose naturally. |
| 328 | + |
| 329 | +== Chapter 7: What a2ml Attestations Are Not |
| 330 | + |
| 331 | +* **Not a replacement for logs.** Logs are for humans debugging at 3am. |
| 332 | + Attestations are for proving "the system made this decision" to an auditor, |
| 333 | + regulator, or post-incident review. You need both. |
| 334 | +* **Not a blockchain.** There are no blocks, no proof-of-work, no consensus |
| 335 | + protocol. It is a hash in a JSON envelope. Fast, simple, verifiable. |
| 336 | +* **Not encryption.** The attestation is plaintext. The hash proves integrity |
| 337 | + (not modified), not confidentiality (not readable). If you need secrecy, |
| 338 | + encrypt the audit store separately. |
| 339 | +* **Not useful for everything.** Health checks, metrics, and internal pings |
| 340 | + do not need attestation. The cost of hashing is low but the storage and |
| 341 | + query cost at high volume is real. |
| 342 | + |
| 343 | +== Summary |
| 344 | + |
| 345 | +A2ML attestations turn ephemeral log lines into content-addressable, tamper-evident |
| 346 | +audit records. They are most valuable for: |
| 347 | + |
| 348 | +* Access denials (provable "why was I blocked?") |
| 349 | +* Routing decisions in infrastructure automation (provable "why did this backend |
| 350 | + get this operation?") |
| 351 | +* Policy changes (provable "what rules were in effect?") |
| 352 | +* Cross-service propagation decisions (provable "was the mirror faithful?") |
| 353 | + |
| 354 | +They are least valuable for: |
| 355 | + |
| 356 | +* Anything git already hashes (commits, trees, blobs) |
| 357 | +* High-volume, low-stakes requests (health checks, static assets) |
| 358 | +* Cases where the log already contains everything you need and tamper-evidence |
| 359 | + is not required |
| 360 | + |
| 361 | +For the a2ml format spec: `standards/a2ml/SPEC.adoc` + |
| 362 | +For K9-SVC contracts: `docs/K9-SVC-EXPLAINED.adoc` + |
| 363 | +For anti-patterns and comparison tables: `standards/k9-svc/examples/NOT-a-good-fit.adoc` |
0 commit comments