Red Hat Quay:鍵のローテーションによるレジストリ乗っ取り(CVE-2026-123710)
Red Hat Quay の分岐版の修正前では、認証されていない呼び出し元が署名鍵を設置し、任意のリポジトリ用のトークンを偽造できます。
要点
- Red Hat Quay の鍵サーバーは、匿名のレジストリトークンを鍵ローテーションの資格情報として受け入れます。
/keysに到達できる攻撃者は署名鍵を設置し、すべてのリポジトリ用のトークンを偽造できます。脆弱性は CVE-2026-123710 として 2026 年 11 月 11 日に公開されました。 - 2つ目の重大な脆弱性 CVE-2026-110697 では、名前空間を作成できるユーザーが、エスケープされていない SQL の
LIKEパターンを通じて他のテナントのロボットアカウントのトークンを読み取れます。どちらも、各分岐版の修正前の自己ホスト型 Quay に影響します。 - 2つの脆弱性はどちらも CVSS v3.1 で重大(Critical)です。CVE-2026-123710 は 9.8、CVE-2026-110697 は 9.9 です。悪用は確認されておらず、2026 年 11 月 12 日時点でどちらも CISA の KEV カタログに登録されていません。Red Hat は Quay.io を修正済みとしています。
- 使用中の分岐版の修正版(3.9.28、3.10.28、3.12.24、3.14.10、3.15.9、3.16.7、3.17.7、3.18.3)に更新してください。更新できない場合は、プロキシで
/keysを遮断し、承認済みのサービス鍵を確認してください。
影響を受けますか?
確認する脆弱性に当てはまる条件がすべて成立すると影響を受けます。
- 自己ホスト型の Red Hat Quay 3 または Quay 5 を運用しており、そのビルドが分岐版の修正版より古い。
- CVE-2026-123710 では、
/keysエンドポイントに信頼できないクライアントから到達できる。Quay 3.18.2 では匿名アクセスが既定で有効であり(FEATURE_ANONYMOUS_ACCESS = True)、鍵サーバーが受け入れるのはこの匿名トークンです。 - CVE-2026-110697 では、展開がマルチテナントであり、攻撃者が組織またはユーザー名前空間を作成できるアカウントを持っている。
鍵乗っ取りの脆弱性にはアカウントが不要です。ロボットトークンの脆弱性には、攻撃者が名前を付けられる名前空間が必要です。単一テナントのレジストリでも、偽造トークンがすべてのリポジトリに届くため、1つ目の脆弱性の影響を受けます。
| 分岐 | 影響を受けるバージョン | 修正版 |
|---|---|---|
| 3.9 | 3.9.28 より前のすべてのリリース | 3.9.28 |
| 3.10 | 3.10.28 より前のすべてのリリース | 3.10.28 |
| 3.12 | 3.12.24 より前のすべてのリリース | 3.12.24 |
| 3.14 | 3.14.10 より前のすべてのリリース | 3.14.10 |
| 3.15 | 3.15.9 より前のすべてのリリース | 3.15.9 |
| 3.16 | 3.16.7 より前のすべてのリリース | 3.16.7 |
| 3.17 | 3.17.7 より前のすべてのリリース | 3.17.7 |
| 3.18 | 3.18.3 より前のすべてのリリース | 3.18.3 |
Red Hat は CVE のページで Red Hat Quay 5 も影響を受けるとしています。ただし、そのページに Quay 5 専用のビルドは記載されていません。Quay 5 の利用者は、更新を計画する前に契約のエラータを確認してください。
OpenShift の展開では、実行中の Quay イメージのタグがビルドを示します。タグは Operator が設定するため、Operator のバージョンではなく Deployment を確認します。
oc get deployment -A -l quay-component=quay-app \
-o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}'
Podman の展開では、実行中のコンテナを検査します。
podman inspect quay --format '{{.Config.Image}}'
タグが Quay のビルドです。読み取り可能なタグのないダイジェストもビルドに対応し、skopeo inspect docker://<image> がそのダイジェストのラベルを表示します。
修正方法
Red Hat は CVE のページで分岐版ごとの修正版を示しています。対応するイメージを導入するか、Operator のサブスクリプションを修正版を含むチャネルに移し、Quay の Pod が新しいイメージで再起動したことを確認します。
- 各 Quay 展開の現在のイメージを記録します。
oc get deployment -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.template.spec.containers[0].image}{"\n"}{end}' - Operator で Quay レジストリを更新するか、分岐版の修正済みタグを取得して再展開します。
- 新しい Pod が修正済みイメージを使用していることを確認します。
oc get pods -A -l quay-component=quay-app \ -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.status.containerStatuses[0].image}{"\n"}{end}' - 本番環境と資格情報を共有する災害復旧用およびステージング用のレジストリを含め、すべての Quay インスタンスで繰り返します。
オープンソースのプロジェクトを自分でビルドする場合、修正は master ブランチのコミット 094cfb024f01 であり、2026-11-11 にマージされました。上流の最新タグは v3.18.2 のままなので、タグ付きの上流リリースにはまだ修正が含まれていません。Red Hat の修正版がサポートされる経路です。
すぐに更新できない場合は、次の対策で露出を減らしてください。どれも修正の代わりにはなりません。
- プロキシまたはロードバランサーで
/keysを遮断し、信頼できる内部コンポーネントだけが到達できるようにします。これで鍵乗っ取りの経路が閉じます。 - 承認済みのサービス鍵を一覧表示し、覚えのない鍵、とくに有効期限のない鍵を削除します。削除した鍵は
/keysが到達可能なままだと再設置できるため、遮断を適用した状態でのみ行ってください。 - 展開で不要であれば匿名アクセスを無効にします。これで認証なしのトークン源はなくなりますが、アカウントを持つ攻撃者は鍵サーバーに到達できる可能性があります。
- CVE-2026-110697 では、組織を作成できる人を制限し、名前にアンダースコアを含む名前空間や衝突する名前空間のロボットトークンをローテーションします。
- 有効期限が null の
service_key_rotateとservice_key_deleteの監査記録、および想定した自動化と一致しないロボットトークンの使用を確認します。
根本原因は何ですか?
2つの脆弱性は同じ製品の別々の誤りです。どちらも、信頼された値を、その値が何のために発行されたかを確認せずに認可の判断に変えます。
1つ目は鍵サーバーにあります。_validate_jwt は署名、オーディエンス、発行者を検証したあと、ペイロードを捨てます。
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)
/v2/auth が発行するレジストリトークンは、この3つの確認を満たします。このエンドポイントは、クライアントが指定した service パラメータからオーディエンスを取り、インスタンス鍵でトークンに署名します。
audience_param = request.args.get("service")
token = generate_bearer_token(
audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys
)
鍵サーバーは、そのトークンをサービス鍵をローテーションできる証拠として扱います。ローテーションのクレームを確認せず、トークンがレジストリトークンでないことも確認しません。put_service_key は新しい鍵を有効期限なしで受け入れ、replace_service_key は署名者の承認を新しい鍵にコピーして署名者を削除します。
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()
2つ目はロボットの一覧表示にあります。_list_entity_robots は、ワイルドカード文字をエスケープせずに名前空間名から LIKE パターンを作ります。
.where(User.robot == True, User.username ** (entity_name + "+%"))
Peewee は ** を LIKE 比較に変換します。SQL では _ が任意の1文字、% が任意の文字列に一致します。したがって ab_c という名前空間は abxc と ab-c のロボットに一致します。同じクエリが RobotAccountToken を結合するため、一致した行は復号済みのトークンを持ちます。
どのように悪用されますか?
CVE-2026-123710 では、攻撃者に /keys へのネットワーク到達性があればよく、アカウントは不要です。
serviceパラメータを Quay のホスト名に設定して/v2/authから匿名レジストリトークンを要求します。- RSA 鍵ペアを生成し、公開鍵の JWK を作ります。
- 匿名トークンを認証情報として
/keys/services/quay/keys/<kid>へのPUTに送り、本文に攻撃者の JWK を置き、expirationパラメータを付けません。 - 鍵サーバーはトークンの署名、オーディエンス、発行者を検証します。トークンが鍵ローテーション用に発行されたかは確認しません。
replace_service_keyはインスタンス鍵の承認状態を攻撃者の鍵にコピーし、有効期限なしで保存してインスタンス鍵を削除します。- 攻撃者は秘密鍵でレジストリトークンに署名します。Quay はそれを、プッシュと上書きを含む任意のリポジトリで受け入れます。
CVE-2026-110697 では、攻撃者に名前空間を作成できるアカウントが必要です。
- 被害者の名前空間と1文字だけ異なり、その位置にアンダースコアを持つ組織またはユーザー名前空間を作成します。
- 新しい名前空間のロボット一覧を要求します。Quay は名前から
LIKEパターンを作り、アンダースコアが被害者の文字に一致します。 - 応答には被害者のロボットアカウントと復号済みトークンが含まれます。
- 盗んだトークンで被害者のイメージを取得またはプッシュします。攻撃者の名前空間を改名または削除すると、一致する被害者のロボットも改名または削除されることがあります。
攻撃の流れは?
一方の脆弱性は匿名レジストリトークンを信頼された署名鍵に変え、もう一方は名前空間名を、別テナントのロボットトークンを読むワイルドカードに変えます。
トークンを要求"] --> B["/v2/auth がインスタンス鍵で
署名"] B --> C["鍵サーバーは署名を確認するが
ローテーションのクレームを確認しない"] C --> D["攻撃者が有効期限なしの
JWK を設置"] D --> E["承認が攻撃者の鍵に
コピーされる"] E --> F["偽造トークンがすべての
リポジトリで受け入れられる"] G["アンダースコアを含む
名前空間名"] --> H["SQL の LIKE が別テナントの
ロボット名に一致"] H --> I["テナントを越えてロボット
トークンを読み取る"]
攻撃者に何ができますか?
鍵乗っ取りを完了した攻撃者は、Quay が提供するすべてのリポジトリを読み書きできます。これにはプライベートイメージも含まれます。偽造トークンは通常のレジストリトークンなので、レジストリが拒否する理由はありません。攻撃者はタグを置き換え、CI/CD パイプラインにイメージを注入し、プライベートイメージを取得して調査できます。
鍵乗っ取りは正規のインスタンス鍵も削除します。修正版を導入して不正な鍵を削除しない限り、攻撃者の鍵が承認されたままなので、正規の運用者による後日のローテーションは取り消される可能性があります。
ロボットトークンの脆弱性では、攻撃者は盗んだロボットの権限を得ます。プッシュ権限を持つロボットは被害者の名前空間のイメージを上書きできます。管理者権限を持つロボットはリポジトリ設定を変更できます。攻撃者は自分の名前空間を通じて被害者のロボットを改名または削除することもできます。
どちらの脆弱性もユーザーの操作を必要としません。CVE-2026-123710 は /keys への到達性と既定の匿名アクセスを必要とします。CVE-2026-110697 はマルチテナント展開で名前空間を作成できるアカウントを必要とします。
修正が機能する理由
修正は1つのコミット 094cfb024f01 で両方の経路を閉じます。鍵サーバーにローテーションのクレームを追加し、レジストリのオーディエンスを固定し、新しい鍵に有効期限を要求し、ロボットの LIKE パターンをエスケープします。
鍵サーバーはデコードしたペイロードを返すようになりました。設置する鍵と一致する key_rotation クレームを要求します。
+ 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)
レジストリトークンは access と context のクレームを持つため拒否されます。ローテーショントークンは新しい鍵を指定する必要があるため、別の用途で発行されたトークンでは鍵を設置できません。新しい鍵には将来の有効期限も必要で、その寿命は署名者の有効期限で上限が決まります。
- 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)
オーディエンスは設定されたホスト名に固定されるため、攻撃者は別のサービス向けのトークンを /v2/auth に要求できません。
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,
)
ロボットの一覧表示は prefix_search を使うようになり、クエリの前に \、%、_ をエスケープします。
- .where(User.robot == True, User.username ** (entity_name + "+%"))
+ .where(User.robot == True, prefix_search(User.username, entity_name + "+"))
修正は鍵の置換と削除も指定したサービスに限定するため、あるサービスの鍵が別のサービスの鍵を置換または削除することはできません。コミットで追加されたテストは、アンダースコアの衝突、ユーザー名の変更、名前空間の削除の経路を再現します。
侵害されたかどうかを確認するには?
鍵サーバーはサービス鍵の操作を監査ログに記録します。運用者が行っていないローテーションと削除、および有効期限のない鍵を探してください。
- 有効期限が null の
service_key_rotateとservice_key_deleteイベント。 - 覚えのない鍵の
service_key_createイベント。 - トークンの使用が自動化と一致しないロボットによるレジストリの取得またはプッシュ。
スーパーユーザー API は承認済みのサービス鍵を一覧表示します。有効期限を含む鍵のメタデータと一覧を確認してください。
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/superuser/keys
ロボットトークンの脆弱性では、名前にアンダースコアを含む名前空間のロボットを一覧表示し、想定した自動化と比較してください。
curl -s -H "Authorization: Bearer <token>" https://<quay-host>/api/v1/organization/<org>/robots
どちらの脆弱性にも、公開されたネットワーク指標、ファイル名、リクエスト署名はありません。鍵乗っ取りが成功すると承認済みの鍵がデータベースに残り、その鍵が最も強い証拠になります。失敗した試行は /keys の 400 または 403 だけを残し、監査エントリを残さないことがあります。
年表
- 2026-10-14:Red Hat がロボット一覧の
LIKE注入に関する最初のバグを起票します(CVE-2026-110697)。 - 2026-11-06:Red Hat が認証なしの鍵ローテーションに関する2つ目のバグを起票します(CVE-2026-123710)。
- 2026-11-11:Red Hat が 15:02 と 17:06 UTC に2つの CVE レコードを公開し、19:43 UTC に修正コミットをマージします。Red Hat は Quay.io を修正済みとし、各分岐版の修正版を示しています。
2つの報告から修正まで28日でした。最初の報告から2つ目までが23日、2つ目の報告から修正までが5日です。
よくある質問
Quay.io は影響を受けますか?
2026年11月11日時点のホスト型サービスは影響を受けません。Red Hat は Quay.io を修正済みとしています。脆弱性は、修正版を適用していない自己ホスト型の Red Hat Quay に影響します。
鍵乗っ取りに匿名アクセスは必要ですか?
いいえ。ただしアカウントが不要になります。Quay 3.18.2 では FEATURE_ANONYMOUS_ACCESS が既定で True であり、/v2/auth が鍵サーバーの受け入れるトークンを発行します。匿名アクセスが無効でも、有効なアカウントを持つ攻撃者はレジストリトークンを取得して同じ要求を試せます。
盗んだロボットトークンでレジストリを乗っ取れますか?
そのロボットの権限の範囲内だけです。プッシュ権限を持つロボットは自分の名前空間のイメージを上書きできます。鍵乗っ取りは異なり、攻撃者が今は名指しできないリポジトリを含むすべてのリポジトリのトークンを偽造します。
どのバージョンを導入すべきですか?
分岐版の修正版を導入してください。3.9.28、3.10.28、3.12.24、3.14.10、3.15.9、3.16.7、3.17.7、3.18.3 です。ソースからビルドする場合はコミット 094cfb024f01 を適用してください。上流の最新タグは依然として v3.18.2 なので、タグ付きの上流リリースにはまだ修正が含まれていません。
ロボットトークンをローテーションすべきですか?
展開内の名前空間名にアンダースコアが含まれ、別の名前空間がその文字だけ異なる場合は、はい。両方の名前空間のロボットをローテーションし、最近の取得とプッシュを確認してください。読み取られたトークンと使われなかったトークンは区別できません。
更新できない場合の回避策はありますか?
プロキシで /keys を遮断し、覚えのない承認済み鍵を削除してください。組織の作成を制限し、衝突するロボットトークンをローテーションします。これらの対策は露出を減らしますが、修正版を導入するまで鍵サーバーは匿名レジストリトークンを信頼し続けます。
参考資料
- CVE-2026-123710 の Red Hat CVE ページ、https://access.redhat.com/security/cve/CVE-2026-123710(2026年11月12日確認)。
- CVE-2026-110697 の Red Hat CVE ページ、https://access.redhat.com/security/cve/CVE-2026-110697(2026年11月12日確認)。
- Red Hat Bugzilla 2568072、鍵ローテーションの脆弱性、https://bugzilla.redhat.com/show_bug.cgi?id=2568072(2026年11月12日確認)。
- Red Hat Bugzilla 2552237、ロボット一覧の脆弱性、https://bugzilla.redhat.com/show_bug.cgi?id=2552237(2026年11月12日確認)。
- Quay の修正コミット 094cfb024f01、https://github.com/quay/quay/commit/094cfb024f01(2026年11月12日確認)。
- Quay v3.18.2 のソース、
endpoints/keyserver/__init__.py、https://github.com/quay/quay/blob/v3.18.2/endpoints/keyserver/init.py(2026年11月12日確認)。 - Quay v3.18.2 のソース、
data/model/user.py、https://github.com/quay/quay/blob/v3.18.2/data/model/user.py(2026年11月12日確認)。 - Quay v3.18.2 のソース、
endpoints/v2/v2auth.py、https://github.com/quay/quay/blob/v3.18.2/endpoints/v2/v2auth.py(2026年11月12日確認)。 - Quay v3.18.2 のソース、
data/model/service_keys.py、https://github.com/quay/quay/blob/v3.18.2/data/model/service_keys.py(2026年11月12日確認)。 - CVE-2026-123710 レコード、https://www.cve.org/CVERecord?id=CVE-2026-123710(2026年11月12日確認)。
- CVE-2026-110697 レコード、https://www.cve.org/CVERecord?id=CVE-2026-110697(2026年11月12日確認)。
- CISA の悪用済み脆弱性カタログ、https://www.cisa.gov/known-exploited-vulnerabilities-catalog(2026年11月12日確認)。