←新闻详情

The New Stack更新于 10月30日 14:13

AI 智能体早已下线,它的访问密钥为何还活着?

What happens when the agent is long gone, but its access key lives on?

AI 导读

OWASP 将非人类身份的不当下线列为 2025 年 Top 10 风险之首,而 AWS IAM 用户访问密钥不会自动过期,被遗忘的密钥会一直有效直到有人手动禁用或删除。文章给出三步治理方案:用 CloudQuery 查询找出 90 天以上未使用且仍激活的密钥,用 OPA 策略在 Terraform/OpenTofu 计划阶段拦截长期凭证创建,并为每个密钥设定有限生命周期。

正文

Imagine: There’s a line on the credential report that no one on the platform team can explain. An IAM user called research-agent-2 has an access key created 14 months ago that was last used 11 months ago. And it’s still active.

The project it supported closed long ago, and the person who approved it doesn’t even work at the company anymore.

While this example is hypothetical, the lifecycle problem isn’t.

This doesn’t require carelessness. An agent is created for a task, the task ends, and nobody revisits its access. A departing person triggers offboarding; a finished agent just leaves a closed tab. AWS IAM user access keys don’t expire automatically, so a forgotten key stays valid until somebody disables or deletes it.

“A departing person triggers offboarding; a finished agent just leaves a closed tab.”

OWASP puts improper offboarding first in its 2025 Top 10 for non-human identities, and the remedy reads like this series’ premise: retire what’s no longer needed, and give the rest an accountable owner. The hard part is knowing what exists.

The problem is that attackers move faster than an annual audit ever could. In September 2026, GreyNoise documented an AI-orchestrated campaign against PaperCut servers: the attacker went from an empty workspace to remote code execution at a real victim in under four hours, and once launched, the campaign compromised at least eleven organizations in 26 seconds. That wasn’t an attack using abandoned AWS IAM keys. It shows how quickly exposed access becomes useful once an attacker is in, and why slow inventory and revocation are dangerous.

An agent’s access can outlive its purpose unless someone governs its lifecycle.

The last three articles covered attaching an owner at discovery, reassigning one before a last day, and what happens when the owner is an agent. Credentials add a related problem, with one distinction. An agent’s deployment identity is what authenticates it, or the deployment platform, to a cloud account.

“A forgotten VM costs money; a forgotten credential can grant access.”

A provisioned credential is a persistent key, secret, or token that a deployment creates for some other workload; the two have different owners and revocation procedures. A forgotten VM costs money; a forgotten credential can grant access. The remedy is still a query, a policy, and a clock, each used for the part of the problem it can solve.

1. The query that finds keys no one is using

CloudQuery can sync AWS IAM credential reports into aws_iam_credential_reports when that table is enabled in the source configuration. The report lists up to two access-key slots per user. This query, written for the ClickHouse dialect CloudQuery Platform uses, takes the latest synced row for each report, turns the slots into rows, and returns active IAM user keys that are at least 90 days old and unused for at least 90 days.

SQL WITH latest_reports AS ( SELECT _cq_platform_id, arn, `user` AS principal, access_key1_active, access_key_1_last_rotated, access_key_1_last_used_date, access_key2_active, access_key_2_last_rotated, access_key_2_last_used_date FROM aws_iam_credential_reports ORDER BY _cq_sync_group_id DESC, _cq_sync_time DESC LIMIT 1 BY _cq_platform_id ), keys AS ( SELECT _cq_platform_id, arn, principal, 1 AS key_slot, access_key1_active AS active, access_key_1_last_rotated AS created, access_key_1_last_used_date AS last_used FROM latest_reports UNION ALL SELECT _cq_platform_id, arn, principal, 2 AS key_slot, access_key2_active AS active, access_key_2_last_rotated AS created, access_key_2_last_used_date AS last_used FROM latest_reports ) SELECT _cq_platform_id, arn, principal, key_slot, created, last_used FROM keys WHERE principal != ' ' AND active = true AND created ' ' AND active AND created 0 every _, m in input.plan.modules { tf_plan(m) } }

resource_changes := object.get(input.plan, "resource_changes", []) if standard_plan

resource_changes := [rc | some _, m in input.plan.modules some rc in object.get(m, "resource_changes", []) ] if run_all_plan

deny contains msg if { not standard_plan not run_all_plan msg := "Unsupported plan format for the credential policy" }

deny contains msg if { some rc in resource_changes key_resources[rc.type] "create" in rc.change.actions msg := sprintf("%s: long-lived credential prohibited", [rc.address]) }

allow if count(deny) == 0

The policy reads a standard Terraform or OpenTofu plan, or the module map from Terragrunt run-all. A plan with no changes can omit resource_changes, so an empty plan counts as valid, while an unrecognized shape is denied rather than silently allowed; attach it only to projects whose templates produce these formats, since Pulumi, CloudFormation, and Kubernetes deployments would all be denied. Replacements include a create action, so they’re caught. The resource list is an example, and the policy can’t see a hardcoded secret or a key generated outside Terraform.

The allow rule matters: with it, anything not denied proceeds without manual approval; without it, a policy that returns nothing defaults to pending, and every deployment waits for a person. Test with the full env zero input wrapper, not bare terraform show -json output, covering key creation, role-only plans, replacements, run-all, missing plan data, and destroys. ENV0_PRINT_POLICY_INPUT=true logs the exact input; ENV0_OPA_VERSION pins the OPA version. The policy runs on env zero’s default OPA (0.46.1) and on 1.x, which is why it uses future.keywords imports instead of import rego.v1.

If an integration genuinely needs a persistent key, don’t just create it in another project. Use a documented exception: a named owner, a specific workload, least privilege, a revocation deadline, secure storage, monitoring, and an approval record. IAM access keys can’t be tagged, so record ownership on the identity and in your credential inventory.

3. Make sure you give every key a finite lifespan

AWS doesn’t give IAM user access keys an expiration date, so the end date has to come from somewhere else. For Terraform-managed credentials, an environment TTL gives you a teardown point: put disposable credentials in the same environment as what they serve and set a TTL. But a TTL isn’t credential expiry.

It starts a destroy, and the key disappears only if Terraform manages it and the destroy succeeds. A failed destroy, a locked environment, a manually created key, or an identity managed elsewhere leaves access behind, so verify revocation instead of trusting the countdown. TTLs can be extended or set to infinite, and TTL limits don’t bind admins. On the Free plan, once an organization passes 250 runs in a month, TTLs are destroyed; wait for the count to reset at the start of the next month (UTC), so a key can outlive its TTL by up to a month.

Govern the trust relationship and permission scope of any OIDC federation, and rotate persistent tokens in the issuing system; destroying infrastructure doesn’t revoke the caller. Terraform-managed access keys also record their secret values in state, so protect the state backend.

For keys outside any environment lifecycle, retire them under control. The credential report doesn’t include key IDs, so look those up, check last-used metadata, confirm the owner and dependency, then deactivate the specific key:

Shell aws iam list-access-keys \ --user-name research-agent-2

aws iam get-access-key-last-used \ --access-key-id <key-id>

aws iam update-access-key \ --user-name research-agent-2 \ --access-key-id <key-id> \ --status Inactive

Deactivation is reversible, but it isn’t harmless: a quarterly batch or disaster-recovery job can fail long after a typical release cycle. Watch for authentication errors through an observation period matched to the workload’s schedule, get owner sign-off, then delete under your change procedure, since it isn’t reversible.

env zero’s Free plan includes custom approval policies and environment TTL, with 250 runs a month and 30 active environments. CloudQuery’s Platform has a free tier, and its CLI syncs to your own Postgres. Sync the credential report, run the query, and count the rows. It’s read-only, so the worst outcome is a list you didn’t want to see.

A credential needs an end date

A resource without an owner costs money until someone notices, and an unowned credential can grant unintended access to whoever finds it.

“An old key doesn’t need to be labeled orphaned for an attacker to use it; it only needs to be valid.”

Agents who finished last quarter’s projects won’t flag their own credentials, and the next agent to encounter one might not belong to your organization. An old key doesn’t need to be labeled orphaned for an attacker to use it; it only needs to be valid.

The post What happens when the agent is long gone, but its access key lives on? appeared first on The New Stack.

来源

The New Stack