Red Hat Quay Registry Takeover via Key Rotation (CVE-2026-123710)
Red Hat Quay before the fixed branch builds lets an unauthenticated caller install a signing key and forge registry tokens for any repository.
TL;DR
- Red Hat Quay's key server accepts an anonymous registry token as a key-rotation credential, so anyone who can reach
/keyscan install a signing key and forge tokens for every repository. The flaw is CVE-2026-123710, published on November 11, 2026. - A second critical flaw, CVE-2026-110697, lets a user who can create a namespace read other tenants' robot account tokens through an unescaped SQL
LIKEpattern. Both hit self-hosted Quay before the fixed build for its branch. - Both flaws are rated CVSS v3.1 Critical: CVE-2026-123710 scores 9.8 and CVE-2026-110697 scores 9.9. No exploitation is known, and neither is in CISA KEV as of November 12, 2026. Red Hat says it has fixed Quay.io.
- Update to the fixed build for your branch (3.9.28, 3.10.28, 3.12.24, 3.14.10, 3.15.9, 3.16.7, 3.17.7 or 3.18.3). If you cannot, block
/keysat the proxy and review the approved service keys.
Am I affected?
You are exposed if the conditions that apply to the flaw you are checking all hold:
- You run a self-hosted Red Hat Quay 3 or Quay 5 deployment, and its build is older than the fixed build for its branch.
- For CVE-2026-123710, the
/keysendpoint is reachable by untrusted clients. Anonymous access is enabled by default in Quay 3.18.2 (FEATURE_ANONYMOUS_ACCESS = True), and the anonymous token is what the key server accepts. - For CVE-2026-110697, the deployment is multi-tenant and the attacker holds an account that can create an organization or a user namespace.
The key-takeover flaw needs no account. The robot-token flaw needs a namespace the attacker can name. A single-tenant registry can still be hit by the first flaw, because the forged tokens reach every repository.
| Branch | Affected versions | Fixed version |
|---|---|---|
| 3.9 | All releases before 3.9.28 | 3.9.28 |
| 3.10 | All releases before 3.10.28 | 3.10.28 |
| 3.12 | All releases before 3.12.24 | 3.12.24 |
| 3.14 | All releases before 3.14.10 | 3.14.10 |
| 3.15 | All releases before 3.15.9 | 3.15.9 |
| 3.16 | All releases before 3.16.7 | 3.16.7 |
| 3.17 | All releases before 3.17.7 | 3.17.7 |
| 3.18 | All releases before 3.18.3 | 3.18.3 |
Red Hat also lists Red Hat Quay 5 as affected in the CVE pages. Those pages do not name a separate Quay 5 build, so Quay 5 customers should read the errata for their subscription before they schedule the update.
On an OpenShift deployment, the running Quay image tag shows the build. The Operator sets it, so read the deployment rather than the Operator version:
oc get deployment -A -l quay-component=quay-app \
-o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}'
On a Podman deployment, inspect the running container:
podman inspect quay --format '{{.Config.Image}}'
The tag is the Quay build. A digest without a readable tag still maps to a build, and skopeo inspect docker://<image> prints the labels for that digest.
How do I fix it?
Red Hat lists the fixed build for each branch in its CVE pages. Install the matching image or move the Operator subscription to a channel that carries the fixed build, then confirm that the Quay pods restarted on the new image.
- Record the current image for every Quay deployment:
oc get deployment -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}' - Update the Quay registry through the Operator, or pull the fixed image tag for your branch and roll the deployment.
- Confirm that the new pods carry the fixed image:
oc get pods -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.status.containerStatuses[0].image}{"\n"}{end}' - Repeat for every Quay instance, including disaster-recovery and staging registries that share credentials with production.
If you build the open-source project yourself, the fix is commit 094cfb024f01 on the master branch, merged on 2026-11-11. The newest upstream tag is v3.18.2, so no tagged upstream release contains the fix yet. Red Hat's fixed builds are the supported path.
If you cannot update immediately, reduce exposure with these measures. None replaces the patch.
- Block
/keysat the proxy or load balancer so only trusted internal components can reach it. This closes the key-takeover path. - List the approved service keys and remove any key you do not recognise, especially one with no expiration date. A removed key can be reinstalled while
/keysstays reachable, so do this only with the block in place. - Turn off anonymous access if your deployment does not need it. This removes the unauthenticated token source, but an attacker with any account may still reach the key server.
- For CVE-2026-110697, restrict who can create organizations, and rotate the robot tokens of any namespace whose name contains an underscore or that collides with one.
- Review audit records for
service_key_rotateandservice_key_deleteevents with a null expiration date, and for robot token use that does not match the expected automation.
What is the root cause?
The two flaws are separate mistakes in the same product. Both turn a trusted value into an authorization decision without checking what the value was issued for.
The first flaw is in the key server. _validate_jwt verifies the signature, the audience and the issuer, then discards the payload:
def _validate_jwt(encoded_jwt, jwk, service):
public_key = jwtutil.jwk_dict_to_public_key(jwk)
try:
jwtutil.decode(
encoded_jwt, public_key, algorithms=["RS256"], audience=JWT_AUDIENCE, issuer=service
)
except jwtutil.InvalidTokenError:
logger.exception("JWT validation failure")
abort(400)
A registry token minted by /v2/auth satisfies those three checks. The endpoint takes the audience from the client-supplied service parameter. It signs the token with the instance key:
audience_param = request.args.get("service")
token = generate_bearer_token(
audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
)
The key server then treats that token as proof that the caller may rotate a service key. It never checks for a rotation claim, and it never checks that the token is not a registry token. put_service_key accepts the new key with no expiration, and replace_service_key copies the signer's approval onto it and deletes the signer:
def replace_service_key(old_kid, kid, jwk, metadata, expiration_date):
try:
with db_transaction():
key = db_for_update(ServiceKey.select().where(ServiceKey.kid == old_kid)).get()
key.metadata.update(metadata)
ServiceKey.create(
name=key.name,
kid=kid,
service=key.service,
jwk=jwk,
metadata=key.metadata,
expiration_date=expiration_date,
rotation_duration=key.rotation_duration,
approval=key.approval,
)
key.delete_instance()
The second flaw is in the robot listing. _list_entity_robots builds a LIKE pattern from the namespace name without escaping the wildcard characters:
.where(User.robot == True, User.username ** (entity_name + "+%"))
Peewee turns ** into a LIKE comparison. In SQL, _ matches any single character and % matches any string. A namespace named ab_c therefore matches robots in abxc and ab-c. The same query joins RobotAccountToken, so the matching rows carry the decrypted tokens.
How is it exploited?
For CVE-2026-123710, the attacker needs network access to /keys and no account:
- Request an anonymous registry token from
/v2/auth, setting theserviceparameter to the Quay hostname so the audience matches. - Generate an RSA key pair and build a JWK for the public key.
- Send the anonymous token as the bearer credential in a
PUTto/keys/services/quay/keys/<kid>, with the attacker's JWK as the body and noexpirationparameter. - The key server validates the token signature, audience and issuer. It does not check that the token was issued for key rotation.
replace_service_keycopies the approved status from the instance key to the attacker's key, stores it without an expiration, and deletes the instance key.- The attacker signs registry tokens with the private key. Quay accepts them for any repository, including push and overwrite.
For CVE-2026-110697, the attacker needs an account that can create a namespace:
- Create an organization or user namespace whose name differs from a victim namespace only at one character, using an underscore in that position.
- Request the robot list for the new namespace. Quay builds a
LIKEpattern from the name and the underscore matches the victim's character. - The response includes the victim's robot accounts and their decrypted tokens.
- Use a stolen token to pull or push the victim's images. Renaming or deleting the attacker namespace can also rename or delete the victim's matching robots.
How does the attack flow?
One flaw turns an anonymous registry token into a trusted signing key, and the other turns a namespace name into a wildcard that reads another tenant's robot tokens.
anonymous registry token"] --> B["/v2/auth signs it with
the instance key"] B --> C["Key server checks the signature
but not the rotation claim"] C --> D["Attacker installs its own JWK
with no expiration"] D --> E["Approval copied to
the attacker key"] E --> F["Forged tokens accepted
for every repository"] G["Namespace name with
an underscore"] --> H["SQL LIKE matches another
tenant's robot names"] H --> I["Robot tokens read
across tenants"]
What can an attacker do?
An attacker who completes the key takeover can read and write every repository that Quay serves, including private images. The forged tokens are ordinary registry tokens, so the registry has no reason to reject them. The attacker can replace tags, inject an image into a CI/CD pipeline, or pull private images for review.
The key takeover also deletes the legitimate instance key. A later rotation by the real operator can be undone while the attacker key remains approved, unless the fixed build is installed and the rogue key is removed.
The robot-token flaw gives the attacker the permissions of the stolen robot. A robot with push access can overwrite images in the victim namespace. A robot with admin access can change repository settings. The attacker can also rename or delete the victim's robots through the attacker namespace.
Neither flaw needs user interaction. CVE-2026-123710 needs reachability of /keys and the default anonymous access. CVE-2026-110697 needs an account that can create a namespace in a multi-tenant deployment.
Why does the fix work?
The fix closes both paths in one commit, 094cfb024f01. It adds a rotation claim to the key server, pins the registry audience, requires an expiration for new keys, and escapes the robot LIKE pattern.
The key server now returns the decoded payload. It requires a key_rotation claim that matches the key being installed:
+ if "access" in payload or "context" in payload:
+ abort(403)
+
+ return payload
+
+
+def _validate_rotation_claim(payload, kid):
+ if payload.get(KEY_ROTATION_CLAIM) != kid:
+ abort(403)
- _validate_jwt(encoded_jwt, signer_jwk, service)
+ payload = _validate_jwt(encoded_jwt, signer_jwk, service)
+ _validate_rotation_claim(payload, kid)
+
+ if signer_key.expiration_date is not None:
+ expiration_date = min(expiration_date, signer_key.expiration_date)
A registry token carries access and context claims, so it is rejected. A rotation token must name the new key, so a token minted for another purpose cannot install a key. The new key must also carry a future expiration, and its lifetime is capped by the signer's own expiration:
- if expiration_date is not None:
- try:
- expiration_date = datetime.utcfromtimestamp(float(expiration_date))
- except ValueError:
- logger.exception("Error parsing expiration date on key")
- abort(400)
+ if expiration_date is None:
+ abort(400)
+
+ try:
+ expiration_date = datetime.utcfromtimestamp(float(expiration_date))
+ except (TypeError, ValueError, OverflowError):
+ logger.exception("Error parsing expiration date on key")
+ abort(400)
+
+ if expiration_date <= datetime.utcnow():
+ abort(400)
The audience is pinned to the configured hostname, so an attacker cannot ask /v2/auth for a token aimed at another service:
token = generate_bearer_token(
- audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
+ app.config["SERVER_HOSTNAME"],
+ subject,
+ context,
+ access,
+ TOKEN_VALIDITY_LIFETIME_S,
+ instance_keys,
)
The robot listing now uses prefix_search, which escapes \, % and _ before the query:
- .where(User.robot == True, User.username ** (entity_name + "+%"))
+ .where(User.robot == True, prefix_search(User.username, entity_name + "+"))
The fix also scopes key replacement and deletion to the named service, so a key from one service cannot replace or delete a key in another. The tests added in the commit reproduce the underscore collision, the username change and the namespace deletion paths.
How do I check if I was compromised?
The key server records service-key actions in the audit log. Look for rotations and deletions that the operator did not perform, and for keys without an expiration date:
service_key_rotateandservice_key_deleteevents with a null expiration.service_key_createevents for a key you do not recognise.- Registry pulls or pushes by a robot whose token use does not match its automation.
The superuser API lists the approved service keys. Review the list and the key metadata, including the expiration:
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/superuser/keys
For the robot-token flaw, list the robots of namespaces whose names contain an underscore, and compare them with the expected automation:
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/organization/<org>/robots
There are no published network indicators, file names or request signatures for either flaw. A successful key takeover leaves an approved key in the database, and that key is the strongest evidence. A failed attempt may leave only a 400 or 403 on /keys and no audit entry.
Timeline
- 2026-10-14: Red Hat files the first bug, for the robot-listing
LIKEinjection (CVE-2026-110697). - 2026-11-06: Red Hat files the second bug, for the unauthenticated key rotation (CVE-2026-123710).
- 2026-11-11: Red Hat publishes both CVE records at 15:02 and 17:06 UTC, and merges the fix commit at 19:43 UTC. Red Hat says Quay.io is fixed and names the fixed builds for each branch.
The two reports reached a fix in 28 days: 23 days from the first report to the second, and 5 days from the second report to the fix.
FAQ
Does this affect Quay.io?
No for the hosted service as of November 11, 2026. Red Hat says it has already fixed Quay.io. The flaws affect self-hosted Red Hat Quay deployments that have not applied the fixed build.
Is anonymous access required for the key takeover?
No, but it removes the need for an account. FEATURE_ANONYMOUS_ACCESS is True by default in Quay 3.18.2, and /v2/auth then mints the token the key server accepts. If anonymous access is off, an attacker with any valid account may still obtain a registry token and try the same request.
Can an attacker with a stolen robot token take over the registry?
Only within that robot's permissions. A robot with push access can overwrite images in its namespace. The key takeover is different, because it forges tokens for every repository, including repositories the attacker cannot name today.
Which version should I install?
Install the fixed build for your branch: 3.9.28, 3.10.28, 3.12.24, 3.14.10, 3.15.9, 3.16.7, 3.17.7 or 3.18.3. If you build from source, apply commit 094cfb024f01. The newest upstream tag is still v3.18.2, so a tagged upstream release does not contain the fix yet.
Do I need to rotate robot tokens?
Yes, if a namespace name in your deployment contains an underscore and another tenant's namespace differs only at that character. Rotate the robots in both namespaces and review their recent pulls and pushes. A token that was read cannot be distinguished from a token that was not used.
Is there a workaround if I cannot update?
Block /keys at the proxy and remove unrecognised approved keys. Restrict organization creation and rotate colliding robot tokens. These measures reduce exposure, but the key server still trusts an anonymous registry token until the fixed build is installed.
References
- Red Hat CVE page for CVE-2026-123710, https://access.redhat.com/security/cve/CVE-2026-123710 (checked November 12, 2026).
- Red Hat CVE page for CVE-2026-110697, https://access.redhat.com/security/cve/CVE-2026-110697 (checked November 12, 2026).
- Red Hat Bugzilla 2568072, the key-rotation flaw, https://bugzilla.redhat.com/show_bug.cgi?id=2568072 (checked November 12, 2026).
- Red Hat Bugzilla 2552237, the robot-listing flaw, https://bugzilla.redhat.com/show_bug.cgi?id=2552237 (checked November 12, 2026).
- Quay fix commit 094cfb024f01, https://github.com/quay/quay/commit/094cfb024f01 (checked November 12, 2026).
- Quay v3.18.2 source,
endpoints/keyserver/__init__.py, https://github.com/quay/quay/blob/v3.18.2/endpoints/keyserver/init.py (checked November 12, 2026). - Quay v3.18.2 source,
data/model/user.py, https://github.com/quay/quay/blob/v3.18.2/data/model/user.py (checked November 12, 2026). - Quay v3.18.2 source,
endpoints/v2/v2auth.py, https://github.com/quay/quay/blob/v3.18.2/endpoints/v2/v2auth.py (checked November 12, 2026). - Quay v3.18.2 source,
data/model/service_keys.py, https://github.com/quay/quay/blob/v3.18.2/data/model/service_keys.py (checked November 12, 2026). - CVE-2026-123710 record, https://www.cve.org/CVERecord?id=CVE-2026-123710 (checked November 12, 2026).
- CVE-2026-110697 record, https://www.cve.org/CVERecord?id=CVE-2026-110697 (checked November 12, 2026).
- CISA Known Exploited Vulnerabilities catalog, https://www.cisa.gov/known-exploited-vulnerabilities-catalog (checked November 12, 2026).