Short answer: a key's last-used timestamp cannot tell an edtech reviewer which service holds it. Attribute the credential through four independent signals: a stable, nonsecret key identifier, authenticated caller identity, workload deployment records, and per-key request events. Record disagreements instead of guessing. An access review is signable only when its owner can inspect the evidence, the time window, and the unresolved exceptions.
The data flow is straightforward. At issuance, bind a key identifier to an accountable owner and intended workload; at each authentication event, record that identifier alongside a verified caller identity and timestamp. Join those events to deployment inventory over the review window, then produce an evidence bundle with a disposition for each key. This is also how a notebook-to-production experiment stays distinguishable from a scheduled course assistant that happens to share its network address.
How can we tell which service holds an API key?
Start with what the verifier actually knows. A source IP may represent several jobs behind NAT, and a user-agent string can be copied. Neither proves service ownership. A request log bearing a key identifier establishes that the credential was used, but the caller still needs separate authentication evidence. If the gateway accepts only the key, the honest result is "observed, caller unverified," not a fabricated service name. For a scheduled retrieval job, this means checking the workload identity actually presented at the verifier, not trusting a deployment label copied into an application log; a label can be wrong even when the request itself succeeds.
Keep the uncertainty visible.
For a course-content retrieval assistant, the inventory might say course-search-prod while deployment records show the key mounted in both that service and lesson-eval-job. That difference matters: an eval job processing sample prompts should not silently inherit production access just because both workloads use the same Python client. Rotate or re-scope shared credentials after you have identified every consumer; premature revocation can turn an attribution exercise into an outage. OWASP's secrets guidance supports inventory, access control, rotation, and logging around the secret lifecycle.
Here is a small, runnable reconciler. Its inputs are deliberately sanitized exports; key_id is an opaque inventory label, never the key value. A request's principal is useful only if the authentication layer verified it independently of the API key.
from collections import defaultdict
inventory = {
"k-course-17": {"owner": "learning-platform", "intended": "course-search-prod"},
"k-review-23": {"owner": "curriculum", "intended": "lesson-eval-job"},
}
deployments = [
{"key_id": "k-course-17", "workload": "course-search-prod"},
{"key_id": "k-course-17", "workload": "lesson-eval-job"},
{"key_id": "k-review-23", "workload": "lesson-eval-job"},
]
events = [
{"key_id": "k-course-17", "principal": "course-search-prod", "verified": True},
{"key_id": "k-course-17", "principal": "lesson-eval-job", "verified": True},
{"key_id": "k-review-23", "principal": None, "verified": False},
]
mounted = defaultdict(set)
seen = defaultdict(set)
for item in deployments:
mounted[item["key_id"]].add(item["workload"])
for event in events:
if event["verified"] and event["principal"]:
seen[event["key_id"]].add(event["principal"])
for key_id, record in inventory.items():
observed = seen[key_id]
candidates = mounted[key_id]
conflicts = observed - candidates
status = "review" if conflicts or len(candidates) != 1 or not observed else "matched"
print(key_id, record["owner"], status, sorted(candidates), sorted(observed))
This example marks both rows for review: the first has two mounted workloads; the second has no independently verified caller. The output is a triage aid, not proof that a deployment still possesses a valid secret. In a real export, include event timestamps, deployment start and end times, credential state, and a link to each underlying record. Join on overlapping intervals, not on a current deployment snapshot applied retroactively to old traffic. The limitation is important: deployment records tell you where a secret was configured, not whether a process read it, and a verified principal tells you who made a request, not who copied the credential elsewhere. Without verifier-side identity, this method can narrow candidates but cannot establish a unique caller; defer sign-off on that row or add independent workload authentication first.
Build evidence that survives a reviewer question
The hard part is preserving the chain of custody without recording the secret itself. Put a nonsecret identifier in authentication logs; redact authorization headers, request bodies, and prompt text. Record issuance, rotation, revocation, owner changes, and access decisions as separate events with consistent timestamps. For each row in the review, show the intended workload, verified principals seen during the window, deployment bindings during that same window, and a named person responsible for resolving any mismatch.
No traffic is ambiguous. A dormant key may be unused, or the logs may have a coverage gap; an empty query is not evidence of absence. Mark the observation window and logging coverage explicitly. If some services bypass the central verifier, reconcile their access logs before claiming complete per-key usage. For an AI feature, count calls and tokens only where trustworthy metering exists; token cost can help prioritize a noisy credential, but expense does not establish ownership. Keep learner data out of the evidence bundle unless a narrowly scoped investigation requires it. This is a real trade-off: narrower logging reduces exposure of student content but also leaves fewer clues after an incident. Retain identifiers and event context rather than collecting prompts by default.
No invented certainty.
NIST's digital identity guidance distinguishes authentication from the attributes and decisions attached to an identity. That distinction applies here: a key proves possession of a secret under the verifier's rules; it does not, by itself, prove which team deployed the process. OWASP's logging guidance also emphasizes consistent event context and avoiding sensitive data in logs. Those are operational requirements, not a reason to log more payload.
Close the review without hiding uncertainty
Before asking for a signature, test the pipeline with a known key mounted in exactly one workload, a shared key, an unverified caller, and a deliberately missing log interval. Check that the first can be reconciled and that the other three remain visible as exceptions. Run the same test after deployment changes; a green notebook result says little about production identity propagation.
Then assign each exception an owner and a dated remediation decision. Re-issue separate credentials for shared workloads when the consumers are known, verify the new bindings in request events, and revoke the old credential only after the migration is observed. Preserve the review window, evidence references, and sign-off decision under the organization's retention policy. The useful deliverable is a reproducible statement of what was observed and what remains unknown, not a neat list of guessed owners.
Top comments (0)