Last updated: September 2026
Storm-3168 is Microsoft’s name for the cloud activity of JADEPUFFER, a threat actor Sysdig discovered in July 2026 and reported to be the first documented agentic ransomware operation. On 25 September 2026, Microsoft Security Research reported that the group used two compromised Azure service principals to map a tenant and then attempt more than 150 destructive or credential-collection operations in about 35 minutes. The lesson for anyone running identity for machines is that a long-lived client secret is a standing key to everything its identity can do. On Keycloak, the equivalent is a confidential client with a static secret and a service account, and the fix is the same: fewer static secrets, narrower permissions, and a tested way to cut one off in minutes.
A service principal is Microsoft Entra’s identity for an application rather than a person. Keycloak has the same idea under a different name: a confidential client with Service account roles switched on, which gets tokens through the OAuth 2.0 client credentials grant by presenting a client ID and secret. If you run identity management as a service for your own product, or run Keycloak yourself, you almost certainly have a handful of these for CI jobs, backend integrations and, increasingly, AI agents. This post walks through what Microsoft saw, why a redacted secret is still a live one, and a practical checklist for Keycloak clients.
What happened in the Storm-3168 attacks?
Microsoft’s write-up (“Storm-3168: Agentic-driven cloud attacks using compromised service principals”, Microsoft Security Blog, 25 September 2026) describes one affected tenant in early June 2026. Two service principals from the same tenant were compromised. The first spent about fifteen and a half hours enumerating virtual machines, subscriptions, resource groups and resources, with more than 300 successful read operations. The second started its own enumeration about 90 minutes later and covered two subscriptions in five seconds.
After a further inventory of App Service configuration stores, which Microsoft suggests was a search for exposed credentials, the second service principal moved to destruction. It attempted more than 150 destructive or credential-collection operations in 35 minutes. The deletion sequence itself lasted about seven minutes and included more than 100 storage account deletion attempts, most of which succeeded, along with the deletion of a Key Vault, a Function App and an App Service plan. About 30 minutes later it sent more than 30 successful ListKeys requests to pull storage account access keys, including keys for accounts used by Azure Site Recovery.
Why two identities made the attack faster
Microsoft observed one principal doing reconnaissance and the other doing discovery, destruction and key collection, both from the same infrastructure and the same python-requests/2.34.2 user agent. It also saw five separate tokens issued to the destructive principal, two of them active in the same 70-second window. Microsoft reads that timing and division of work as strong evidence of automated or scripted execution, which matches the “agentic” label Sysdig gave JADEPUFFER.
The practical consequence is speed, because when an automated attacker holds a valid credential, the gap between first use and serious damage can be minutes. A response plan that depends on someone noticing an alert, opening a ticket and finding the owner of the credential will usually lose that race, so the controls that matter most are the ones that were already in place before the attack started.
What actually slowed the attacker down
Two things limited the damage in Microsoft’s account. Azure resource locks and storage account deletion protection blocked some of the deletions even though the attacker’s identity had broad administrative permissions. And every attempt to delete Azure SQL databases failed, because the attacker used an API version the SQL resource type does not support. The first of those is a control you can plan for, while the second was an attacker mistake that no defender should count on.
Why is a redacted GitHub secret still a live credential?
Microsoft reports that the affected service principal’s client ID, client secret and tenant ID had been posted in plain text in a public GitHub issue by an employee of the victim organisation. The issue was later edited to remove the secret, but the secret stayed readable in the issue’s public edit history. Microsoft says it could not confirm whether that secret was the one used in the attack, so treat it as a plausible path rather than the proven one.
The general point does not depend on that detail. Removing a secret from the place where it leaked does nothing to the credential itself, because the identity provider still accepts it. Copies can survive in edit history, forks, caches, logs, chat exports and search indexes. Microsoft’s own guidance is to treat any credential that was publicly exposed as compromised, revoke or rotate it immediately, and then investigate how it was used.
This is exactly the same on Keycloak. A client secret pasted into an issue, a support ticket or a CI log stays valid until someone regenerates it in the admin console or through the Admin REST API, and nothing about deleting the paste changes that.
How does this map onto Keycloak confidential clients?
A Keycloak confidential client with a client secret and a service account is the direct counterpart of an Entra service principal with a client secret. Anyone holding the ID and secret can call the realm’s token endpoint with grant_type=client_credentials and receive an access token carrying the service account’s roles. Our guide to machine-to-machine authentication in Keycloak covers the grant itself in more detail.
Three properties decide how bad a leaked secret is:
- What the service account can do. Roles on the service account end up in the token. If the service account holds
realm-managementroles such asmanage-usersormanage-clients, a leaked secret is a path to administering your realm, which is the Keycloak version of an over-privileged service principal. - How long the credential lives. A client secret has no expiry by default. It works until someone regenerates it.
- How long tokens live. The default access token lifespan is five minutes for realms you create (the
masterrealm starts at one minute), and each client can override it under Advanced > Advanced settings. By default, the client credentials grant does not issue a refresh token (the Use refresh tokens for client credentials grant switch under the client’s OpenID Connect Compatibility Modes is off), so the attacker has to keep coming back to the token endpoint with the secret.
That default is worth keeping, because a service account without refresh tokens has to present its secret every few minutes, disabling the client or regenerating the secret stops new tokens almost immediately. The exposure that remains is the tokens already issued, which is why short lifespans matter.
Offline tokens and user-delegated clients are a different risk
Not every machine credential in Keycloak is a client secret. Integrations that act on behalf of a user often hold an offline token, which is a refresh token that survives the user’s session. Offline tokens are standing privilege in the same sense as a static secret, and they need their own inventory. Our post on JWT token lifecycle management explains how refresh and offline tokens expire and how to revoke them.
Which Keycloak features reduce reliance on static client secrets?
Keycloak 26.x gives you several ways to authenticate a client without a long-lived shared secret. Which one fits depends on where the workload runs.
| Client authentication | What the client holds | What leaks if the workload’s config leaks | Status in Keycloak 26.7 |
|---|---|---|---|
| Client ID and secret | A static shared secret | A reusable credential until regenerated | Supported |
Signed JWT (private_key_jwt) |
A private key; Keycloak holds only the public key or a JWKS URL | Nothing, if the key lives in a KMS or HSM; a key file otherwise | Supported |
| X509 certificate (mutual TLS) | A client certificate and key | The key, unless it lives in hardware | Supported |
| Federated client authentication | A token from a trusted issuer, such as a Kubernetes service account token or an OIDC provider’s assertion | A short-lived token, not a standing secret | Supported since 26.6 (SPIFFE still preview) |
Signed JWT means the client proves its identity by signing a short-lived assertion with a private key. Keycloak never stores anything that could be replayed on its own, and if you keep the key in a cloud KMS the workload can sign without ever holding the key material. The Keycloak 26.6.0 release notes also mark federated client authentication as supported: a client can authenticate with a token issued by an issuer Keycloak already trusts, such as a Kubernetes service account token or an assertion from an external OpenID Connect provider, which the release notes describe as eliminating “the need to assign and manage individual secrets for each client”. SPIFFE JWT-SVIDs remain a preview option because the underlying specification is still a draft. Our guide to workload identity federation with SPIFFE and Keycloak covers that path, and the companion post on SPIFFE node compromise covers its failure modes.
Where you must keep a secret, use rotation rather than a secret that lives forever. Keycloak’s Client Secret Rotation lets a client policy rotate a confidential client’s secret on a schedule while keeping the previous secret valid for a grace period, so consumers can switch without downtime. It is a preview feature in the 26.7 releases, so you have to enable it (--features=client-secret-rotation), and the draft 26.8.0 release notes on the Keycloak main branch promote it to supported (as of late September 2026, 26.7.4 is the newest release).
How do I cut off a compromised Keycloak client in minutes?
Write the runbook before you need it, and practise it on a test client. The order below stops new tokens first and then deals with the tokens already in circulation.
- Disable the client. Turn off Enabled on the client’s settings page, or send
PUT /admin/realms/{realm}/clients/{id}with"enabled": false. Keycloak then refuses new token requests from it. The token introspection endpoint also checks whether the issuing client is still enabled, so resource servers that introspect see existing tokens as inactive straight away. - Regenerate the secret. On the Credentials tab, regenerate the secret, or call
POST /admin/realms/{realm}/clients/{id}/client-secret. Do this even if you plan to re-enable the client, because the old secret is the thing that leaked. - Invalidate tokens already issued. Under Advanced > Revocation, click Set to now to record a not-before time for the client. Keycloak’s refresh token and introspection checks both honour that value, so tokens issued before it stop working anywhere Keycloak is asked about them. Resource servers that validate JWTs locally will not see any of this and will accept the old access token until it expires, which is the main reason to keep access token lifespans for machine clients short.
- Investigate its history. Search login events for
CLIENT_LOGINfrom the client, which is the event type Keycloak records for the client credentials grant, and admin events for anything the service account changed. This only works if events were being stored before the incident, which is worth checking now rather than later.
For the user side of revocation, our guide to revoking a single Keycloak session covers the equivalent controls for people.
What should a day-two checklist for machine clients include?
Three of the five mitigations in Microsoft’s Storm-3168 post translate directly to Keycloak: protect and continuously assess application credentials, rotate exposed credentials and prefer mechanisms that reduce long-lived credentials, and apply least privilege to workload identities. On Keycloak that becomes a short routine you can run every quarter, or after every headline like this one.
- Inventory confidential clients. List every client with Client authentication on, and record an owner, the authentication method, the date the secret was last rotated, and whether Service account roles is enabled. A client with no owner is the first one to disable.
- Strip administrative roles from service accounts. Check each service account’s role mappings for
realm-managementroles and for broad composite roles. Grant the narrowest role that lets the integration work. - Restrict audience. Use client scopes and audience mappers so that a token issued to one integration is only accepted by the APIs it actually calls, and make sure those APIs check
aud. - Keep access tokens short. For machine clients, a few minutes is usually enough, and it caps how long a stolen token is useful.
- Alert on the right events. New confidential clients, secret regenerations, service account role changes and unusual
CLIENT_LOGINvolume or source addresses are all worth a notification, and they only show up if Keycloak is storing login and admin events (see our audit logs feature). - Protect what you would need to recover. Microsoft’s point about resource locks applies to identity too: keep realm exports or database backups somewhere the service accounts in that realm cannot delete.
AI agents make this list more urgent, because teams now create client credentials for agents faster than they create them for services. Our post on authenticating AI agents with Keycloak explains how to give each agent its own client and scopes, and the NIST IR 8587 token protection guide covers sender-constrained tokens such as DPoP, which make a stolen access token useless without the matching key.
Where does managed Keycloak fit?
A managed service does not decide which client should hold which role; that stays with your team because only you know what each integration needs. What it removes is the operational work around that decision: keeping Keycloak on current patch releases, storing events and backups outside the blast radius of a single realm, and giving you a tested place to run the revocation steps above. At Skycloak we run upstream Keycloak for you, so the controls in this post work the same way whether you use our clusters or export your realm and run it yourself.
Frequently asked questions
What is JADEPUFFER?
JADEPUFFER is a threat actor that Sysdig discovered in July 2026 and that is reported to be the first documented agentic ransomware operation, meaning an attack run largely by automated, AI-driven tooling rather than by a person at a keyboard. Microsoft tracks its cloud activity as Storm-3168. In the Azure case Microsoft published on 25 September 2026, the group used two compromised service principals to enumerate a tenant, delete storage accounts, a Key Vault and a Function App, and collect storage account keys, with timing Microsoft describes as strongly indicating automated or scripted execution.
Is Keycloak affected by Storm-3168?
No Keycloak vulnerability is involved. Storm-3168 abused valid credentials for Azure service principals. The same pattern applies to any identity provider, including Keycloak, because a confidential client with a leaked secret gets tokens exactly as the legitimate integration would, with whatever roles its service account holds.
Does deleting a leaked secret from GitHub make it safe?
No. Microsoft’s guidance is that a publicly exposed credential should be treated as compromised even if the original post was edited or deleted, because copies survive in edit history, caches and archives. On Keycloak, regenerate the client secret and review the client’s CLIENT_LOGIN events.
How do I avoid client secrets entirely on Keycloak?
Use Signed JWT (private_key_jwt) with a key held in a KMS, mutual TLS with a certificate, or federated client authentication, which became supported in Keycloak 26.6 for Kubernetes service account tokens and external OpenID Connect assertions. Each removes the reusable shared secret that a leak would expose.
How quickly can a Keycloak client be shut off?
Disabling the client stops new tokens immediately, and introspection reports existing tokens as inactive. Resource servers that validate JWTs locally keep accepting already issued access tokens until they expire, which is five minutes under Keycloak’s default for new realms unless you have changed it.
Sources
- Microsoft Security Research (Yossi Weizman and Tushar Mudi), “Storm-3168: Agentic-driven cloud attacks using compromised service principals”, Microsoft Security Blog, 25 September 2026, retrieved 2026-09-28, https://www.microsoft.com/en-us/security/blog/2026/09/25/storm-3168-agentic-driven-cloud-attacks-using-compromised-service-principals/
- Sysdig, “JADEPUFFER: Agentic ransomware for automated database extortion”, July 2026 (cited by Microsoft; not reachable from our research environment), https://www.sysdig.com/blog/jadepuffer-agentic-ransomware-for-automated-database-extortion
- Keycloak, release notes for 26.6.0, “Federated client authentication (supported)”, keycloak/keycloak on GitHub, retrieved 2026-09-28, https://github.com/keycloak/keycloak/blob/main/docs/documentation/release_notes/topics/26_6_0.adoc
- Keycloak, draft release notes for 26.8.0, “Client Secret Rotation (supported)”, keycloak/keycloak main branch, retrieved 2026-09-28, https://github.com/keycloak/keycloak/blob/main/docs/documentation/release_notes/topics/26_8_0.adoc
- Keycloak,
Profile.javaat tag 26.7.4 (feature status forCLIENT_SECRET_ROTATION,CLIENT_AUTH_FEDERATED,SPIFFE), retrieved 2026-09-28, https://github.com/keycloak/keycloak/blob/26.7.4/common/src/main/java/org/keycloak/common/Profile.java - Keycloak,
AccessTokenIntrospectionProvider.javaat tag 26.7.4 (disabled-client and not-before checks), retrieved 2026-09-28, https://github.com/keycloak/keycloak/blob/26.7.4/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java - Keycloak,
OIDCAdvancedConfigWrapper.javaat tag 26.7.4 (refresh tokens for client credentials default to off), retrieved 2026-09-28, https://github.com/keycloak/keycloak/blob/26.7.4/services/src/main/java/org/keycloak/protocol/oidc/OIDCAdvancedConfigWrapper.java