How CPS1 protects your organization from supply chain attacks
Executive summary
In September 2025, there was a large-scale supply chain attack that compromised more than 500 packages in the NPM ecosystem, which are downloaded millions of times per week. The attack exploits the common practice of computers used in development containing credentials to publish packages, access cloud resources, and third-party APIs. This vulnerability stole several access keys, including secrets for popular cloud providers, such as Amazon Web Services (AWS), Google Cloud Platform (GCP), and Microsoft Azure.
In this post, we briefly explain the "Shai Hulud" attack, how it exploits the existence of credentials on developers' machines, and how the use of CDEs, particularly CPS1, protects the development process from supply chain attacks.
What was the Shai Hulud attack
In the last couple of weeks, a supply chain attack called Shai Hulud compromised more than 500 NPM packages. The malicious code exploits the presence of secrets, access keys, and credentials on developers' computers. It's a common practice nowadays for a development computer to contain credentials for publishing packages, acessing cloud resources, and making requests for third-party APIs.
Since Shai Hulud scans a computer for secrets, it has the capability to automatically replicate itself. When the attack detects a secret to publish packages on NPM, it discovers which packages the infected machine has access to and modifies those packages, publishing new versions with the attack code. When one of those now infected packages is downloaded in another computer, the cycle repeats.
This attack scans computers for access credentials for cloud providers such as Amazon Web Services (AWS), Google Cloud Platform (GCP), and Microsoft Azure. It also looks for secrets to trigger their auto-replication mechanism, such as keys to GitHub and NPM itself. Which credentials were stolen, however, isn't limited to these services, because the attack used the open-source tool TruffleHog to collect a wide variety of leaked credentials.
After harvesting credentials, the malicious code creates a repository in the user's account with a file containing all the collected credentials. The repository also contains a file configuring a GitHub Actions pipeline, which sends the discovered credentials to a webhook registered in the webhook.site development tool.
At first, all these activities seem regular in the day-to-day of software development: publishing a package, creating a repository, and running a CI pipeline that makes a request to a testing tool. Nothing particularly noteworthy. It shouldn't suprise that it took a while for the attack to be identified and for it to have compromised so many packages.
Cloud development environment (CDE) and supply chain attacks
In the software development industry nowadays, the difference between development and production environments handling poses some risks. In recent years, while production environments have acquired good practices regarding automation and security, the management and maintenance of development environments are still handled as a secondary task, of individual responsibility, with low visibility and performed with less priority, on a best effort basis.
Adopting a cloud development environment (CDE), you bring some of these practices from production environments to the development environment, such as environment isolation. Whether through containers, virtual machines, or other mechanisms, applications running in a production environment usually don't have access to the files and computing resources of other applications. With a CDE, the same care applies to the development environment.
When working on project A, your environment contains development tools and files that relevant to project A and only to project A, not project B. This by itself greatly reduces the attack surface and its impact: when downloading a compromised library in project A, the malicious code will only have access to what's in that specific environment, not all the projects and credentials that a development computer typically has access to.
In Cloud Programming Shell (CPS1), a development environment is defined declaratively, either with the CPS1 interface or the Kubernetes API, using the Template system. A development environment is always created from a template. When a template is updated, the interface indicates that the development environment is out of date and must be recreated. This workflow helps ensure that development environments are always up-to-date, following the practices aligned by the team, requiring no manual effort.
Additionally, you can also leverage all the extensions and infrastructure available for Kubernetes. For example, you can use Network Policies to create network isolations in development environments. In the next section, we'll show an example where we create a network policy that would protect a developer from the Shai Hulud attack, even if the environment had GitHub API secrets.
Network Policy example
Suppose someone works with front-end development. Also assume that the user in this example is called "devfe." In this example, the user "devfe" works only on projects that use NPM packages and has their source code stored on GitHub. Therefore, on a day-to-day basis, their development environments should be able to clone GitHub repositories, download NPM packages, and resolve DNS (to make the calls mentioned above).
Let's create a network policy that allows access only to necessary services for the daily work of "devfe":
- SSH/Git access to GitHub
- HTTP access to the NPM registry
- Access to the Kubernetes cluster's DNS service
Do note that the user will not have access to the GitHub APIs, only to the Git service for operations such as clone, pull, and push.
GitHub has an API to query which IPs should be included in an allowlist. Therefore, we can use a script to start a file that defines our NetworkPolicy by querying this endpoint and using the addresses for SSH/Git operations:
#!/bin/bash
json=$(curl -s https://api.github.com/meta)
cidrs=$(echo "$json" | jq -r '.git[]')
cat <<EOF
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
EOF
for cidr in $cidrs; do
if [[ "$cidr" != *:* ]]; then
cat <<EOF
- ipBlock:
cidr: $cidr
EOF
fi
done
After the IP list, we can add the port restriction in the same to
object to
ensure that only git/ssh requests are allowed to those addresses:
ports:
- protocol: TCP
port: 22
The NPM registry doesn't have an API to list their IPs. And NetworkPolicies,
as of Kubernetes 1.34, don't support FQDNs, only IPs in CIDR notation. We can,
therefore, make a verbose curl
call from within a CPS1 workspace to see which
IPs are resolved. Example:
curl -vvvv registry.npmjs.org
* Host registry.npmjs.org:80 was resolved.
* IPv6: 2606:4700::6810:23, 2606:4700::6810:123, 2606:4700::6810:1e22, 2606:4700::6810:223, 2606:4700::6810:1b22, 2606:4700::6810:1822, 2606:4700::6810:1c22, 2606:4700::6810:1922, 2606:4700::6810:1d22, 2606:4700::6810:323, 2606:4700::6810:1a22, 2606:4700::6810:1f22
* IPv4: 104.16.27.34, 104.16.26.34, 104.16.30.34, 104.16.3.35, 104.16.2.35, 104.16.24.34, 104.16.0.35, 104.16.31.34, 104.16.25.34, 104.16.28.34, 104.16.29.34, 104.16.1.35
(...)
Now we have another list of IPs to place in another egress block of our NetworkPolicy:
- to:
- ipBlock:
cidr: 104.16.0.35/32
- ipBlock:
cidr: 104.16.1.35/32
- ipBlock:
cidr: 104.16.2.35/32
- ipBlock:
cidr: 104.16.3.35/32
- ipBlock:
cidr: 104.16.24.34/32
- ipBlock:
cidr: 104.16.25.34/32
- ipBlock:
cidr: 104.16.26.34/32
- ipBlock:
cidr: 104.16.27.34/32
- ipBlock:
cidr: 104.16.28.34/32
- ipBlock:
cidr: 104.16.29.34/32
- ipBlock:
cidr: 104.16.30.34/32
- ipBlock:
cidr: 104.16.31.34/32
Finally, we need to allow DNS queries to the cluster's DNS service. This configuration may vary depending on your cluster, but here's an example:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
The complete YAML, with the policy to allow Git/SSH requests to GitHub, requests to the NPM registry, and requests to the cluster's DNS service should look similar to the following example:
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 192.30.252.0/22
- ipBlock:
cidr: 185.199.108.0/22
- ipBlock:
cidr: 140.82.112.0/20
- ipBlock:
cidr: 143.55.64.0/20
- ipBlock:
cidr: 20.201.28.151/32
# (...) mais IPs do GitHub
ports:
- protocol: TCP
port: 22
- to:
- ipBlock:
cidr: 104.16.0.35/32
- ipBlock:
cidr: 104.16.1.35/32
- ipBlock:
cidr: 104.16.2.35/32
- ipBlock:
cidr: 104.16.3.35/32
- ipBlock:
cidr: 104.16.24.34/32
- ipBlock:
cidr: 104.16.25.34/32
- ipBlock:
cidr: 104.16.26.34/32
- ipBlock:
cidr: 104.16.27.34/32
- ipBlock:
cidr: 104.16.28.34/32
- ipBlock:
cidr: 104.16.29.34/32
- ipBlock:
cidr: 104.16.30.34/32
- ipBlock:
cidr: 104.16.31.34/32
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
To apply this network policy to all projects under the "devfe" user,
simply apply the above configuration to the user's namespace. Each user in
CPS1 has their own namespace in Kubernetes. In this example, assume your
namespace is "u-devfe-tl6hmctz." CPS1 namespaces currently follow this
pattern: u-<username>-suffix
, with a configurable prefix (defaults to u-) and
a randomly generated suffix. To apply the policy:
kubectl apply -n u-devfe-tl6hmctz -f netpol.yaml
To validate, create a workspace with the user and try making a request to NPM. Example with curl:
user@netpoltest-yddzgj:~$ curl --no-progress-meter -m 1 -I https://registry.npmjs.org | head -n 1
HTTP/2 200
Also check that it is possible to perform Git/SSH operations to GitHub, but not to their API:
user@netpoltest-yddzgj:~$ ssh -T [email protected]
Hi devfe! You've successfully authenticated, but GitHub does not provide shell access.
user@netpoltest-yddzgj:~$ curl --no-progress-meter -m 1 -I https://github.com | head -n 1
curl: (28) Connection timed out after 1000 milliseconds
You can test requests to other addresses and confirm that they won't work.
To send the harvested credentials, Shai Hulud creates a repository in GitHub through its API. With the configuration we build here, although the user can clone repositories and pull/push changes, no process whithin the workspace can create new repositories at GitHub. Also, if the attack attempted to send the credentials from a compromised environment directly to webhook.site, skipping the repository creation, it wouldn't work as well.
Our vision and next steps
At CPS1, we believe that bringing best practices learned in production environments to the development environment improves both the productivity and stability of developed software. Helping to develop software securely, protecting its production chain, is part of this vision.
While it's already possible to use the example above to improve development security, we'll be adding product enhancements to make this process even easier: allowing you to edit and view these policies through the interface, associate them with templates, and support the use of FQDNs as well as IP addresses. We also plan to add a visualization of a template's Software Bill of Materials (SBOM) to quickly identify vulnerabilities before it even reaches a CI/CD pipeline. These measures will help to identify and protect against attacks like Shai Hulud.
To find out when this and other improvements will be available, follow our blog and changelog.
Talk to us!
Want to learn more about how CPS1 improves the security of your software development process? Let's talk.