Skip to main content

Implementation Guide: Automated IAL3 Validation for FedRAMP 20x Class D

5 min read
Implementation Guide: Automated IAL3 Validation for FedRAMP 20x Class D

This guide holds the reference implementations behind FedRAMP 20x Class D Requires Four Automated Methods per KSI. Here Are Five for IAL3. Read that first for the architecture, the shared-dependency table, and the failure-mode matrix; this piece is the code.

Everything here is illustrative. Function names such as proofing_provider.status() and evidence_store.get_bytes() stand for whatever your provider's API and your storage client actually expose; field names are chosen to be readable, not to match any vendor's schema. The Security Decision Record excerpt at the end is shaped to the official schema but is an excerpt, not a complete submission.

Conventions used throughout:

  • reference_id is the customer-side identifier shared between the identity provider, the evidence store, and the proofing provider. It is the join key for everything.
  • emit(metric, value, **labels) writes one line of deterministic telemetry to the metric store with a UTC timestamp and the method ID as a label.
  • Every method writes a heartbeat on completion. The method-health monitor at the end turns a missing heartbeat into a finding.
  • All code is trimmed to fit a blog column. Production versions need logging, secrets handling, and tests that are not shown.

Method 1: Ledger Reconciliation

The reconciler runs synchronously on grant and reinstatement as a provisioning gate, and daily over the whole population. The daily run is the one that produces coverage.

The binding-path check implements the three ways SP 800-63-4 permits a credential to enter a proofed account: initial binding in the attended session (SP 800-63A), post-enrollment binding of an additional authenticator under AAL3 authentication with notification (SP 800-63B-4 §4.1.2.1), and recovery after loss of all AAL3 authenticators via biometric comparison against the retained proofing sample (§4.2.2.3). The evidence for each path lives in a different place, and the check reads all three.

# m1_ledger_reconcile.py
# Cycle: synchronous on grant/reinstatement; daily full sweep.

import hashlib
import time

PERMITTED_PATHS = {"attended_session", "aal3_authenticated", "recovery_biometric"}
MAX_LEDGER_AGE_S = 15 * 60         # reject ledger responses older than this


def fetch_with_retry(fn, *args, attempts=4):
    """Retry transient API failures with backoff. Raise on exhaustion so
    the run fails loudly rather than reporting a clean sweep."""
    delay = 1.0
    for i in range(attempts):
        try:
            return fn(*args)
        except TransientAPIError:
            if i == attempts - 1:
                raise
            time.sleep(delay)
            delay *= 2


def ledger_entry(reference_id):
    entry = fetch_with_retry(proofing_provider.status, reference_id)
    if entry is None:
        return None
    # A cached or replayed response is a stale source; treat as unknown.
    if time.time() - entry.as_of_epoch > MAX_LEDGER_AGE_S:
        raise StaleLedgerResponse(reference_id)
    return entry


def binding_path_ok(cred, entry, idp_record):
    """cred: one credential from the IdP's authenticator record.
    Returns the permitted path it entered by, or None."""
    if cred.credential_id == entry.initial_credential_id:
        return "attended_session"
    ev = idp_record.binding_event(cred.credential_id)
    if ev is None:
        return None
    if (ev.authenticated_at_aal >= 3
            and ev.notification_sent
            and ev.path == "aal3_authenticated"):
        return "aal3_authenticated"
    if ev.path == "recovery_biometric" and entry.has_recovery_event(ev.id):
        return "recovery_biometric"
    return None


def check(identity):
    pkg = evidence_store.get_bytes(identity.reference_id)
    if pkg is None:
        return "NO_AUDIT_PACKAGE"
    entry = ledger_entry(identity.reference_id)
    if entry is None:
        return "NO_LEDGER_ENTRY"
    if hashlib.sha256(pkg).hexdigest() != entry.package_hash:
        return "PACKAGE_HASH_MISMATCH"
    if entry.status != "valid":
        return f"LEDGER_{entry.status.upper()}"      # REVOKED, EXPIRED, ...
    if entry.ial != 3:
        return f"WRONG_IAL_{entry.ial}"
    if entry.completed_at > identity.first_access_granted_at:
        return "PROOFING_AFTER_GRANT"

    # Sponsor attestation from the IGA/ticketing system: a named
    # reviewer confirmed the session outcome matches the expected
    # worker's HR record. The provider cannot write this record.
    att = iga.sponsor_attestation(identity.reference_id)
    if att is None or att.session_id != entry.session_id:
        return "NO_SPONSOR_ATTESTATION"
    if att.decided_at > identity.first_access_granted_at:
        return "ATTESTATION_AFTER_GRANT"
    if att.discrepancies_open:
        return "HR_RECORD_MISMATCH_UNRESOLVED"

    idp_record = idp.authenticator_record(identity.reference_id)
    seen = set()
    for cred in idp_record.credentials:
        if cred.credential_id in seen:
            return "DUPLICATE_CREDENTIAL_ID"          # within one subject
        seen.add(cred.credential_id)
        if not cred.aal3_eligible:
            # Eligibility is a credential property (hardware-bound,
            # multi-factor, phishing-resistant by construction). Whether
            # a login *achieves* AAL3 is a transaction property; M5 checks
            # that. This check only asserts nothing weaker is bound.
            return "NON_AAL3_ELIGIBLE_AUTHENTICATOR_BOUND"
        if binding_path_ok(cred, entry, idp_record) is None:
            return "AUTHENTICATOR_BINDING_PATH_UNVERIFIED"
    return None


def on_grant(identity):
    """Provisioning gate. Called before access into the boundary is granted."""
    reason = check(identity)
    if reason is not None:
        block_grant(identity, reason)
        emit("ksi.iam.ial3.grants_blocked", 1, reason=reason)


def enumerate_boundary_identities():
    """Union of every system that can confer boundary access. Each source
    is paginated; a source that fails is a finding, not an empty set."""
    sources = {
        "idp_groups": idp.members_of_boundary_groups,
        "cloud_iam": cloud.iam_principals_with_boundary_roles,
        "pam": pam.vault_users,
        "break_glass": vault.break_glass_holders,
        "vendors": idp.vendor_accounts_with_boundary_access,
    }
    identities = {}                   # reference_id -> BoundaryIdentity
    for name, fn in sources.items():
        page = None
        while True:
            batch, page = fetch_with_retry(fn, page)
            for raw in batch:
                ident = identities.get(raw.reference_id)
                if ident is None:
                    # Wrap the source object; sources is ours, not theirs.
                    ident = BoundaryIdentity.from_source(raw, sources=set())
                    identities[raw.reference_id] = ident
                ident.sources.add(name)
                ident.privileged = ident.privileged or raw.privileged
            if page is None:
                break
        emit("ksi.iam.ial3.source_enumerated", 1, source=name)
    return list(identities.values())


def daily_sweep():
    in_scope = enumerate_boundary_identities()
    by_ref = {i.reference_id: i for i in in_scope}
    if not in_scope:
        # An empty boundary population is not 100 percent coverage.
        emit("ksi.iam.ial3.population", 0, tier="all")
        emit("ksi.iam.ial3.sweep_error", 1, reason="EMPTY_POPULATION")
        return

    violations = []
    for i in in_scope:
        try:
            reason = check(i)
        except (StaleLedgerResponse, TransientAPIError) as e:
            reason = f"CHECK_FAILED_{type(e).__name__.upper()}"
        if reason:
            violations.append((i, reason))

    # Cross-subject collision: one credential ID on two subjects. check()
    # only sees one subject at a time, so this lives in the sweep.
    owners = {}
    for i in in_scope:
        for cred in idp.authenticator_record(i.reference_id).credentials:
            owners.setdefault(cred.credential_id, set()).add(i.reference_id)
    for cred_id, subjects in owners.items():
        if len(subjects) > 1:
            for ref in subjects:
                violations.append((by_ref[ref], "CREDENTIAL_ID_SHARED"))

    failed = {i.reference_id for i, _ in violations}
    for tier in ("all", "privileged"):
        pop = [i for i in in_scope if tier == "all" or i.privileged]
        if not pop:
            continue
        covered = sum(1 for i in pop if i.reference_id not in failed)
        emit("ksi.iam.ial3.coverage_pct", 100 * covered / len(pop), tier=tier)
        emit("ksi.iam.ial3.population", len(pop), tier=tier)

    for reason, n in count_by_reason(violations).items():
        emit("ksi.iam.ial3.violations", n, reason=reason)
    emit("ksi.iam.ial3.heartbeat", 1, method="M1")

Two design choices to keep. A check that cannot complete, because the ledger API failed or returned something stale, is recorded as its own violation reason rather than skipped, so the day's coverage number reflects what was actually verified. And a credential identifier that appears twice in one subject's record (DUPLICATE_CREDENTIAL_ID, caught in check) or on two subjects (CREDENTIAL_ID_SHARED, caught in the sweep, which is the only place that sees the whole population) is a finding in its own right; it should not happen with FIDO credentials, and when it does something upstream is wrong.

Method 2: Enforcement Configuration Assurance

The input to the policy is the live configuration read from the identity provider and gateway management APIs, plus a drift flag computed by diffing that live state against the infrastructure-as-code state file in the same run. The same Rego runs in CI against the plan as a preventive gate; only the scheduled live run is a persistent method.

# m2_enforcement.rego
# input.gateway and input.idp are read live via management APIs.
# input.drift is computed by diffing live state against IaC state.

package ksi.iam.ial3.enforcement

# Every effective allow policy on a boundary application must require
# the IAL3 group. One good policy does not excuse a second one that
# bypasses it; the rule fires once per offending policy.
deny[msg] {
  app := input.gateway.applications[_]
  app.tags[_] == "boundary-application"
  policy := app.policies[_]
  policy.decision == "allow"
  policy.enabled == true
  not policy_requires_ial3(policy)
  msg := sprintf("gateway app %s: allow policy %s lacks IAL3 group",
                 [app.id, policy.id])
}

policy_requires_ial3(policy) {
  policy.require[_].group == data.ial3_proofed_group_id
}

# A boundary application with no allow policy at all is a config error,
# not a pass.
deny[msg] {
  app := input.gateway.applications[_]
  app.tags[_] == "boundary-application"
  count([p | p := app.policies[_]; p.decision == "allow"; p.enabled]) == 0
  msg := sprintf("gateway app %s has no enabled allow policy", [app.id])
}

deny[msg] {
  rule := input.idp.sign_on_rules[_]
  rule.applies_to_group == data.boundary_group_id
  not rule.conditions.required_groups[data.ial3_proofed_group_id]
  msg := sprintf("IdP sign-on rule %s lacks IAL3 group", [rule.id])
}

# An empty allowed-class list is not "AAL3 only"; on most gateways it
# means "no restriction". Require the list to be exactly {"aal3"}.
deny[msg] {
  app := input.gateway.applications[_]
  app.tags[_] == "boundary-application"
  classes := {c | c := app.allowed_authenticator_classes[_]}
  classes != {"aal3"}
  msg := sprintf("gateway app %s authenticator classes %v, expected {aal3}",
                 [app.id, classes])
}

deny[msg] {
  input.drift.gateway_policies_differ_from_iac == true
  msg := "live gateway policy differs from infrastructure-as-code state"
}

Emit the count of deny results as config_drift_findings, and a heartbeat.

Method 3: Behavioral Probing

Only the negative probe is automated. It uses a dedicated non-human test identity in the identity provider, configured like a boundary user but with no proofing record and no AAL3 credential entered through a permitted path, and it needs no successful authentication to produce its result. The positive probe is attended, for the reason given in the main article: an unattended daily AAL3 login would put a hardware authenticator, its activation factor, and a proofed person's credential into automation.

#!/usr/bin/env bash
# m3_negative_probe.sh — daily, and after enforcement-path deployments.
set -euo pipefail

URL="https://admin.example.gov/healthz"
CANARY="${UNPROOFED_CANARY:?}"        # dedicated test identity, no record
fail=0

# Session establishment for the canary uses a non-AAL3 authenticator on
# purpose; the assertion is that the boundary refuses it regardless.
status="$(session_for "$CANARY" | probe "$URL" || true)"

if [[ "$status" != "403" ]]; then
  echo "negative probe expected 403, got ${status:-none}" >&2
  fail=1
fi

emit "ksi.iam.ial3.negative_probe_refused" "$(( fail == 0 ? 1 : 0 ))"
emit "ksi.iam.ial3.heartbeat" 1 method=M3
exit "$fail"

The attended positive probe is a runbook, not a script. A proofed operator, after any change to the enforcement path and on a documented cadence, authenticates with their own bound AAL3 authenticator to the same health route and records the result in the evidence store with the change ticket; then, with Method 5 in place, attempts to register a hardware key that has not entered through a permitted path, and records the refusal. The automation's role is to open the checklist, collect the two results, and emit positive_probe_attended with the operator's reference ID and the change reference. That keeps the credential in the operator's hand and the evidence in the record.

Method 4: Session-to-Record Reconciliation

Four queries. The first is the violation query; the next two are the completeness controls without which the first is only as good as your ingestion; the last splits revocation latency into the two quantities that are actually measurable.

auth_events is the union of gateway access logs, identity-provider authentication logs, and cloud audit logs, normalized to one schema with a source_system and a boundary_access classification. proofing_status is populated from the provider's webhooks (reference_id, ial, completed_at, revoked_at), independently of Method 1's verdicts.

-- Q1: every successful boundary session must join to a record valid
-- at session time. Expected: zero rows.

WITH sessions AS (
  SELECT subject_ref, session_start, source_system, privileged
  FROM auth_events
  WHERE outcome = 'success'
    AND boundary_access = true
    AND session_start >= now() - interval '1 day'
)
SELECT s.subject_ref, s.session_start, s.source_system, s.privileged
FROM sessions s
LEFT JOIN proofing_status p
  ON  p.reference_id = s.subject_ref
  AND p.ial = 3
  AND p.completed_at <= s.session_start
  AND (p.revoked_at IS NULL OR p.revoked_at > s.session_start)
WHERE p.reference_id IS NULL;

boundary_access = true is a classification your pipeline applies. It tells you nothing about paths that are not ingested or are misclassified, which is what the next two queries are for.

-- Q2: source inventory. access_paths is a maintained table of every
-- way into the boundary and the log source that covers it, reviewed
-- with every architecture change. Expected: zero rows.

SELECT a.path_id, a.description, a.expected_source
FROM access_paths a
LEFT JOIN (
  SELECT DISTINCT source_system
  FROM auth_events
  WHERE event_time >= now() - interval '1 day'
) seen ON seen.source_system = a.expected_source
WHERE a.active = true
  AND seen.source_system IS NULL;
-- Q3: pipeline freshness. Expected: max_age below each source's floor.

SELECT a.expected_source AS source_system,
       now() - max(e.event_time) AS max_age,
       a.freshness_floor
FROM access_paths a
LEFT JOIN auth_events e ON e.source_system = a.expected_source
WHERE a.active = true
GROUP BY a.expected_source, a.freshness_floor
HAVING max(e.event_time) IS NULL
    OR now() - max(e.event_time) > a.freshness_floor;

Keep the access_paths table under change control and reconcile it, on a slower cadence, against the gateway's application list and the cloud provider's asset inventory, so a new path is a finding when it is created rather than when someone remembers it.

Revocation latency is two numbers, neither of which is "time until someone tried and was denied."

-- Q4: revocation latency, split.
--   ack_seconds: revocation event -> enforcement point's own audit record
--                of the change (group removal, allowlist update, credential
--                invalidation).
--   invalidation_seconds: revocation event -> termination of the last
--                active session, from the session store.
-- Any successful session after revoked_at is a Q1 violation, not latency.

-- required_enforcement_points lists every point that must act on a
-- revocation (IdP group, gateway allowlist, credential store, PAM ...).
-- Acknowledgement is complete only when the LAST required point has
-- recorded its change; a missing point is a failure, not a shorter time.

WITH required AS (
  SELECT p.reference_id, p.revoked_at, r.enforcement_point
  FROM proofing_status p
  CROSS JOIN required_enforcement_points r
  WHERE p.revoked_at >= now() - interval '1 day'
),
acks AS (
  SELECT q.reference_id, q.revoked_at, q.enforcement_point,
         min(a.recorded_at) AS acked_at
  FROM required q
  LEFT JOIN enforcement_audit a
    ON  a.subject_ref       = q.reference_id
    AND a.enforcement_point = q.enforcement_point
    AND a.action IN ('group_removed', 'allowlist_removed',
                     'credential_invalidated')
    AND a.recorded_at >= q.revoked_at
  GROUP BY q.reference_id, q.revoked_at, q.enforcement_point
),
open_sessions AS (
  SELECT s.subject_ref, count(*) AS still_open
  FROM sessions s
  JOIN proofing_status p ON p.reference_id = s.subject_ref
  WHERE s.started_at < p.revoked_at
    AND s.terminated_at IS NULL            -- explicit failure
  GROUP BY s.subject_ref
)
SELECT a.reference_id,
       a.revoked_at,
       bool_and(a.acked_at IS NOT NULL)          AS fully_acknowledged,
       extract(epoch FROM (max(a.acked_at) - a.revoked_at))
                                                 AS ack_seconds,
       (SELECT extract(epoch FROM (max(s.terminated_at) - a.revoked_at))
          FROM sessions s
         WHERE s.subject_ref = a.reference_id
           AND s.started_at < a.revoked_at
           AND s.terminated_at >= a.revoked_at)  AS invalidation_seconds,
       coalesce(o.still_open, 0)                 AS sessions_open_after_revocation
FROM acks a
LEFT JOIN open_sessions o ON o.subject_ref = a.reference_id
GROUP BY a.reference_id, a.revoked_at, o.still_open;

ack_seconds is the time to the last required acknowledgement, so one slow enforcement point sets the number rather than being averaged away. A row with fully_acknowledged = false is emitted as revocation_unacknowledged, and any sessions_open_after_revocation > 0 is a violation in its own right, not a null in a latency series.

Method 5: Bound-Authenticator Posture at Authentication

Evaluated on every authentication to a boundary application. The policy reads two things. From the subject's authenticator record, which SP 800-63B-4 already requires the credential service provider to maintain: whether the presented credential is AAL3-eligible and whether its recorded binding path is one of the three permitted, both established at binding. From the verifier: whether this transaction actually achieved AAL3, because AAL is a property of the authentication, not of the credential. A hardware key can be used in a single-factor ceremony, without user verification, or through a protocol that is not phishing-resistant; the verifier knows which and exposes it as flags. The verifier has already checked the assertion signature against the registered public key before this policy runs; there is no fresh attestation at authentication, and the policy does not ask for one.

# m5_posture.rego
# Evaluated per authentication.
#   input.subject_ref, input.credential_id  — from the verifier
#   input.txn                                — achieved-AAL flags from the
#                                              verifier for THIS transaction
#   data.authenticator_record                — subject's record (63B-4 §4.1)

package ksi.iam.ial3.posture

permitted_paths := {"attended_session", "aal3_authenticated",
                    "recovery_biometric"}

cred := data.authenticator_record[input.subject_ref][input.credential_id]

# AAL3 is achieved by the transaction, not conferred by the credential.
txn_achieved_aal3 {
  input.txn.factors_used >= 2
  input.txn.user_verified == true
  input.txn.intent_demonstrated == true
  input.txn.phishing_resistant == true
  input.txn.replay_resistant == true
  input.txn.approved_crypto == true
}

default allow := false

allow {
  cred.aal3_eligible == true          # established at binding
  cred.binding_path in permitted_paths
  not cred.invalidated
  txn_achieved_aal3
}

# A set, not a single value: several reasons can be true at once and a
# single-valued rule would produce a conflict.
deny_reasons[r] {
  not cred
  r := "CREDENTIAL_UNKNOWN"
}
deny_reasons[r] {
  cred.invalidated
  r := "CREDENTIAL_INVALIDATED"
}
deny_reasons[r] {
  not cred.binding_path in permitted_paths
  r := "AUTHENTICATOR_BINDING_PATH_UNVERIFIED"
}
deny_reasons[r] {
  cred.aal3_eligible != true
  r := "AUTHENTICATOR_NOT_AAL3_ELIGIBLE"
}
deny_reasons[r] {
  cred
  not txn_achieved_aal3
  r := "TRANSACTION_NOT_AAL3"
}

The binding_path field is written by the three workflows that are allowed to create credentials, and by nothing else. In the closed-enrollment model, self-service and help-desk direct registration are disabled for boundary users, so a credential without a permitted path cannot exist and the policy's deny branches are defense in depth. Where enrollment cannot be closed, the deny branches are the control, and every AUTHENTICATOR_BINDING_PATH_UNVERIFIED decision is investigated the day it occurs.

Emit each element of deny_reasons as auth_denied by reason (one decision can carry several), the daily count of allowed boundary authentications as bound_hardware_auth, and a heartbeat from the policy engine's health endpoint. TRANSACTION_NOT_AAL3 on an eligible, correctly bound credential is the interesting one: it means the key is right and the ceremony was not.

Metrics and Method Health

One line per observation, appended to a store with a defined retention period of at least 18 months; the main article suggests the life of the certification plus one assessment cycle.

{"ts": "2026-09-02T04:00:12Z", "ksi": "KSI-IAM-AAM",
 "measure": "IAL3", "method": "M1", "metric": "coverage_pct",
 "value": 100.0, "labels": {"tier": "privileged"}}

The health monitor runs every hour, which is the one place hourly is justified, because it is watching for absence.

# method_health.py — hourly. A method that has not reported within its
# documented cycle is a status of unknown, and unknown is a finding.

CYCLES_S = {"M1": 86400, "M2": 86400, "M3": 86400,
            "M4": 86400, "M5": 3600}   # M5 heartbeat from policy engine

def check_health(now):
    reporting = 0
    for method, cycle in CYCLES_S.items():
        last = metrics.last_heartbeat(method)
        if last is None or now - last > cycle * 1.25:
            emit("ksi.iam.ial3.method_stale", 1, method=method)
        else:
            reporting += 1
    emit("ksi.iam.ial3.methods_reporting", reporting)

Security Decision Record Excerpt

The official SDR schema requires certificationPackageOverviewUri and fedRampRequirements at the top level and carries indicators in a keySecurityIndicators array. Each entry requires ksiId, ksiImplementation, ksiValidation, ksiAssessment, ksiTests, and ksiEvidence; the statement fields are arrays of Markdown strings, and evidence entries carry a typed evidenceType from a fixed list. The excerpt below is one entry in that array, shaped to the schema, and it is an IAL3 measure fragment: the statements, tests, and evidence for the identity-proofing measure only. A complete KSI-IAM-AAM entry also carries the provisioning, deprovisioning, role and group change, privilege change, and exception measures, each with its own methods, and the surrounding document must supply the required top-level fields. It is not a complete SDR and not a complete KSI-IAM-AAM entry.

{
  "ksiId": "KSI-IAM-AAM",
  "ksiImplementationStatus": "Implemented",
  "ksiImplementation": [
    "Identity proofing measure IAL3: every human identity with access into the boundary holds a NIST SP 800-63-4 IAL3 proofing record held under split custody (audit package in provider-controlled evidence store; hash, status, and binding events on the proofing provider's ledger).",
    "Population: IdP groups mapped to boundary applications, cloud IAM roles, PAM vault users, break-glass holders, vendor accounts. Reported in two tiers: all, privileged."
  ],
  "ksiValidation": [
    "M1 ledger reconciliation (verification): synchronous on grant and reinstatement; daily full sweep. Reads provider ledger via API and package hash in evidence store.",
    "M2 enforcement configuration (verification): on deploy and daily. Reads live IdP and gateway policy via API, diffed against IaC state.",
    "M3 behavioral probing (validation): daily automated negative probe; attended positive probe after enforcement-path changes and quarterly.",
    "M4 session reconciliation (validation): daily batch over gateway, IdP, and cloud audit logs joined to webhook-fed proofing status; source inventory and freshness checks.",
    "M5 bound-authenticator posture (validation): evaluated on every authentication to a boundary application; daily datapoint from decision log. Also recorded against KSI-IAM-APM.",
    "Shared dependencies across methods are documented in artifact DEP-IAL3-2026-09. Method health is monitored hourly; a method not reporting within its cycle is a finding."
  ],
  "ksiAssessment": [
    "Independent assessor reviews method source, cycle documentation, the shared-dependency register, and 18 months of daily metrics; samples audit packages from the evidence store and verifies hashes against the ledger."
  ],
  "ksiTests": [
    "M1-LEDGER-RECONCILE", "M2-ENFORCEMENT-DRIFT", "M3-NEGATIVE-PROBE",
    "M3-ATTENDED-POSITIVE-PROBE", "M4-SESSION-JOIN", "M4-SOURCE-INVENTORY",
    "M4-FRESHNESS", "M4-REVOCATION-LATENCY", "M5-POSTURE-DECISION",
    "HEALTH-METHODS-REPORTING"
  ],
  "ksiEvidence": [
    {
      "evidenceType": "Log",
      "evidenceDescription": "Daily metric lines for all IAL3 methods, 18-month retention.",
      "evidenceLocation": "https://evidence.example.gov/ksi/iam-aam/ial3/metrics/",
      "lastUpdated": "2026-09-02"
    },
    {
      "evidenceType": "Configuration",
      "evidenceDescription": "Rego policies for M2 and M5 with commit hashes.",
      "evidenceLocation": "https://evidence.example.gov/ksi/iam-aam/ial3/policy/",
      "lastUpdated": "2026-09-02"
    },
    {
      "evidenceType": "Procedure",
      "evidenceDescription": "Attended positive-probe runbook and completed checklists.",
      "evidenceLocation": "https://evidence.example.gov/ksi/iam-aam/ial3/runbooks/",
      "lastUpdated": "2026-09-02"
    },
    {
      "evidenceType": "Audit Record",
      "evidenceDescription": "Shared-dependency register and failure-mode matrix.",
      "evidenceLocation": "https://evidence.example.gov/ksi/iam-aam/ial3/DEP-IAL3-2026-09.pdf",
      "lastUpdated": "2026-09-02"
    }
  ]
}

The statement strings exceed a comfortable code-column width because the schema wants prose in them; that is the correct shape, and the scroll is the price.

What to Take From This

The code is short because each method asks one question of one thing. The work is in the plumbing around it: paginated enumeration, retry and stale-response handling, a source inventory under change control, an authenticator record that carries a binding-path field, and a runbook for the one probe that should not be automated. Build those and the five methods are a week of engineering; skip them and the metrics will look fine right up to the day an assessor asks what a clean sweep actually verified.

Back to the architecture article, or talk to us about producing proofing records your methods can reconcile against.

Share: X LinkedIn

About the Trust Swiftly Team

We publish practical guidance on identity assurance, fraud prevention, and FedRAMP-aligned controls for high-risk workflows.

Comments