Skip to main content

Google.IAM.Admin.V1.Undeleteserviceaccount Remediation

Event Information​

  • Event meaning & impact

    • google.iam.admin.v1.UndeleteServiceAccount is emitted when a previously deleted Service Account is restored within its 30-day soft-delete window.
    • The Service Account’s identity (email/unique ID) becomes active again, and it can regain access to resources via existing or reattached IAM bindings, keys, and workload identities.
  • Security & compliance considerations

    • Undeleting can silently re-enable access paths that were intentionally removed, impacting least-privilege, SoD, and deprovisioning controls (e.g., ISO 27001 A.9, SOC 2 CC6.x, CIS GCP 1.x, PCI-DSS 7.x).
    • Treat this as a privileged action requiring justification and change records; ensure the actor (user/service) performing undelete has appropriate roles (e.g., roles/iam.serviceAccountAdmin) and is operating under an approved change.
  • Operational response & guardrails

    • Correlate this event with prior DeleteServiceAccount events, IAM policy changes, and key usage to verify the restore is intentional and risk-acceptable.
    • Automate alerts on this event, trigger a review workflow (re-validate IAM roles, keys, and bindings), and, if the account was decommissioned for security reasons, immediately re-delete or restrict it via organization policy and IAM.

Examples​

  • Re-activating a previously compromised service account

    • An attacker (or malicious insider) could undelete a service account that was disabled/deleted following a security incident and regain access tokens/keys or rebind new keys.
    • Violates least-privilege and incident-response expectations in ISO 27001, SOC 2, and NIST 800-53 (IR, AC families) if no approval / change management is enforced.
  • Restoring legacy high-privilege roles and bypassing access reviews

    • A service account with broad roles (e.g., roles/owner, roles/editor, or org-level custom roles) might be undeleted after being removed during an access review, effectively rolling back a risk remediation.
    • Undermines access governance and periodic recertification controls required in SOX, PCI DSS, and NIST (AC-2, AC-6).
  • Circumventing identity lifecycle and separation-of-duties controls

    • If identity lifecycle dictates that unused/breached service accounts be permanently removed, UndeleteServiceAccount can be used to bypass that process without ticketing or approval, especially if logging/alerts are weak.
    • Non-compliant with change-management and SoD controls (e.g., ISO 27001 A.9 & A.12, PCI DSS 7 & 10) if the same role both deletes and undeletes accounts without independent oversight.

Remediation​

Using Console​

  • Prevent reactivation of compromised / retired service accounts

    • In GCP Console, permanently delete high‑risk service accounts so they cannot be undeleted:
      1. Go to IAM & Admin → Service Accounts → select the project.
      2. Locate the service account (if disabled, it still appears).
      3. Click the service account → Delete → confirm.
      4. After 30 days soft‑delete window, it is permanently removed and cannot be undeleted (aligns with ISO 27001, SOC 2, NIST IR/AC).
    • For accounts that must remain:
      1. Remove all keys: Service account → Keys → delete all keys.
      2. Remove all roles: IAM & Admin → IAM → find the service account principal → click Edit principal → remove high‑privilege roles (Owner, Editor, custom org roles) → Save.
      3. Disable the account: Service account → three‑dot menu → Disable; use this only with strong monitoring and approval workflow.
  • Control UndeleteServiceAccount via IAM and approvals

    • Restrict who can undelete accounts:
      1. Go to IAM & Admin → Roles → open custom admin roles and ensure they do not include iam.serviceAccounts.undelete.
      2. In IAM & Admin → IAM, remove Owner/Editor from users/groups; instead, assign least‑privilege custom roles without undelete permission.
      3. Reserve undelete capability for a tightly controlled group (e.g., Security Admins) with a documented change‑management / ticketing process (supports NIST AC‑2/AC‑6, PCI DSS 7).
    • Enforce separation of duties:
      1. One group manages service account lifecycle (create/delete); another group (security / compliance) alone has undelete (or vice versa).
      2. Use Cloud Identity Groups and grant roles to groups only, not individuals, so SoD is enforced centrally.
  • Monitor, alert, and validate against compliance (GCP Console + Cloud Logging)

    • Enable audit logs for IAM in all projects and org:
      1. Go to IAM & Admin → Audit Logs → select IAM Service.
      2. Turn on Admin Read and Admin Write for All principals for each project and at the Organization level.
    • Create alerts for UndeleteServiceAccount and high‑risk changes:
      1. Go to Logging → Logs Explorer.
      2. Use a filter, e.g.:
        protoPayload.methodName="google.iam.admin.v1.IAM.UndeleteServiceAccount"
        OR protoPayload.methodName="google.iam.admin.v1.IAM.DeleteServiceAccount"
      3. Click Create alert → configure Notification channel (email / Pub/Sub → SIEM) and require incident tickets for each alert (supports ISO 27001 A.9/A.12, PCI DSS 10).
    • Periodically review:
      1. In IAM & Admin → IAM, run quarterly reviews to identify and remove legacy high‑privilege roles and org‑level custom roles from service accounts.
      2. Export IAM policies and audit logs to BigQuery / SIEM and reconcile with access reviews to ensure that deleted accounts or roles have not been reintroduced without approval.

Using CLI​

  • Prevent undelete by design (preferred)

    • Replace deletion with a “tombstone” pattern: remove all bindings, keys, and workload identity bindings, but do not rely on delete as the security measure:
      • Strip roles from the service account:
        gcloud projects get-iam-policy PROJECT_ID \
        --format=json > /tmp/policy.json

        # Edit /tmp/policy.json to remove bindings referencing serviceAccount:SA_NAME@PROJECT_ID.iam.gserviceaccount.com

        gcloud projects set-iam-policy PROJECT_ID /tmp/policy.json
      • Disable the service account (blocks token use and avoids the need for undelete):
        gcloud iam service-accounts disable SA_NAME@PROJECT_ID.iam.gserviceaccount.com \
        --project=PROJECT_ID
      • Delete all keys (user‑managed) to meet key‑revocation / incident-response requirements:
        gcloud iam service-accounts keys list \
        --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com \
        --project=PROJECT_ID \
        --format="value(name)" | xargs -I{} \
        gcloud iam service-accounts keys delete {} \
        --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com \
        --project=PROJECT_ID --quiet
  • Constrain who can undelete / restore high-privilege service accounts

    • Remove roles/iam.serviceAccountAdmin, roles/owner, and custom roles containing iam.serviceAccounts.undelete from operational / dev teams; assign only to a tightly controlled break-glass or security-admin group:
      gcloud projects remove-iam-policy-binding PROJECT_ID \
      --member="group:ops-team@ORG_DOMAIN" \
      --role="roles/iam.serviceAccountAdmin"

      gcloud projects add-iam-policy-binding PROJECT_ID \
      --member="group:security-admins@ORG_DOMAIN" \
      --role="roles/iam.securityAdmin"
    • Create a custom role that explicitly excludes undelete for normal admins:
      gcloud iam roles create saLimitedAdmin \
      --project=PROJECT_ID \
      --title="SA Limited Admin" \
      --permissions="iam.serviceAccounts.get,iam.serviceAccounts.list,iam.serviceAccounts.update,iam.serviceAccounts.create,iam.serviceAccounts.disable,iam.serviceAccounts.enable" \
      --stage="GA"
      Then bind this role instead of roles/iam.serviceAccountAdmin:
      gcloud projects add-iam-policy-binding PROJECT_ID \
      --member="group:iam-admins@ORG_DOMAIN" \
      --role="projects/PROJECT_ID/roles/saLimitedAdmin"
  • Enforce approval, monitoring, and lifecycle compliance

    • Require that “restore” operations happen only via a controlled CI/CD or ITSM pipeline (change ticket ID required) that calls gcloud with restricted service accounts:
      gcloud iam service-accounts undelete SA_UNIQUE_ID \
      --project=PROJECT_ID
      Ensure the pipeline’s service account has iam.serviceAccounts.undelete but no direct console access.
    • Enable and monitor Cloud Audit Logs for UndeleteServiceAccount, SetIamPolicy, and CreateServiceAccountKey, and pipe to SCC / SIEM with real-time alerts to satisfy ISO 27001 / PCI DSS logging requirements:
      gcloud logging sinks create sa-iam-audit-sink \
      storage.googleapis.com://BUCKET_NAME \
      --log-filter='protoPayload.methodName=("google.iam.admin.v1.IAM.UndeleteServiceAccount" OR "google.iam.admin.v1.IAM.SetIamPolicy" OR "google.iam.admin.v1.IAM.CreateServiceAccountKey")' \
      --project=PROJECT_ID
    • Codify “no legacy high-privilege roles” as policy using Org Policy + Policy Controller (if using GKE/Anthos) to block restoring roles/owner, roles/editor, or disallowed custom roles, and periodically reconcile via script:
      gcloud projects get-iam-policy PROJECT_ID \
      --format="json(bindings)" | jq '.bindings[] | select(.role=="roles/owner" or .role=="roles/editor")'

Using Python​

  • Enforce strong IAM guardrails on iam.serviceAccounts.undelete

    • Create an org‑level custom role without iam.serviceAccounts.undelete and migrate all human/admin identities to it; only a tightly controlled break-glass group should retain this permission.
    • Attach an org policy to restrict who can manage service accounts and keys, e.g.:
      gcloud org-policies set-policy org-policy.yaml
    • Example Python snippet to enumerate who has iam.serviceAccounts.undelete (via Cloud Asset Inventory) for review/cleanup:
      from google.cloud import asset_v1

      client = asset_v1.AssetServiceClient()
      scope = "organizations/1234567890"
      asset_types = ["cloudresourcemanager.googleapis.com/Project"]
      resp = client.analyze_iam_policy(
      request={
      "analysis_query": {
      "scope": scope,
      "options": {"expand_groups": True, "expand_roles": True},
      "access_selector": {
      "permissions": ["iam.serviceAccounts.undelete"]
      },
      }
      }
      )
      for binding in resp.main_analysis.analysis_results:
      print(binding.iam_binding)
  • Automate detection & response for UndeleteServiceAccount to enforce approvals

    • Enable Admin Activity Logs in Cloud Logging and create a log‑based alert on google.iam.admin.v1.UndeleteServiceAccount to meet ISO/SOC2/NIST monitoring expectations.
    • Use a Cloud Function / Cloud Run (Python) triggered from Logging to auto-revoke any undeleted SA (remove keys, high-priv roles) unless a change ticket/approval exists.
    • Example Python handler for a log-based trigger (pseudo-response workflow):
      import base64, json
      from googleapiclient.discovery import build
      from google.oauth2 import service_account

      SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
      CREDS = service_account.Credentials.from_service_account_file(
      "sa.json", scopes=SCOPES
      )

      def _iamadmin():
      return build("iam", "v1", credentials=CREDS, cache_discovery=False)

      def _cloudresourcemanager():
      return build("cloudresourcemanager", "v1", credentials=CREDS, cache_discovery=False)

      def remediate_undelete(event, context):
      data = json.loads(base64.b64decode(event["data"]).decode("utf-8"))
      proto = data["protoPayload"]
      method = proto.get("methodName", "")
      if method != "google.iam.admin.v1.IAM.UndeleteServiceAccount":
      return

      # Extract service account name and who did it
      target_sa = proto["response"]["name"] # e.g. projects/1234567890/serviceAccounts/sa@p.iam.gserviceaccount.com
      actor = proto["authenticationInfo"].get("principalEmail")

      iam = _iamadmin()

      # 1. Immediately disable the SA again (if policy requires ticket/approval)
      iam.projects().serviceAccounts().disable(
      name=target_sa,
      body={}
      ).execute()

      # 2. Delete any user-managed keys to prevent token/credential reuse
      keys = iam.projects().serviceAccounts().keys().list(
      name=target_sa,
      keyTypes="USER_MANAGED"
      ).execute().get("keys", [])
      for k in keys:
      iam.projects().serviceAccounts().keys().delete(name=k["name"]).execute()

      # 3. Optionally remove high-privilege roles bound to this SA
      # (simple example across a single project – extend to folders/org as needed)
      project_id = target_sa.split("/")[1]
      crm = _cloudresourcemanager()
      policy = crm.projects().getIamPolicy(
      resource=project_id, body={}
      ).execute()

      member = f"serviceAccount:{target_sa.split('/')[-1]}"
      high_risks = {
      "roles/owner",
      "roles/editor",
      # add legacy custom roles here: "organizations/ORG_ID/roles/LegacyAdmin"
      }

      changed = False
      for b in policy.get("bindings", []):
      if b["role"] in high_risks and member in b.get("members", []):
      b["members"].remove(member)
      changed = True

      if changed:
      crm.projects().setIamPolicy(
      resource=project_id, body={"policy": policy}
      ).execute()
  • Codify lifecycle, SoD, and “no‑undelete” rules for compromised / legacy SAs

    • Maintain a CMDB / security registry (e.g., in Firestore or a Git repo) of service accounts marked as COMPROMISED/DECOMMISSIONED; your remediation function should auto-disable and alert if any such SA is undeleted, enforcing IR and change‑management controls.
    • Ensure SoD by having: one role allowed to delete SAs, another (security/change manager) allowed to approve/temporarily undelete; enforce via separate groups and documented workflows (ISO 27001 A.9/A.12, PCI DSS 7 & 10).
    • Example Python snippet to enforce “never re‑enable compromised SAs” using a local deny‑list (replace with Firestore / DB in production):
      COMPROMISED_SAS = {
      "sa-compromised@project-id.iam.gserviceaccount.com",
      # ...
      }

      def is_compromised(sa_email: str) -> bool:
      return sa_email in COMPROMISED_SAS

      def remediate_undelete(event, context):
      import base64, json
      from googleapiclient.discovery import build
      from google.oauth2 import service_account

      data = json.loads(base64.b64decode(event["data"]).decode("utf-8"))
      proto = data["protoPayload"]
      if proto.get("methodName") != "google.iam.admin.v1.IAM.UndeleteServiceAccount":
      return

      sa_name = proto["response"]["name"]
      sa_email = sa_name.split("/")[-1]

      if not is_compromised(sa_email):
      # Optionally allow but still strip high-priv roles as in previous example
      return

      creds = service_account.Credentials.from_service_account_file(
      "sa.json",
      scopes=["https://www.googleapis.com/auth/cloud-platform"],
      )
      iam = build("iam", "v1", credentials=creds, cache_discovery=False)

      # Re-disable immediately and notify SOC
      iam.projects().serviceAccounts().disable(name=sa_name, body={}).execute()
      # TODO: send notification to SIEM / email / ticketing system