Skip to content
Cloud Offensive Security and DevSecOps Red Team Assessment
Cloud Security

Cloud Offensive Security & DevSecOps: Attacking the Control Plane

A profound, high-octane narrative detailing a multi-cloud Red Team engagement for a financial entity. From breaking into GitLab pipelines to exploiting Kubernetes namespaces and chaining IAM roles for an AWS account takeover, this article maps the elite mindset required to conquer and secure modern cloud infrastructure.

Pros

  • Demonstrates deep mastery of Multi-Cloud architectures (AWS, Azure, GCP) beyond traditional network pentesting
  • Exposes exactly how attackers pivot from a fragile CI/CD pipeline leak directly to full cloud control plane takeover
  • Combines extreme tactical execution with high-level threat modeling and business risk translation
  • Integrates DevSecOps defensive insights, proving the author knows how to both break and definitively secure the infrastructure
  • Highlights Identity & Access Management (IAM) as the true modern perimeter

Cons

  • Requires deep contextual knowledge of Kubernetes RBAC, Serverless architectures, and Cloud IAM mechanics
  • Focuses purely on advanced cloud-native exploitation, skipping basic web application attacks (XSS, SQLi)
  • Tools and techniques discussed change rapidly as Cloud Service Providers update their APIs

The tell that someone is new to cloud offensive work is the first command they run. It is nmap. In an environment where an auto-scaling group replaces every instance twice a day and the addresses are drawn from a pool, a port scan produces a snapshot of a network that will not exist by the time the report is written — and it lands in GuardDuty within minutes, which is the worst possible trade: loud, and useless.

In the cloud, identity is the boundary. The control plane is a REST API guarded by a policy document. Whoever can call that API owns the infrastructure, and no amount of network segmentation below it changes that.

What follows is an after-action narrative from a multi-cloud red team engagement against a heavily regulated financial institution — a mature environment, well funded, with a real SOC. The specifics are composited and the identifiers are illustrative; the technique chain is the one that actually works. The point is not the tooling. It is the sequence of decisions, and in particular the ones about what not to touch.

1. Entry Strategy: Thinking Like a Cloud Attacker

The starting position is a pair of low-privileged AWS access keys belonging to a junior developer — the assumed-breach premise, which is realistic because that is how it happens: a key in a public Gist, a laptop with ~/.aws/credentials and no disk encryption, a contractor’s GitHub account without MFA.

Three questions, in order. Who am I? What can I become? Where are the secrets? Everything else is premature.

The second question is the one that separates cloud work from network work. Traditional testing targets the workload — the application, the host, the service running on it — and a compromised host yields a host. Cloud attackers target the control plane, because an over-permissive IAM policy does not yield a machine, it yields the account. There is no equivalent in a data centre: no single misconfigured file on a Linux box has ever handed over every other Linux box simultaneously.

2. IAM Enumeration: Mapping Your Real Permissions

aws sts get-caller-identity returns arn:aws:iam::123456789012:user/dev-jdoe. It is the cheapest call in the API, it is invisible in any realistic detection stack because every SDK makes it, and it establishes the only fact that matters at this stage.

Then the policies — inline, attached, and the group memberships people forget to audit. I am not reading them to learn what the user can do. I am reading them for three permissions that are individually unremarkable and collectively fatal: iam:PassRole, iam:CreatePolicyVersion, and sts:AssumeRole.

iam:CreatePolicyVersion is the quiet one. It reads like a change-management permission, and it lets the holder write a new version of a policy they are attached to and set it as default. That is administrator access expressed as a routine operational grant, and it survives most reviews because the reviewer is looking for *:* and this is not that.

I do not need admin. I need the ability to pass a role more privileged than my own to a compute resource I control. That is the whole chain, and it is a design feature of IAM rather than a bug — PassRole exists precisely so services can act on your behalf. The vulnerability is in who holds it and against which roles, and Resource: "*" on a PassRole grant is the single most common critical finding in AWS environments.

Advertisement

3. Reconnaissance: Mapping the Cloud Landscape

Mapping the environment now, using the CLI and purpose-written Boto3 rather than a scanner. GuardDuty’s behavioural findings key on volume and pattern, so the constraint is not “avoid logging” — every call is in CloudTrail regardless — it is “look like an SDK doing ordinary work”. Read calls spread across hours, in a plausible order, from a region the account actually uses.

What I’m looking for:

  • IAM Roles: Trust relationships across the organisation. The Principal block is the map — it tells you which accounts can assume what, and cross-account trust with a bare account ID and no ExternalId is a bridge someone built and forgot.
  • S3 Buckets: Terraform state files. A .tfstate is a complete infrastructure inventory with plaintext values for anything Terraform touched, including passwords marked sensitive in the HCL — that flag hides the value from CLI output, not from the state file.
  • Compute: EC2 instances and the instance profiles attached to them. An instance running with a role stronger than mine is a target for a command execution primitive, not a network exploit.
  • Serverless: Lambda environment variables. GetFunctionConfiguration returns them to anyone with read access to the function, and developers put database credentials there because the console makes it the path of least resistance.

No vulnerability scanner has run at this point, and none will. There is no CVE here. Every finding is a logical error — a trust policy that is too broad, a bucket ACL nobody revisited, a secret in the wrong place — and scanners are built to match known-vulnerable software versions, which is a different problem entirely. This is why an environment can be fully patched, pass its compliance scan, and be trivially takeable.

4. The Turning Point: Choosing Your Target

Within the first hour, three viable paths are on the table:

  1. s3://finance-backup-assets allows any authenticated AWS user to read objects.
  2. arn:aws:iam::role/Lambda-Execution-Role has an overly permissive trust policy.
  3. An internal API Gateway is missing AWS WAF protection.

The decision: read the S3 bucket, leave the other two alone.

The API Gateway needs fuzzing, and fuzzing is thousands of requests that a WAF-less endpoint will still log to CloudWatch — high volume, low certainty. The Lambda trust policy needs AssumeRole attempts, and failed AssumeRole calls are one of the few CloudTrail events that mature teams alert on directly, because almost nothing legitimate generates them at volume.

A GetObject against a bucket in a DevOps account is different. It is the most common API call in AWS. Data events are not logged unless someone deliberately enabled S3 object-level logging on that bucket and is paying for the volume, which in most environments they have not — and that gap is silent in exactly the way that matters: the account looks fully logged, the CloudTrail dashboard is green, and the read leaves no trace anyone will ever query.

The bucket held what DevOps buckets hold. Terraform state.

5. Privilege Escalation: Building the Attack Chain

The state file contains a GitLab Runner registration token, in plaintext, because the Terraform resource that created the runner took it as an input and Terraform records every input it receives. Nobody put it there deliberately. That is the point — state files leak secrets as a property of how the tool works, not as a mistake anyone made.

The runner’s role, arn:aws:iam::role/gitlab-deployment-runner, holds ec2:RunInstances and iam:PassRole. Both are entirely reasonable for a deployment runner. That is why the combination survives review: each permission has an obvious business justification, and nobody is looking at pairs.

Here’s the chain:

  1. I trigger a rogue pipeline job in GitLab.
  2. That job spins up a new EC2 instance using ec2:RunInstances.
  3. When launching it, I attach the arn:aws:iam::role/Prod-Database-Admin role using iam:PassRole.
  4. Once the instance is running, I pull temporary credentials from its metadata service at http://169.254.169.254/latest/meta-data/iam/security-credentials/Prod-Database-Admin.

Junior developer to production database administrator, with no exploit code, no memory corruption, and nothing that a signature would ever match. Every call in that sequence is documented, supported, and indistinguishable in isolation from a deployment. The detection opportunity is not any single event — it is RunInstances with a PassRole for a production role, originating from a pipeline job nobody scheduled, which requires correlating three CloudTrail records against a build history most SIEMs are not ingesting.

Worth noting what the metadata service step assumes: this works cleanly against IMDSv1. IMDSv2’s session-token requirement does not make credential theft impossible, but it breaks the trivial SSRF-to-credentials path and is the single highest-value configuration change on this list. Enforce it account-wide rather than per-instance, or the one instance launched from an old AMI is the one that matters.

6. Network Exploitation: An Open Door

With the elevated role, the network layer becomes readable, and it contains the thing every environment eventually contains: a security group allowing 0.0.0.0/0 to port 6379 on an ElastiCache cluster. Somebody opened it during an incident at two in the morning, the incident resolved, and the rule outlived everyone who remembered why it existed.

Redis inside a VPC is typically deployed without authentication, on the reasoning that the network boundary is the control. That reasoning holds exactly as long as the network boundary does. The cluster held cached JWT session tokens for authenticated banking customers — session material that is bearer-equivalent, so possession is authentication, and no password reset invalidates it unless the application tracks sessions server-side.

This is where the engagement stopped and the client was contacted. Reading live customer session tokens is the point at which continuing serves the report rather than the client, and the rules of engagement should — and here did — draw the line before it, not after.

7. Kubernetes Compromise: Lateral Movement to Containers

A separate track, on the Azure side: a large AKS cluster, and a pod running an internally written reporting service with command injection in a parameter that gets passed to a shell. A shell in the container follows.

A traditional tester’s next move is a kernel exploit or a container escape. Neither is necessary and both are noisy. The interesting file is mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token, and it is there because automountServiceAccountToken defaults to true — nobody chose this.

The escalation is the ServiceAccount binding: this pod’s identity had cluster-admin. That happens for a mundane reason worth stating plainly, because it recurs everywhere — a deployment failed with an RBAC permission error, someone bound cluster-admin to make it work, intending to narrow it later, and the ticket to narrow it was closed as done when the deployment succeeded. With that token:

curl -ik -H "Authorization: Bearer $(cat token)" https://kubernetes.default.svc/api/v1/namespaces/kube-system/secrets

That returns every Secret in kube-system — TLS private keys, database passwords, API keys for services in other namespaces entirely. Kubernetes Secrets are base64-encoded, not encrypted, unless encryption at rest was explicitly configured against a KMS key, which is off by default on several managed distributions.

The lesson is not that containers are dangerous. It is that a container inherits an identity, and the blast radius of any application flaw is bounded by that identity rather than by the process boundary. A command injection in a reporting service should cost you a reporting service. Here it cost the cluster, and the difference between those two outcomes was one RBAC binding written under time pressure eighteen months earlier.

8. Serverless Exploitation: Lambda Functions

Back on AWS: a Lambda behind API Gateway at /process-ledger, parsing a JSON field and passing it to eval(). A twenty-year-old vulnerability class in a 2020s runtime, and it persists in serverless code specifically because functions are small, written quickly, and reviewed less carefully than the monolith they were carved out of.

Code execution inside a Lambda gives up the execution role immediately: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN are injected as environment variables by the runtime, so no metadata service call is needed and nothing resembling credential theft appears in the logs. Reading your own environment is not an API call.

That role had direct DynamoDB access to the tables the function served. The general point: “ephemeral” describes the container’s lifetime, not the identity’s power. A function that runs for 200 milliseconds and a server that runs for two years can hold precisely the same permissions, and the function is likelier to have been granted them by a developer in a hurry — and its credentials, unlike an EC2 instance profile’s, are handed over without a single request an auditor could later find.

9. CI/CD Pipeline Compromise: The Supply Chain Attack

If there is one thing to take from this narrative, it is this section. Compromising production gets you production, once, until someone reimages it. Compromising the pipeline gets you everything the pipeline will ever deploy, indefinitely, through a channel the organisation has explicitly decided to trust.

The secrets lived in Jenkins environment variables rather than a secrets manager. A vulnerable plugin on a worker node gave process memory, and process memory gave the deployment keys — Jenkins plugins being the durable weak point of that ecosystem, since the plugin surface is enormous, community-maintained, and updated on a cadence set by whoever is least available.

At that point direct exploitation stops being necessary. A commit to the main repository becomes a backdoor in production, deployed by the organisation’s own tooling, signed off by its own change process, and recorded in its own audit trail as a successful release. Every control in the pipeline now works for the attacker: the automation, the artefact promotion, the approval gate that approved a diff nobody read closely.

This is also the finding clients resist most, because the remediation is not a setting. It is branch protection that cannot be bypassed by administrators, required review by someone other than the author, signed commits, and short-lived pipeline credentials — four changes that each slow delivery slightly and are therefore each negotiable, right up until they are not.

10. Defense: Shifting Left with Continuous Scanning

Once the chain is demonstrated, the conversation turns to what would have broken it. Weekly Nessus scans would not have. In an environment where infrastructure is recreated on every merge, a scan describes a state that no longer exists — and none of the findings above were the kind a network scanner reports anyway.

The control that works is a policy check on the change itself. Checkov or tfsec against the Terraform plan, Trivy against the image, both on every commit, both failing the build on a PassRole with Resource: "*" or a security group open to the world. Catching it in the plan costs an engineer four minutes; catching it in production costs an incident.

Two honest caveats, because teams that skip them abandon the control within a quarter. First, these scanners are noisy out of the box, and a pipeline that fails on 200 pre-existing findings the first time it runs will be given a bypass flag by the end of the week — baseline the existing estate, enforce on new changes only, and narrow from there. Second, a failing build blocks delivery, so someone must own exceptions with an expiry date. Without that, the exception process becomes a permanent # checkov:skip comment, and the comment is invisible to everyone except the person who wrote it.

11. Custom Tooling: Evading Detection

ScoutSuite and Pacu are good tools and they announce themselves. Their user agents are recognisable, their call ordering is deterministic, and a SOC with any cloud detection content at all has a rule for them. Against a defended environment, purpose-written Boto3 is the difference between an assessment and a demonstration of the client’s alerting.

The scripts pace themselves — jitter between calls, work spread across the working day, regions matching the account’s actual footprint — and the privilege-escalation logic is written against how this organisation names and structures its policies rather than a generic ruleset.

The trade-off is real and rarely stated: custom tooling costs days of engagement time that could have gone into finding more issues, and it produces results that are harder for the client to reproduce afterwards. It is worth it when evasion is in scope — when the client is paying to test detection rather than configuration. When it is not, use the standard tools, tell the blue team you are running them, and spend the saved days on coverage.

One practical warning that applies either way: enumeration scripts against a large organisation can generate enough API calls to trip throttling, and in some services, real cost. Rate limiting is not just about stealth. It is about not being the reason the client’s production deployment fails at 4 p.m.

12. Threat Modeling: Building Identity Graphs

Network diagrams are close to useless for cloud threat modelling — they describe a layer the attack does not traverse. What you want on the wall is an identity graph: every principal, every role it can assume, and every resource each role can reach. Read as a graph, privilege escalation stops being a discovery and becomes a path anyone can trace before an attacker does. Here is the pattern:

Attack Chain:

  1. Attacker compromises a GitHub Personal Access Token (PAT).
  2. That PAT gains access to the Terraform repository.
  3. The attacker modifies the IAM AssumeRole policy.
  4. The pipeline automatically deploys those changes to production.
  5. The attacker uses the new identity to silently copy sensitive S3 data to an external account.

The Defense: Every step in that chain except the last is legitimate activity performed with legitimate credentials, so detection has almost nothing to work with. The last step is different. A service control policy that denies S3 access unless the request arrives from your known networks or VPC endpoints — an aws:SourceIp or aws:SourceVpce condition, the pattern AWS documents as a data perimeter — means the compromised identity still works and the data still cannot leave.

This does not neutralise the attack; the attacker retains their access and can still read, alter and destroy. It removes exfiltration specifically, which is the outcome that produces the regulatory notification. The cost is that IP-based conditions break legitimate access from anywhere you did not anticipate — a contractor, a new office, an AWS service calling on your behalf — so expect to spend the first fortnight after enforcement adding exceptions, and expect the pressure to loosen it to come from inside.

13. Detection vs. Evasion: The Speed-Stealth Tradeoff

Every management API call lands in CloudTrail. Every Azure control-plane action lands in Log Analytics. The logging is close to total, which sounds like bad news for an attacker until you look at what is actually queried: a handful of dashboards, a few dozen rules, and a retention window that is often ninety days because storage was a budget conversation.

The technique is living off the cloud. aws s3 sync is what DevOps engineers run all day, so an exfiltration performed with aws s3 sync is a needle in a stack of identical needles. Nothing custom, nothing compiled, nothing with a signature.

The price is time. Data that could move in an hour takes three days, because volume is the one thing threshold alerts do catch reliably. That patience is genuinely available to a nation-state operator and to a red team with a four-week window. It is not available to most ransomware crews, which is why volume-based detection still earns its place — it fails against the adversary you fear most and works against the one you will actually meet.

The defensive read: if your detection depends entirely on anomalous volume or unrecognised tools, you are covered against the common case and blind to the patient one. The signal that survives is behavioural and relational — this role has never called this API, this principal has never operated from this region, this pipeline deployed outside its schedule.

14. Turning Technical Risks into Business Language

A board does not fund a fix for iam:PassRole with a wildcard resource. It funds a fix for a consequence it can picture. The translation is not dumbing down — it is the analytical step of working out what the finding actually costs, which is work most reports skip:

  • IAM Misconfiguration: “A contractor’s compromised credentials allowed complete takeover of the entire AWS organizational root account.”
  • Exposed S3 Bucket: “A single misconfigured bucket exposed 5 million unencrypted banking transactions, violating GDPR and causing severe reputational damage.”
  • Pipeline Compromise: “An attacker could inject malicious code into our proprietary trading algorithms, causing us to automatically distribute malware to our clients.”

These land because they name a consequence with an owner. One caution, though: every number in a translated finding has to be defensible. Say “five million records were reachable by this identity” only if you established the record count, and never imply you exfiltrated data you did not touch. Inflate one figure and the next report you write — the one with the genuinely urgent finding — gets read as sales material.

15. Risk Assessment: Beyond CVSS Scores

Scanners rate a missing X-Frame-Options header Medium and a PassRole wildcard Medium. Those two findings are not within an order of magnitude of each other, and a remediation queue sorted by scanner severity will have engineers fixing headers for a fortnight while the account takeover path stays open.

CVSS is the wrong instrument here, and not because it is badly designed — it deliberately encodes no environmental context, which is exactly the information that determines cloud risk. Three dimensions do the work instead:

  1. Exploitability: Is this a documented API call anyone can make, or does it need a chain of preconditions and real expertise?
  2. Blast Radius: Does compromise cost you one instance, one account, or the organisation? This is the axis scanners cannot see, because it depends on trust relationships they never enumerate.
  3. Data Sensitivity: Marketing collateral or customer financial records. The same misconfiguration on two buckets is two different findings.

Blast radius is the one to weight hardest. In a data centre the answer was usually “this host”. In the cloud, often enough, it is “everything”, and the finding that reads as ordinary in isolation is the one that made that true.

16. Remediation: Strategic Fixes

Nothing in this engagement is fixed by a patch. Every step exploited a design decision, so every fix is a design change — which is why remediation here is slower, more political, and more durable than a patch cycle.

Top priorities:

  1. Identity Boundaries: Condition high-privilege roles on aws:SourceIp or a VPC endpoint, so a leaked credential is inert outside your networks. Cost: anything legitimately calling from an address you did not anticipate breaks, and the failure appears as an opaque AccessDenied that will consume an afternoon of somebody’s debugging.
  2. Network Isolation: Default-deny NetworkPolicy in Kubernetes, with explicit allows. Cost: this is real work in a cluster with many services, and the first enforcement will break something at an inconvenient hour — roll it out namespace by namespace in audit mode first.
  3. Pipeline Secrets: Replace static Jenkins credentials with short-lived OIDC tokens issued per job. This removes the dumped-memory step from the chain entirely, and it is the highest-value item here. Cost: the trust policy must constrain the sub claim to specific repositories and branches — omit that and you have built an endpoint that will issue production credentials to any pipeline on the platform, which fails open and looks configured correctly.

And one item that is not technical: give the escalation paths an owner. Every finding above existed because a temporary decision became permanent when the person who made it moved on. Controls decay; the review cadence is what keeps them from decaying silently.

17. Practical Collaboration: Finding Real Solutions

At the debrief, the infrastructure team pushes back: “We can’t remove iam:PassRole from GitLab. The entire deployment process depends on it.”

They are right, and a tester who treats that as obstruction has misread the room. The recommendation was wrong, not the objection — “remove the permission” ignores why the permission exists.

The workable version keeps the capability and bounds it. Constrain PassRole to a specific list of role ARNs rather than *, so the runner can pass the deployment roles it needs and not Prod-Database-Admin. Where the runner genuinely must create roles, attach a permissions boundary so no role it creates can exceed a defined ceiling. Deployments keep working; the escalation path closes.

This is the part of offensive work that determines whether findings get fixed. A report that lists ten permissions to remove gets one removed and nine risk-accepted. A report that proposes a bounded version of each capability gets implemented, because it does not ask anyone to break their own delivery process to satisfy a document.

18. Mentorship: Building Better Security Teams

Cloud offensive work does not transfer by watching someone run a tool. A junior tester who watches Pacu produce a privilege escalation path learns that Pacu produces privilege escalation paths. Pairing them on raw Terraform instead — reading a module and predicting which resource it will create with more permission than intended — builds the thing the tool is a shortcut for.

Reconnaissance scripts get reviewed together, and the review is mostly about error handling and rate limiting rather than technique. A script that paginates without backoff against a large organisation can generate a genuinely alarming API bill, and one that ignores exceptions will silently skip half an account and report a clean result — the worst outcome in this work, because a false negative in a security assessment is indistinguishable from good news.

The reasoning is what transfers. Anyone can learn the commands in a week; knowing which three of the twenty available paths are worth taking is the part that takes years.

19. Long-Term Security: From Reactive to Proactive

A clean penetration test is a statement about one fortnight. The useful question is whether the same finding can recur, and for most organisations it can — the open security group was fixed, the mechanism that allowed it to be created was not, and it reappears the next time someone is debugging at midnight.

Encoding the control in the pipeline changes the class of problem: an open security group stops being an incident to detect and becomes a build that fails. “Permanently” overstates it, though. Policy-as-code rots like any other code — new resource types appear that no rule covers, exceptions accumulate, and a check that has passed for a year is usually a check that stopped matching anything. Review the rules on a schedule and periodically verify that they still fail on a deliberately bad plan. A control nobody has tested is a control nobody knows the state of.

20. The Fundamental Difference: Cloud vs. Traditional Security

Traditional testing assumes the network is the boundary — defend the perimeter and the inside is yours. In the cloud the network is a software abstraction in someone else’s building, and the boundary sits above it, in a policy document.

Nothing in the chain above involved a memory corruption, an unpatched service, or a single exploit. It was: read a state file, take a token, pass a role, read an environment variable, use a trust relationship the way it was designed to be used. The targets were IAM policies and Terraform files. A firewall would not have noticed any of it, and neither would a vulnerability scanner.

So the skill set moves. Deep familiarity with API semantics rather than protocol internals; automation, because manual review does not scale to thousands of policy statements that change daily; and the ability to state what a finding costs in terms a person with a budget can act on. That last one is not a soft skill appended to the technical work. It is the difference between a finding that gets fixed and a PDF in a shared drive.


Share article

Subscribe to my newsletter

Receive my case study and the latest articles on my WhatsApp Channel.

Warning

Ask CyberROX AI