0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

片方のHTTPSだけ通る謎:token交換は成功、/users/meはX509 code 20の正体

0
Posted at

片方のHTTPSだけ通る謎:token交換は成功、/users/meはX509 code 20の正体

Rust + Docker(slim)でX OAuthを実装したら、oauth2クレートのトークン交換は通るのに、自前のreqwest/users/meを叩くとだけunable to get local issuer certificateで死んだ。3日溶かした原因は「TLSバックエンドが2種類混在」だった。

環境

  • Rust nightly (rustlang/rust:nightly-bookworm) / edition 2021 / axum
  • oauth2 = "4.4" (内部で reqwest 0.11 を引き、その TLS は rustls + webpki-roots)
  • reqwest = "0.12" (アプリ直依存。default = native-tls / OpenSSL)
  • runtime: debian:bookworm-slim

症状は二段ロケット

  1. authorizeでXが「問題が発生しました/アプリにアクセスを許可できません」
  2. PKCE直したら今度はcallbackが500で{"code":"SRV_001"}

ログを掘ると最後はこれ:

reqwest::Error: X509 code 20: unable to get local issuer certificate

同じXのAPI、同じコンテナ、同じタイミングなのに片方だけ落ちる。

原因1:XはPKCEが必須になっていた

最初はこれ。X OAuth 2.0は今、confidential clientでもPKCEしか受け付けない。OAuth 2.1の仕様でもPKCEを全クライアントに必須化していて、もともとモバイル用だったPKCEが今は全てのOAuthクライアントに適用されるようになっている。

僕のx_logincode_challengeを送っていなかった。そりゃXが拒否する。

修正は1行:

let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let auth_url = client
   .authorize_url(|| CsrfToken::new_random())
   .set_pkce_challenge(pkce_challenge)
   .url();
 // verifierはセッションに保存 (tower-sessions の insert は async)
session.insert("pkce_verifier", pkce_verifier).await?;

callback側で:

let token = client
   .exchange_code(code)
   .set_pkce_verifier(pkce_verifier)
   .request_async(async_http_client)
   .await?;

原因2:callbackがエラーを握りつぶしていた

PKCEを入れてもまだ500。理由はこっち:

#[derive(Deserialize)]
struct AuthCallbackQuery {
    code: String, // ←必須にしてた
    state: String,
}

Xが?error=access_denied&error_description=...を返すとserdeが「missing field code」で落ちて、真のエラーがログに出ない。自分で真因を隠してた。

全部Optionにして素直に受ける:

#[derive(Debug, Deserialize)]
struct AuthCallbackQuery {
    code: Option<String>,
    state: Option<String>,
    error: Option<String>,
    error_description: Option<String>,
}

if let Some(err) = q.error {
    tracing::error!(error = %err, desc = ?q.error_description, "X OAuth error");
    return Err(StatusCode::BAD_REQUEST);
}

原因3(本丸):CA証明書欠落とTLS非対称

ここが「片方だけ通る」の正体。

  • oauth2 = 4.4 が内部で引く reqwest 0.11 は rustls + webpki-roots を使う。 webpki-roots は「Mozilla's root certificates for use with the webpki or rustls crates」、つまり「compiled-in copy of the root certificates trusted by Mozilla」で、ルート証明書がバイナリに焼き込まれていてOSのCAストアに依存しない。だから slim でも token 交換は通った。
  • 一方アプリが直接使う reqwest 0.12 はデフォルトで native-tls、つまり OpenSSL。 OpenSSL はOSのトラストストア /etc/ssl/certs を見る。
  • debian:bookworm-slimca-certificatesが入っていない → OpenSSL のトラストストアが空 → /users/meだけ「unable to get local issuer certificate」。ca-certificatesで**ルート証明書(トラストアンカー)**を入れると解決する。Debian系でCAが無いとcurlでも同じになる典型例だ。

依存ツリーで裏が取れる。rustls 用の CA (webpki-roots) と native-tls をそれぞれ逆引きすると、別バージョンの reqwest にぶら下がっているのが見える:

$ cargo tree -i webpki-roots
webpki-roots v0.25.4
└── reqwest v0.11.27          # ← oauth2 が内部で引く古い方 (rustls)
    └── oauth2 v4.4.2
        └── axum_vue_api v0.1.0

$ cargo tree -i native-tls
native-tls v0.2.14
├── hyper-tls v0.6.0
│   └── reqwest v0.12.22      # ← アプリが直接使う方 (OpenSSL)
│       └── axum_vue_api v0.1.0
└── …(dev-dependencies は略)

つまり同一プロセス内に reqwest が2バージョン同居し、TLS実装も2つ(rustls / OpenSSL)、片方だけルート証明書を内蔵していた。これが「片方だけ通る」の正体。

修正は3つだけ

  1. DockerfileにCA追加
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
 && rm -rf /var/lib/apt/lists/*
  1. 可能ならreqwestもrustlsに統一
# Cargo.toml
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
  1. 上のPKCEとcallback修正

これでtoken/users/meも同じTLSスタックで通る。

再発防止チェックリスト

  • slim/distrolessを使うならca-certificatesは必ず入れる。入れないとビルドは通るのに実行時だけ死ぬ
  • 「Aは通る、Bは証明書エラー」を見たらcargo tree | grep -E "rustls|native-tls"でバックエンド混在を疑う
  • OAuthのcallbackは成功形だけを必須にしない。error/error_descriptionを必ず受ける
  • X APIはもうPKCE無しは通らない。confidentialでもcode_challenge必須

まとめ

症状が原因を隠す典型だった。ログのX509 code 20だけ見ると「Xが不安定」に見えるけど、実態は「rustlsは内蔵CAで動く、OpenSSLは空っぽ」。

片方のHTTPSだけ通る時は、証明書じゃなくTLSバックエンドの非対称を最初に疑おう。


同じ轍を踏んだ人、他にも「slimでハマった」ネタあれば教えてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?