hgot07 Hotspot Blog

主に無線LANや認証連携などの技術についてまとめるブログです。ネコは見る専。

LinuxでO_CREATを使うとグループパーミッションがあるのに既存ファイルがオープンできない問題

最近、openSUSE Leap 15.6から16.0に乗り換え作業をしていたら、nmhのincコマンドが

"inc: error zero'ing /var/mail/user1: Permission denied, continuing..."

というエラーを吐いて、ファイルをゼロクリアできないことに気付きました。色々と原因の可能性をつぶしていった結果、表題のような問題だと判明しました。丸一日付き合ってくれたCopilotくんに感謝していますが、色々とヒントをくれたものの、ズバリの回答は出せませんでした。

原因究明のヒントになった記事がこちらです。

unix.stackexchange.com

ざっくり言うと、

# sysctl fs.protected_regular=0

にすると従来通り動作することが分かりました。

 

症状

以下のような条件で症状が出ました。

  1. /var/mail/user1 は owner=user1, group=user1grp で、group R/W permissionが付いている。すなわち -rw-rw---- (0660) の状態。
  2. incコマンドを実行するユーザは user2 で、user1grp がgroupsに含まれている。
    (コマンドラインからは問題なく cp /dev/null /var/mail/user1 とかできる)

当初、様々なものを疑いましたが、どれも当たらず_('、3」∠)_

  1. incコマンドのバグ?
  2. メールスプールをNFSマウントしているので、NFSの問題か?
    (NFSv4からNFSv3に変更しても変わらず)
  3. ロック機能の問題?
  4. メールスプールのパーミッション、拡張パーミッションの問題?
    (ファイルを /tmp に移してみてもダメ)
  5. umaskがおかしい?
    (umask 000 でもダメ)
  6. AppArmorがブロックしている?

 

原因究明のためのツール

問題を絞り込むために、半日以上Copilotくんに相手をしてもらい、もらったサンプルコードを少し手直ししたのがこちら。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
  int fd = open("/tmp/user1", O_WRONLY|O_CREAT|O_NONBLOCK, 0666);
  if(fd==-1){ perror("open"); return 1; }
  close(fd);
  puts("open ok"); return 0;
}

実行すると open: Permission denied になります。

次のコマンドにより、EACCES になっていることが分かりました。

(ユーザ user2 として実行)
$ strace -f -s 200 -e trace=process,openat,open,stat,access -o /tmp/open-c-full.strace ./open_test

$ sed -n '1,200p' /tmp/open-c-full.strace | egrep 'setgroups|setgid|setuid|setreuid|setresuid|openat|open|access|stat' | tail -n 80

出力はこんな感じ ↓

5242  execve("./open_test", ["./open_test"], 0x7ffc4ea90ed8 /* 66 vars */) = 0
5242  access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
5242  openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
5242  openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
5242  openat(AT_FDCWD, "/var/tmp/user1", O_WRONLY|O_CREAT|O_NONBLOCK, 0666) = -1 EACCES (Permission denied)

openのところで O_CREAT を外すと、open ok になります。つまり、上書きはグループパーミッションで正常にできるということです。

 

解決にむけて

ここまで相当の時間を費やしたのですが、ファイルサーバとして既に動いていたので、AppArmorを止めてみるところまでは試しませんでした。

翌朝、前述した記事を見つけて試してみたところ、見事に open ok になりました。

セキュリティ強化のために Linux に導入された fs.protected_regular が悪さをしていたようです。

しかし、これをオフにしたくはないので、別の方法がないかどうかさらに調査を進めます。

/tmp や /var/mail にスティッキービットが立っているのが気になって、Copilotくんに「影響ある?」と聞いてみたところ、きっぱりと否定されてしまいました。

しかし、どうしても気になったので、別のディレクトリを 0777 で掘って、そこで試してみたところ、open ok になるじゃあありませんか!chmod 1777すると、失敗します。

再度Copilotくんに聞いてみたところ、このようなお答えでした ↓

What to do (recommendations)

    Don’t remove the sticky bit on a multi-user shared directory like /tmp — that would be insecure.
    Preferred secure fixes while keeping fs.protected_regular=1 and keeping the sticky bit:
        Make the file owned by the writer process: sudo chown <writer-user>:<writer-group> /var/mail/user1 This is the simplest correct fix if that ownership change is acceptable.
        Avoid O_CREAT/O_TRUNC as a non-owner — write to a temp file and atomically rename into place: tmp=$(mktemp /var/mail/user1.tmp.XXXXXX)

はい、要するに /var/mail の中で group permission でゼロクリアするのは無理そうということです。

 

まとめ

こんな条件で問題が発生していたことが分かりました。

  1. 最近のLinuxで、fs.protected_regular が有効になっている。
  2. ディレクトリにsticky bitが立っている。
  3. プログラム内で既存ファイルをopenするところに、O_CREAT が含まれている。(おそらくO_TRUNCも同様)
  4. openしようとするファイルのownerは、プログラムを実行するownerと異なるが、ファイルにgroup r/w permissionが付いていて、実行者はそのグループに属している (他のプログラムでは正常に読み書きできることがある)。

原因は分かったものの、完全な解決には至りませんでした。

 

だ、だれかー_(゚。3」∠)_ 

省スペースPCのx1スロットにSSDを増設する (10G NICも載せたい)

x1のスロットしか空いていないみなさん、いかがですか? 😊

 

省スペースPCにSolarisを載せてファイルサーバにしていたのですが、Oracle Solarisになってから変更された管理コマンド類が覚えられなくて、他の人もいじれないので、Linuxに移行することにしました。AMD Ryzen 7 PRO 4750Gを搭載した、HP ProDesk 405 G6 SFFを使っています。

省スペースPCでありがちなのが、PCI Express x1とx16の二本構成です。さて、SSDと10G NICを増設したいのですが……、まぁ難しいですね😅

 

10G NICでPCIe x1は無理

他のNASとのデータのやり取りもしたいので、高速なNICを積みたいところです。しかし、10GbpsともなるとPCIe x4以上の製品になるので、x16側に挿すしかありません。

探しまわったところ、10GbEでPCIe x1対応の製品もあるようですが、PCIe 4.0でないと速度が出ません。5Gbpsで諦めてもよいなら、RTL8126搭載のx1対応の製品が若干あります。

増設するSSDに爆速は要求しないので、SSDにどちらのスロットを使うかで、NICへの要求が決まります。

 

PCIe x1のスロットにNVMe SSDを載せられるか

マザーボードにはSSDが一枚しか乗らないので、PCIeスロットに増設するしかありません。速度を求めないならUSBでつなぐという手もあるでしょうが、外付けでぶらぶらは少し気持ち悪いので、今回はもう少し頑張ってみます。

M.2 NVMe - PCIe のカードは色々とあるのですが、ほとんどが PCIe x4 タイプなので、x1スロットには刺さりません。諦めかけていたところで、やっと見つかりました! (PR)

 

 

NVMe - PCIe変換カード (パッケージ内容)

NVMe - PCIe変換カード (パッケージ内容)

NVMe - PCIe変換カード (仕様)

NVMe - PCIe変換カード (仕様)

ロープロファイル用のブラケットもちゃんとあります (最優先事項)。

ブラケットが取り付けられない状態で届くので、ノーマルの方を外す手間がありません (笑)

これに、EXCERIA PLUS G4 NVMe SSD (2TB) なんていう、贅沢仕様のSSDを載せてみます。リード最大10,000MB/sとかいう製品なので、本当にもったいないのですが、安く買えたのでオーライ。

 

速度評価

PCIe x1でどれぐらいの速度になるか、ざっくりと測ってみました。

hdparm -t で 766.81 MB/s と出ました。ゎー、さすがに遅い 😅

マザーボード直付けなら3,000 MB/sは出ます。USB 3.2のケースに入れた場合は、443 MB/s でした。SATAのHDDなら200 MB/sとかなので、悪くはないです。

というわけで、今回はNICの方を優先させてみました。使用したのはこちらで、PCIe x8、SFP+ 2系統という欲張りなものです。

 

同型のPCにも同じものを挿して、Twinaxで接続してみました。

iperf3 で 9.42 Gbits/sec とか出ているので、合格でしょう。

 

おしまい

"ESSID"という用語はもはや存在しない

無線LAN関係でよく見かける "ESSID" という用語、実はもう使われません!

ΩΩ Ω<な、なんだってー!!

先日テレカンで指摘されるまで知りませんでした_('、3」∠)_

 

ウェブを見ると、ESSID (Extended Service Set Identifier) として2025年の記事でも書かれていることが多いです。かつては、SSIDがESSIDの省略形として扱われていたのですが、仕様上はSSIDで、ESSIDは存在しないそうです。

IEEE 802.11-2020を見てみると、ESSIdentifier という用語は見えますが、ESSID はないことが分かります。-2016 や -2007 まで遡ってみても、見当たりません。

 

standards.ieee.org

RFC 5414 (obsoleted by RFC 5415) や、各種ソースコードの中には、名残を見つけることができました。

 

datatracker.ietf.org

ESS (Extended Service Set) という用語はあります。

BSSID (Basic Service Set Identifier) も存在します。基地局MACアドレスに相当するものです。

似た名前で HESSID (Homonenuous Extended Service Set Identifier) という用語がありますが、これは基地局MACアドレスを設定するものです。HESSIDは、複数ある基地局を仮想的に同一の基地局とみなすために用いられます。Passpointでよく見かけます。

 

というわけで、しつこく ESSID と書き続けていると、めでたくWi-Fi老人会の仲間入りということになるでしょう。ふえぇ

 

おしまい

最近の(届かない)電子メール事情

「届かねーだろ、Gmailなんか使うなよ!」

 「知らん。DKIM署名付けろ!」

SPFがちゃんとあるぞ!」

 「だから、署名付けろって!」

「DMARCにも対応してる!」

 「署名がなきゃ他にも届かねぇっつの!」

Yahoo!もけしからん!」

 「テメーのサーバ、早く潰れろ!」

 

なぜか、SFPとDMARCだけ設定して、頑なにDKIM非対応なサーバが、結構ありますね。送信ドメイン認証に対応するようにと、もうかれこれ10年ぐらい言われていたと思いますが……

 

おそまつ

 

設定の参考になった記事 ↓ 

qiita.com

Edgecore EAP105のWireGuard VPN機能を試す

EdgecoreのWi-Fi 7対応機、EAP105に、待望のWireGuard VPN機能が付きました。3年ほど前から、無線LANアクセスポイントにはオープンで高速なVPN機能が欲しいよと、お願いしていたものです。 

なんで「オープンで高速」なのかというと、何度か書いてきたように、ケーブルの部分で通信を保護したいことと、大して速くもないのに割高になるのが不可避な専用コントローラ (WLC)ではなくLinux箱などで受けたいことが理由です。EAP101の記事から抜粋します。

hgot07.hatenablog.com

顧客 (オーナー) の店舗に転がすようなマネージドWi-Fiの用途では、オーナーに自前の回線への接続を任せることがあります。このような設置方法の場合、通信事業者の立場では、フリーWi-Fiトラフィックをオーナーや他の来客のいたずらから保護するために、ケーブルの部分をVPNで保護するなどの対策が欲しいものです。

 

なお、この記事ではWireGuardのみ扱い、無線LAN部分については一切触れません。

 

EAP105とは

とりあえず、バァーーン!

Edgecore EAP105

Edgecore EAP105

裏面はこう。全面がヒートシンクになっている感じです。

Edgecore EAP105 (裏面)

Edgecore EAP105 (裏面)

電源供給が従来の丸型DCジャックではなくPD3.0になっているところが目新しいです。もちろんPoEにも対応しています。

製品情報はこちら。なかなか魅力的なスペックです。

wifi.edge-core.com

 

プロセッサはQualcomm Dragonwing N7 Platform IPQ5332のようです。以前Immersive Home 3210 Platformと呼ばれていたものが、改名されたとのこと。

www.qualcomm.com

OSはいつものOpenWrtベースですが、例によってopkgによるパッケージマネージャは塞がれています。

 

EAP105のWireGuard VPN

Firmware v12.6.7から対応しています。

今のところ、ecCLOUDには設定項目がなく、スタンドアロンで使うことになります。ecCLOUDに登録した状態でクラウド側の設定がプッシュされると、WireGuardの設定が無効になってしまいます

設定画面はあっさりとしていて、こんな感じです。

WireGuardの設定画面

WireGuardの設定画面

ただし、ここだけではないので要注意です。設定項目が分散して分かりにくいのは、OpenWrtの悪いところですね。ファイアウォールの設定をしないと、パケットが流れません。

ファイアウォールの設定

ファイアウォールの設定

ip ruleで見ると、WireGuard VPNトラフィックを分離するようなルールが入っていました。

 

本体からWireGuardサーバに到達できない?

WireGuardの設定はサクサクと進んで、wg show で見るとリンクアップしているのですが、コマンドラインからサーバ側のIPアドレスを叩くとなぜか

ping: sendto: Operation not permitted

になってしまいました。これでは、RADIUSパケットを通したり、リモートから管理画面に入ったりという、本当に使いたかった機能がダメです。ip ruleが設定されているので、素直には通してくれないようです。

散々悩みました_(゚。3」∠)_

/etc/config/network を見ると、まずは一つ落とし穴が。route_allowed_ipsを '1' に設定しないといけません。(なんで '0' のままだったの?)

/etc/config/networkの設定

/etc/config/networkの設定

次に、/etc/config/firewall にも落とし穴が!変更前の値はこのとおりで、input/output/forwardが全部DROPになっていました。

/etc/config/firewallの設定

/etc/config/firewallの設定

ACCEPTに変更して、やっと開通ヽ(・∀・)ノ

これでVPNサーバにRADIUSの通信を集約したり、サーバを踏み台にして基地局の管理画面にアクセスできるようになります。

 

WireGuard VPNの性能

最近のCPUで、そこそこ上位の機種なので、500Mbpsぐらい出るものと期待していました。初めに、下りが300~400Mbpsぐらい出る自宅の光回線で試してみたところ、ありゃ?50Mbpsしか出ない……???どうも上りが遅いのに引っ張られているようです。あと、盆休みで回線が混雑していた兆候もあったので、とりあえず保留。

仕事場のLAN (GbE)で試してみたところ、予想通りの性能が得られました。

Speedtest結果

Speedtest結果

Speedtestで上下530Mbps出ています。

次に、VPNサーバのIPアドレスをめがけて iperf3 を試してみると、こんな感じ。

iperf3の結果

iperf3の結果

正直なところは、700Mbpsぐらい出るCPUだとよかったのですが、フリーWi-Fiの用途では十分かもしれません。Wi-Fi 7の速度はうたえないことになりますが、速度が欲しい場所は、いたずらされないようにガッチリとケーブルを固定して、VPN無しで設置しましょう。

 

他社も追従して、オープンなVPN機能を搭載してくれたらいいのになぁ

あと、Edgecore / IgniteNet さんは、EAP105以外のモデルにもVPN機能を追加して、ecCLOUDやecOPENから設定ができるようにしてほしいところです。VPN箱は自前で高速なものを用意するから。

欲を言えば L2TP/IPsecOpenVPN のソフトウェアも入れておいてほしい。

 

おしまい

無線LANローミングから見たNIST SP 800-63-4

先日、待望の Digital Identity Guidelines Revision 4 がリリースされました。

www.nist.gov

これが無線LANローミングにどう関係するのか、なんとなく気付いたら、認証連携をやり込んだ人ではないでしょうか。

 

無線LANローミングの本体は認証連携

無線LANローミングの中心は、認証連携と信頼関係 (トラスト) です。無線通信ではありません。

ローミング環境では、利用者にネットワークサービスを提供する組織はアクセスネットワークプロバイダ (ANP) と呼ばれ、利用者のアイデンティティを保持して認証処理を行う組織はアイデンティティプロバイダ (IdP) と呼ばれます。ANPはサービスプロバイダ (SP) と呼ばれることもあります (eduroamなど)。

ANPは、IdPが利用者をきちんと把握しているという前提の下に、ネットワークサービスを提供するという枠組みになっています。すなわち、ANP-IdP間に信頼関係が必要です。

例えば、利用者が何らかの不正利用を行った場合、外部から見えるのはANPのネットワークです。外部の者には、ANPの組織に不正利用者がいるようにしか見えません。しかし、ローミング環境にはプライバシー保護の仕組みがあり、利用者が実際に誰なのかをANPが把握できないのが通例です。苦情が届いた場合、ANPは自己の責任を回避したいので、当該利用者のIdPに捜査の目を向けさせなければなりません。

IdPは、どの人物に無線LANのアカウントを発行して、利用を許可したのかを、ある程度把握しておく必要があるでしょう。利用者が匿名ならば、不正利用の責任の一部をANPやIdPが負う恐れが出てきます。

もしIdPが無責任ならば、ANPはそのIdPの利用者に対して、サービスを提供したくないでしょう。そこで、ANPはIdPに対して、一定の Identity Assurance Level (IAL) というものを要求することになります。

 

NIST SP 800-63-3のIAL定義

Revision 3 では、IAL1, IAL2, IAL3と、3段階のIALが定義されていました。このうち、無線LANローミング基盤では IAL1, IAL2 が重要です。

csrc.nist.gov

Revision 3におけるIALの定義は、このようなものでした。

IAL1: There is no requirement to link the applicant to a specific real-life identity. Any attributes provided in conjunction with the authentication process are self-asserted or should be treated as such (including attributes a Credential Service Provider, or CSP, asserts to an RP).

IAL2: Evidence supports the real-world existence of the claimed identity and verifies that the applicant is appropriately associated with this real-world identity. IAL2 introduces the need for either remote or physically-present identity proofing. Attributes can be asserted by CSPs to RPs in support of pseudonymous identity with verified attributes.

IAL3: Physical presence is required for identity proofing. Identifying attributes must be verified by an authorized and trained representative of the CSP. As with IAL2, attributes can be asserted by CSPs to RPs in support of pseudonymous identity with verified attributes.

IAL2でも、利用者の実在性を確認する必要があり、様々なオンラインサービスでもなかなか達成が難しいものです。フリーWi-Fiの用途ではなおさらです。

IAL1は、要するに自己申告でしかなく、匿名利用と変わりないことから、フリーWi-Fiの登録に使うには物足りないということになります。不正利用者に辿り着くヒントがないのですから。従って、Revision 3ではザルのIAL1を要求しても意味がなく、OpenRoamingによるフリーWi-Fiでも、IAL2を目指すしかないという、厳しい状況でした。

 

NIST SP 800-63-4のIAL定義

無線LANローミングでも、責任の所在を明らかにするという観点で、利用者の紐づけを確実にするIAL2を目指したいという点は従来と変わりありません

しかしながら、フリーWi-Fiのサインアップを利用者に行わせる場合に、本人確認はなかなか難しいという現実があります。例えば、クレジットカードの情報まで確認できるならば、利用者の紐づけは間接的に実現できるでしょう。フリーWi-Fiを使うのにクレジットカードの登録まで要求したら、利用者は大いに躊躇することでしょう。

結局、現在のフリーWi-Fiでは、電話番号やメールアドレス、SNSアカウントを確認することで、お茶を濁しているという状況です。利用者の紐づけとしては不十分ながらも、多くの場合は利用者に辿り着く有力なヒントが得られるため、現在の落し所はこのレベルということになります。

さて、Revision 3のIAL1は先のとおりザルなので、ANPがIdPにIAL1を要求したところで無意味でしょう。このため、IAL2に近づけるように努力せよとしか言えなかったわけです。

Revision 4でIALの定義が変更され、多くのサービスが期待するものに近づきました。

IAL1: IAL1 supports the real-world existence of the claimed identity and provides some assurance that the applicant is associated with that identity. Core attributes are obtained from identity evidence or self-asserted by the applicant. All core attributes are validated against authoritative or credible sources, and steps are taken to link the attributes to the person undergoing the identity proofing process.

IAL2: IAL2 requires collecting additional evidence and a more rigorous process for validating the evidence and verifying the identity.

IAL3: IAL3 adds the requirement for a trained CSP representative (i.e., proofing agent) to interact directly with the applicant, as part of an on-site attended identity proofing session, and the collection of at least one biometric.

IAL1の定義を見ると、ある程度確実な情報とリンクすることが求められています。「ある程度」がどれぐらいのものか、微妙なところはありますが、従来のIAL2より達成しやすく、匿名同様だったIAL1と比べたら制約が明確になってきたと言えます。

無線LANローミングの世界で言うと、以前はIAL1を要求する意味がなかったところ、これからはIAL1を一つの基準として要求できるという、大きな進歩になりそうです。

 

電話番号やSNSアカウントで十分なのか?

ごもっともな疑問です。

実際、どの程度の紐づけができれば十分なのか、明確な基準がありません。色々な組織で話し合って、今のバランスに落ち着いているというのが現状でしょう。

Gmailのようなフリーメールなら即ダメかというとそうでもなくて、電話番号やクレジットカード番号が登録されているなら、そのアカウントはある程度信頼できるものでしょう。IdPが捜査に協力するかどうかは、また別の問題です。この辺もまだ無線LAN業界で議論の真っ最中です。

国によって、温度差があるのも事実です。国によっては、空港でパスポートを見せないとフリーWi-Fiのアカウントがもらえなかったりします。一方、そのような国でも、空港内のカフェでさえパスワードが貼り出されているなど、運用がかなり緩いことがあります。十数年前のドイツでは、ホテルで個別ID/PWが印刷された紙切れを渡されることがあったのですが、フロントのカゴから適当に拾えたりしました。カゴじゃなくてザルだったかも。

 

以上、発散してきましたが、少し進展があったという話でした。

おしまい

Apache 2.4 + mod_auth_openidc でJWT認証を実現してみる

APIの認証ではよくJSON Web Token (JWT)が使われている……」なんてあちこちに書かれているので、そんなに面倒なことはないだろうと思っていたら、結構な いばらの道 でした (2025年7月時点の話)。JWTの構造自体は単純で、そこそこ年数も経っているのに。

AlmaLinux 9のパッケージだけで、なんとか認証までたどり着くことができたので、自分用のメモとして残します。色々とウソもつかれたけど、調査を助けてくれたCopilotくんに感謝!

やりたかったこと:

  • 異なる組織、異なる計算機にまたがってアプリを連携させたいので、APIのための安全で軽量な認証がほしい。(Basic認証IPアドレス制限では怖い)
  • できればそこそこ枯れた新しい技術を使ってみたい。
  • 共通鍵ではなく、公開鍵暗号を使いたい。
  • いつも使っているのがApache HTTP Serverなので、これを使いたい。
  • 自前でビルドするのは避けたい。

つまずいたのは、ざっとこんなところです ↓

  • Apache用のJWT認証というと、まっさきに mod_auth_jwt が出てくる (Copilotくんも)。しかし、肝心のリポジトリが見当たらない。似たようなものがGitHubに幾つかあるものの、パッケージで提供されているようなものではない。
  • Apache 2.5には mod_autht_jwt というモジュールがあり、これがApacheの本命に見える。しかし、2.5はまだ出ていない。
  • とりあえずトークンを自前で作ってみようとしたら、署名がうまく行かない。

今回、本当に綱渡りで動かしたので、バージョンが違うと動かないことがあるかもしれません。それはそれで、ソフトウェアの品質としてどうなのという思うところはあります。Apacheには早く mod_autht_jwt を提供していただきたいところ。

以下、ノークレームで!

 

Apache 2.4でJWT認証できる簡単な方法を探す

まず、Copilotくんに聞いてみたら、「mod_auth_jwt というのがあるよ」とのこと。使い方も簡単に見えたので、いざAlmalinuxでdnf searchしてみたら、ない、ないよ……

Copilotくんが指し示したリポジトリは 404 でした。

他に使えそうなものを探しまくっていたら、Apache 2.5にはmod_autht_jwtがあるとのこと。ところが、2.5なんて出ていないんですよねー。

さらにCopilotくんと頑張って探し続けていたところ、ポロリと有用な回答が。

「それ、mod_auth_openidc でもできるよ!」

OIDC特有のリダイレクト機能などを使わず、JWTの認証部分だけを使えばよいとのこと

 

mod_auth_openidc の設定例では動かなかった

ドキドキしながら、AlmaLinux 9でdnf searchしてみたら、ありました、mod_auth_openidc-2.4.10-1 !

Copilotくんが示した Complete working example がこちら ↓ (httpdの設定ファイルの中に書くもの)

OIDCValidateJWT On
OIDCJWTIssuer https://your-issuer/
OIDCJWTVerifyJwksUri https://your-issuer/.well-known/jwks.json
OIDCJWTRequiredClaim aud your-audience

<Location /api>
    AuthType openid-connect
    Require valid-user
</Location>

さっそくhttpdを再起動してみたら……、立ち上がりません _('、3」∠)_

そもそも Syntax error なので、重症です。

 

色々と戦った後に得られたのがこちら ↓ 

OIDCOAuthVerifyJwksUri https://your-issuer.example.com/.well-known/jwks.json
OIDCOAuthRemoteUserClaim sub
OIDCOAuthVerifyClaims "iss=your-issuer" "aud=your-audience"

 

<Location /api>
    AuthType oauth20
    Require valid-user
</Location>

まだ、立ち上がりません _('、3」∠)_

OIDCOAuthVerifyClaimsがおかしいので、削除してみたところ、httpd が動き始めました。

(認可周りの調査は今後の課題)

 

ES256にチャレンジしてみる

結論から述べると、ES256で特に面倒が増えるという感じはなかったです。強いて言えば、情報が少ないぐらい。

さくっと鍵を作ってしまいます。

$ openssl ecparam -name prime256v1 -genkey -noout -out privkey.pem
$ openssl ec -in privkey.pem -pubout -out pubkey.pem

JWTは、<ヘッダー>.<ペイロード>.<署名> というシンプルな構造をしているので、とりあえず簡単なヘッダーとペイロードを作って、BASE64URLエンコードして、ピリオドで連結しておきます。

$ echo -n "{\"alg\":\"ES256\", ...}" | basenc --base64url -w0 > tmp.txt
$ echo -n "." >> tmp.txt
$ echo -n "{ペイロード...}" | basenc --base64url -w0 >> tmp.txt

改行コードが入らないように細心の注意を払います。不安だったら hexdump -Cv で確認。

 

署名の罠

「あとは署名だから openssl dgst -sha256 -sign privkey.pem tmp.txt で簡単やろ」と思ったら、落とし穴がわんさか……

opensslで作った署名をBASE64URLエンコードしてくっつけても、JWTデコーダー に突っ込んでみると Invalid Signature になってしまいます。

ES256 (ECDSA P-256)

    The signature is a concatenation of two 32-byte numbers: R and S.
    Many libraries and tools (like OpenSSL) output ECDSA signatures in ASN.1 DER format, but JWT expects the raw format:
        Raw format: [R (32 bytes)] || [S (32 bytes)] (total 64 bytes)
        Then base64url-encoded without padding.

ぁー、形式が違うんですね。

Copilotくんに言われるまま、

$ openssl asn1parse -in signature.der
Error: offset out of range
$

ありゃ?

-inform der が要ります。Copilotくんの嘘つき!(

いや、この辺の処理が面倒くさいので、perlスクリプトを書いてしまった方が早いです。use Crypt::JWT qw(encode_jwt); して、秘密鍵を読み込ませて、以下の感じで行けます (なげやり)。keyを参照で渡さないといけないところに気付くまでさらにウン十分。

my $j = encode_jwt(
        payload => $payload,
        alg => 'ES256',
        key => \$privkey,
        extra_headers => {typ=>'JWT',kid=>'012389'},
);
print $j; 

やっとこさ、JWTデコーダー で Signature Verified になりました。

 

ウェブサーバに公開鍵を仕込む

jwks.json というファイルに、こんな風に書きます。

{
  "keys": [
    {
      "kty": "EC",
      "kid": "012389",
      "use": "sig",
      "crv": "P-256",
      "alg": "ES256",
      "x": "...",
      "y": "..."
    }
  ]
}

はて、xとyは?

以下のようにすると、公開鍵の中身が表示されます。

$ openssl ec -in pubkey.pem -pubin -text -noout

pub: の項目で、最初の 04 を除くと、残りが32バイトのx座標値と、32バイトのy座標値です (P-256/ES256なので)。コロンを削除して、16進数表記をBASE64URLに変換して、パディングを削除して……って、やってられんわー・・・・・(ノ`Д´)ノ彡┻━┻

(自動化すべき)

[2025/7/31追記] 以下のperlスクリプトで x, y 座標値が表示できます。

#!/usr/bin/perl

use Crypt::PK::ECC;
use MIME::Base64;
use JSON::PP;

# Read public key from PEM
my $pem = do { local $/; open my $fh, '<', 'pubkey.pem' or die $!; <$fh> };
my $pk = Crypt::PK::ECC->new(\$pem);

my $pub = $pk->export_key_jwk('public');

my $json=JSON::PP->new->pretty->canonical;

print $json->encode( $json->decode($pub) );

 

結果

できました!ヽ(・∀・)ノ

Hello, JWT!

こんな感じで動作確認できます ↓

$ curl -H GET 'https://api.example.com/api/index.txt' -H 'Content-Type: application/json;charset=utf-8' -H 'Authorization: Bearer <作成したトークン>'

念のため、JWTを送らなかったり、わざと別のJWTを送ってみたりして、アクセスが拒否されることを確認します (重要)。

ペイロードに exp を埋め込んでおくと、その時刻以降は"正しく"認証が通らなくなります (確認済み)。

 

おしまい