
Where Real Exposure Lives The human perimeter is secure, but the non-human perimeter still has a lot of problems, including: Service accounts created for pipelines and never audited. API keys spun up for integrations, never rotated. Agent credentials are issued on demand and abandoned when the workload moves on. How many non-human identities per employee? The numbers are real. Here is why: SaaS and financial services: API integrations, automated pipelines, AI agents — stacked on top of each other. Every workload : its own identity. No exceptions. Most credentials : too broad. Last too long. Why Agents Break the Old Model A service account is predictable. Narrow the permissions, rotate quarterly, and watch for anything unusual. An AI agent is nothing like that, though. It decides in real time what to read, what to label, and what to put out, so if you give an agent a broad set of credentials, you are effectively granting standing access to things that shift around unpredictably across your environment. The fix is straightforward to implement in practice. Credentials should exist only for the duration of the task, scoped to exactly what that job needs, and then they are gone the moment it finishes; nothing carries over between runs, ever. Task-scoped token, expires with the job def get_agent_token(task_id: str, resources: list[str]): token = sts.assume_role( RoleArn = 'arn:aws:iam::ACCT:role/agent-base', RoleSessionName = f'agent-{task_id}', DurationSeconds = 900, # 15 min hard ceiling Policy = least_privilege_policy(resources) ) return token['Credentials'] def least_privilege_policy(resources): return json.dumps({'Version': '2012-10-17', 'Statement': [{ 'Effect': 'Allow', 'Action': ['s3:GetObject', 'dynamodb:Query'], 'Resource': resources}]}) \ The duration of a credential is like the biggest lever in non-human identity risk. The chart here shows the relationship clearly. Credential lifespan vs. breach risk \ The longer a credential lives, the more damage it can do. A Governance Model That Actually Works Three capabilities close most of the exposure, and none of them need new tooling, just someone owning them deliberately: Live inventory: every credential, in every account and repo, named and owned; Lifecycle policy : nothing is issued without an expiry. Service accounts at 90 days, agent tokens at 15 minutes, API keys auto-revoke at 180; Flag stale credentials automatically def flag_stale_credentials(records: list, days: int = 90) -> dict: cutoff = datetime.now(timezone.utc) - timedelta(days=days) results = {"stale": [], "notified": [], "errors": []} for record in records, try: last_used = getattr(record, "last_used", None) if last_used and last_used >= cutoff: continue results["stale"].append(record) notify_owner(record) results["notified"].append(record) schedule_revoke(record) except Exception as e: results["errors"].append({"record": record, "error": str(e)}) audit_log(action="stale_scan", stale=len(results["stale"]), notified=len(results["notified"])) \ Identity broker governance model When Agents Talk to Each Other One orchestrates. One executes. One validates. The credential problem multiplies with every hop. Each agent: its own scoped token. Nothing shared. Each delegation: a subset only. The child never exceeds the parent. Each hop: explicitly allowed. Nothing assumed. You give a small agent too much rope. It was never supposed to have it. It does anyway. Constrained delegation across agent hops def delegate_token(parent_token: dict, child_scope: list[str], task_id: str) -> dict: parent_scope = parent_token.get("allowed_resources", []) safe_scope = [r for r in child_scope if r in parent_scope] if not safe_scope: audit_log(task_id=task_id, action="delegation_denied", success=False) raise PermissionError("Child scope exceeds parent grant") audit_log(task_id=task_id, action="delegation_granted", scope=safe_scope) return issue_scoped_token(safe_scope, ttl_seconds=300) Secrets Are Not the Same as Credentials Most teams think secrets cover credentials. They do not. Credential: proves who an identity is. Secret: a value that must never be seen — a key, a certificate, a password. The overlap is where exposure hides. Secrets managers: store and rotate. That is all. Credential brokers: issue short-lived tokens, enforce scope, tie access to the workload running right now. \ Fetch a secret, bind it to the current workload only def get_db_secret(task_id: str, vault_path: str) -> str: token = get_agent_token(task_id, allowed_paths=[vault_path]) secret = vault.read(vault_path, token=token) if not secret or "data" not in secret: audit_log(task_id=task_id, path=vault_path, action="secret_read_failed") raise VaultReadError(f"No secret found at {vault_path}") audit_log(task_id=task_id, path=vault_path, action="secret_read") return secret["data"]["value"] When the Audit Log Is the Last Line of Defence Most teams find out about a breach from the post-mortem. Not the monitor. Not the alert. The post-mortem. Audit log : every action, every identity, every timestamp. No exceptions. Alert threshold : baseline normal first. Flag anything outside it. Response time : the log is useless if nobody reads it. The breach already happened. The log tells you how far it got. Flag anomalous access in real time def check_access_anomaly(task_id: str, resource: str, action: str) -> None: baseline = get_baseline(task_id, resource) if action not in baseline.allowed_actions: audit_log(task_id=task_id, resource=resource, action="anomaly_detected") raise AccessAnomalyError(f"Unexpected action '{action}' on '{resource}'") audit_log(task_id=task_id, resource=resource, action=action) When Rotation Is Not Enough Rotating a credential does not fix a bad scope. It just issues a fresh version of the same problem. Rotation : changes the value. Not the permissions. Scope : the real risk. Too broad and it does not matter how often you rotate. Revocation : the only clean answer when something looks wrong. Short-lived beats rotated. Scoped beats both. Revoke and reissue with the right scope de def reissue_credential(task_id: str, old_token: dict, resources: list[str]) -> dict: revoke_token(old_token) audit_log(task_id=task_id, action="token_revoked") token = issue_scoped_token(resources, ttl_seconds=900) audit_log(task_id=task_id, action="token_reissued", scope=resources) return token When Trust Is Assumed, Not Verified Most systems do not verify trust. They inherit it. An agent spins up, gets handed a token, and nobody checks whether that agent should have it. The token says it is allowed. That is enough. It should not be. Verify first: every agent proves who it is before it touches anything. Trust nothing inherited : a token passed down is not the same as a token earned. Challenge at every hop : one verification at the start is not verification. It is assumption. Revoke on doubt : if something looks wrong, pull access. Reissue when it is clean. Inherited trust is not trust. It is a gap with a name. The Window Is Closing The teams that get this right did not wait for something to go wrong. They started early, stayed deliberate, and never assumed someone else was watching. Enumerate: know every identity before someone else finds it. Assign ownership: no credential without an owner. Enforce expiry: automate it, do not rely on memory. Monitor behaviour: catch the breach before the post-mortem does. Assume breach: build as if something is already wrong. It probably is. Review access : permissions grow quietly. Trim them before they become the problem**.** \
View original source — Hacker Noon ↗



