Docker で検証用 CAS サーバ

CAS との連携を確認するのに、Docker でサクッと建てようと思ったら思いのほか手を焼いたのでメモ。

やりたいこと

CAS でSSOするサービス側モジュールの検査のために、とりあえずのCASサーバを立てて使いたい。細かい設定はいらない。

昔は仮想マシンに Tomcat 入れて動かしていましたが、今ならもう Docker でサクッと立つはず。。。

コンテナイメージの配布状況

本家のドキュメント、この素っ気なさからしていやな予感。

本家のコンテナイメージと、ビルドソース

いろいろ試した結果、配布されているコンテナイメージをそのまま何とか動かしても思ったように扱えない。

このコンテナイメージは SRC1 をビルドしたもので、SRC1 は gradle 実行環境と SRC2 をコンテナイメージの中に展開して gradle でビルドをしている。ただし、SRC1 は SRC2 のビルドオプションを最小限しか設定しておらず、実用性に難がある。

SRC2 が本命だが、SRC2 には gradle のビルド環境構築手続きが含まれていない。それを書くと結局は SRC1 を引っ張り出すことになるので、SRC1 を調整してカスタムイメージをビルドすることにする。

念のために Vagrant も探してみたけどいまいちでしたので、やっぱりコンテナのビルド。

予備知識、前提

はまりポイント、特に Java のビルド環境についてあまり経験の無い方は、ビルドオプションを調整するところかと思います(私がそうでした)。

ビルド環境の用意

まずは github.com/apereo/cas-webapp-docker をクローンしてビルドの準備をします。

git clone https://github.com/apereo/cas-webapp-docker.git
cd cas-webapp-docker
# git reset --hard a2d664fd006d86f0bb09d65c2ffb0eeb25018fdf

README.md の SSL セクションを実行します。ここはローカル開発用と割り切って、鍵もPASSもそのままです。

keytool -genkeypair -alias cas -keyalg RSA -keypass changeit \
        -storepass changeit -keystore ./thekeystore \
        -dname "CN=cas.example.org,OU=Example,OU=Org,C=AU" \
        -ext SAN="dns:example.org,dns:localhost,ip:127.0.0.1"

sudo su root -c " echo '127.0.0.1 cas.example.org' >> /etc/hosts"

実際に使うにはまだ設定が必要ですが、念のために一度ビルドして動かしてみます。

./build.sh 6.4
./run.sh 6.4

うまくいけば、コンソール出力に大きく「READY」と表示されます。(ここのコードブロックでの表示では形が崩れているかもしれません)。

2021-12-21 05:02:57,689 INFO [org.apereo.cas.web.CasWebApplication] - <>
2021-12-21 05:02:57,689 INFO [org.apereo.cas.web.CasWebApplication] - <

  ____  _____    _    ______   __
 |  _ \| ____|  / \  |  _ \ \ / /
 | |_) |  _|   / _ \ | | | \ V / 
 |  _ <| |___ / ___ \| |_| || |  
 |_| \_\_____/_/   \_\____/ |_|  

>
2021-12-21 05:02:57,689 INFO [org.apereo.cas.web.CasWebApplication] - <>
2021-12-21 05:20:05,860 INFO [org.apereo.cas.web.CasWebApplication] - <Ready to process requests @ [2021-12-21T05:20:05.859Z]>
2021-12-21 05:20:05,916 INFO [org.apereo.cas.services.AbstractServicesManager] - <Loaded [0] service(s) from [InMemoryServiceRegistry].>

特に、コードブロックにコピーしたログの一番最後の行、 [org.apereo.cas.services.AbstractServicesManager] - <Loaded [0] service(s) from [InMemoryServiceRegistry].> というのがポイントです。これは、Application ServiceRegistry に登録がないよ、と言っています。後で改善します。

この状態でブラウザで https://cas.example.org:8443/cas/login を開くとログイン画面を表示できるはずですが、実際には証明書が INVALID なために警告が表示されてしまいます。以下のコマンドで証明書を吐き出して許可リストに加えるか、ブラウザで開いたときに強制的に表示させます。

keytool -export -alias cas -keystore thekeystore -rfc -file cas-self.cert

ログイン画面が表示されると、casuser アカウントで認証は成功するはずです。

アプリケーションサーバの連携確認

アプリケーションサーバにこのCASサーバを登録するには、以下を確認してください。

  • ブラウザは、CASサーバとアプリケーションサーバのどちらにもアクセスできること
  • アプリケーションサーバがこのCASサーバに 8443:tcp でアクセスできること
  • アプリケーションサーバの hosts ファイルにも cas.example.org を足しておくこと
  • アプリケーションサーバの CAS 連携モジュールは証明書チェックを無視する設定になっていること

特に、PHPのCURLについては以下の設定を使います。

curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);

アプリケーションサーバでCAS認証しようとすると、CASサーバにリダイレクトされますが、以下の表示になると思います。

また、CASサーバのログでは試した直後には以下のログが出ているはずです。

2021-12-21 04:13:02,682 ERROR [org.apereo.cas.services.web.support.RegisteredServiceResponseHeadersEnforcementFilter] - <Service unauthorized>

つまり、許可サービスリストの登録が0で、いま試したアプリケーションサーバもはじかれている状態になります。

Docker のカスタマイズと Application ServiceRegistry の設定

ここからは、チェックアウトしているDocker のビルドキットをカスタマイズして、Application ServiceRegistry を設定し直したコンテナイメージを作り直していきます。

まずは2つファイルを新規追加します。1つは cas サーバの起動設定ファイルで、JSONリポジトリを有効に指定します。1つはテスト用アプリケーションサーバの定義ファイルで、http://192.168.56.??/ で動作することを前提としています。

etc/cas/config/cas.properties

cas.server.name=https://cas.example.org:8443
cas.server.prefix=${cas.server.name}/cas

logging.config=file:/etc/cas/config/log4j2.xml

# for json services ( need build option
cas.service-registry.core.init-from-json=true
cas.service-registry.json.watcher-enabled=true
cas.service-registry.json.location=file:/etc/cas/services
#cas.service-registry.yaml.location=file:/etc/cas/services

# cas.authn.accept.enabled=false

etc/cas/services/test-2.json

{
     "@class" : "org.apereo.cas.services.RegexRegisteredService",
            "id":2,
            "serviceId":"http://192.168.*",
            "name":"DEV",
            "description":"dev",
            "allowedToProxy":true,
            "enabled":true,
            "ssoEnabled":true,
            "anonymousAccess":false,
            "allowedAttributes":["uid", "mail"]
            "extraAttributes":{
                "someCustomAttribute":"Custom attribute value"
            },
            "evaluationOrder":2
}

続いて、gradle のビルド定義ファイルを置き換えます。ちょっと面倒なのですが、以下の build.grade ファイルを取得して、手元のファイルを置き換えてしまいます。

その上で、L930 くらいにある dependencies のセクションに ServiceRegistry の JOSN ストレージモジュールを足します。${project.'cas.version'} の変数も、このまま書きます。

dependencies {
    /**
     * CAS dependencies and modules may be listed here.
     *
     * ...
     */
     ...

    /* add */
    implementation "org.apereo.cas:cas-server-support-json-service-registry:${project.'cas.version'}"
}

続いて、Dockerfile を編集します。2カ所はファイルコピー操作を追加、gradle のビルドコマンドは、./gradlew dependencies コマンドを追加しています。2ステップに分解していますが、まとめてもかまいません。

FROM centos:centos7

MAINTAINER Apereo Foundation

ENV PATH=$PATH:$JRE_HOME/bin
ARG cas_version

RUN yum -y install wget tar unzip git \
    && yum -y clean all

# Download Azul Java, verify the hash, and install \
RUN set -x; \
    java_version=11.0.3; \
    zulu_version=11.31.11-ca; \
    java_hash=20218b15ae5ef1318aed1a3d5dde3219; \
    cd / \
    && wget http://cdn.azul.com/zulu/bin/zulu$zulu_version-jdk$java_version-linux_x64.tar.gz \
    && echo "$java_hash  zulu$zulu_version-jdk$java_version-linux_x64.tar.gz" | md5sum -c - \
    && tar -zxvf zulu$zulu_version-jdk$java_version-linux_x64.tar.gz -C /opt \
    && rm zulu$zulu_version-jdk$java_version-linux_x64.tar.gz \
    && ln -s /opt/zulu$zulu_version-jdk$java_version-linux_x64/ /opt/java-home;

# Download the CAS overlay project \
RUN cd / \
    && git clone --depth 1 --single-branch -b $cas_version https://github.com/apereo/cas-overlay-template.git cas-overlay \
    && mkdir -p /etc/cas \
    && mkdir -p cas-overlay/bin;

COPY thekeystore /etc/cas/
COPY bin/*.* cas-overlay/
COPY etc/cas/config/*.* /cas-overlay/etc/cas/config/
COPY etc/cas/services/*.* /cas-overlay/etc/cas/services/
# 追記
COPY build.gradle /cas-overlay/build.gradle

RUN chmod 750 cas-overlay/gradlew \
    && chmod 750 cas-overlay/*.sh \
    && chmod 750 /opt/java-home/bin/java;

EXPOSE 8080 8443

WORKDIR /cas-overlay

ENV JAVA_HOME /opt/java-home
ENV PATH $PATH:$JAVA_HOME/bin:.

# 書き換え
RUN mkdir -p ~/.gradle  && echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties
RUN ./gradlew dependencies && ./gradlew clean build --parallel \
    && rm -rf /root/.gradle

# 追記
RUN mkdir -p /etc/cas/config && mkdir -p /etc/cas/services && cp etc/cas/config/* /etc/cas/config/ && cp etc/cas/services/* /etc/cas/services/

CMD ["/cas-overlay/run-cas.sh"]

あらためてビルドと実行を行います。

./build.sh 6.4
./run.sh 6.4

もし JSON のサービス定義ファイルをロードできていれば、「READY」の後に以下のログが出ます。 [org.apereo.cas.services.AbstractServicesManager] - <Loaded [1] service(s) from [JsonServiceRegistry].> となっていれば、JSON のアプリケーション定義をロードするのに成功しています。

2021-12-21 09:07:28,348 INFO [org.apereo.cas.web.CasWebApplication] - <Ready to process requests @ [2021-12-21T09:07:28.342Z]>
2021-12-21 09:07:28,780 INFO [org.apereo.cas.services.AbstractServicesManager] - <Loaded [1] service(s) from [JsonServiceRegistry].>

アプリケーション定義ファイルのURL正規表現と、アプリケーションのURLがマッチしていれば、SSO認証したときに無事に CAS のログイン画面が表示されるはずです。パスワード認証に成功すると、CASサーバはチケットを発行してアプリケーションにリダイレクトします。ログは以下が残ります。

2021-12-21 09:39:39,055 INFO [org.apereo.cas.DefaultCentralAuthenticationService] - <Granted service ticket [ST-11-Li75-erdbw7sPZ5vdnW5QG1IcAY-66a57e874b0d] for service [http://192.168.56.76/login.php?auth_mode=CAS] and principal [casuser]>
2021-12-21 09:39:39,055 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: casuser
WHAT: {service=http://192.168.56.76/login.php?auth_mode=CAS, return=ST-11-Li75-erdbw7sPZ5vdnW5QG1IcAY-66a57e874b0d}
ACTION: SERVICE_TICKET_CREATED
APPLICATION: CAS
WHEN: Tue Dec 21 09:39:39 UTC 2021
CLIENT IP ADDRESS: 172.17.0.1
SERVER IP ADDRESS: 172.17.0.2
=============================================================

アプリケーションサーバがリダイレクトURLより認証チケットを受け取ると、アプリケーションサーバからCASサーバにチケットのValidationの問い合わせを行います。Validation に成功すると、CASサーバには以下のログが残り、アプリケーションサーバではユーザ名など属性情報を受け取ります。

2021-12-21 09:39:39,159 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: casuser
WHAT: {ticket=ST-11-Li75-erdbw7sPZ5vdnW5QG1IcAY-66a57e874b0d, service=http://192.168.56.76/login.php?auth_mode=CAS}
ACTION: SERVICE_TICKET_VALIDATE_SUCCESS
APPLICATION: CAS
WHEN: Tue Dec 21 09:39:39 UTC 2021
CLIENT IP ADDRESS: 172.17.0.1
SERVER IP ADDRESS: 172.17.0.2
=============================================================

今後

Java のビルドツールはあまり使ったことがないので、gradle の設定ファイルの記入位置を見つけるのに思いのほか時間がかかりました。

ですが、これですぐに検証サーバが用意できます。CASサーバのスタブを書くまでの間、クライアントのライブラリ更新やリファクタリングなどで頼もしく動いてくれるのを期待します。

docker コンテナを使ってPHP のバージョンへの対応チェック

こんにちは。福岡です

ご無沙汰になってしまいましたが、最近に調整していた開発環境の話を紹介します。いろんなPHPのバージョンで動くか検査する方法です。

PHPバージョンの対応状況

WebClass は Debian と RHEL をサポートしています。具体的には、両者について OS の側でサポートされている期間の間、WebClass も動くように維持するというポリシーです。したがって、サポートする PHP バージョンもこれらOSが現在サポートしている範囲になります。

正直なところ、特に RHEL はメジャーバージョン単位のサポート期間が長くてしんどいです。RHEL 6 の PHP 5.3 はようやく去年にサポート・メンテナンス2が終了しました( REF:Red Hat Enterprise Linux 6 (RHEL6) のサポート終了と移行について)。これで 5.3 が消えると思いきや、RHEL 7 は PHP 5.4 !! 。PHP 5.4 の最初のリリースは 2012年 3月でして、これももう10年になろうとしています(REF: Version 5.4 changelog)。なお、Debian 11 が先週にリリース(REF)されたため、これから PHP 7.4 の対応等を進めていくことになります。

どうやって PHP 5.3 - PHP 7.4 で動くように確認するのか? これが問題です。

昔は、それこそ Debian 8 と Debian 9 と CentOS6 と、という感じで複数のサーバを構築していました。ですが、機能も増えてきている今の WebClass を1つ1つ手でチェックしていくというのは無理です。ベテランプログラマーはバージョン依存が出にくい PHP のコーディングスタイルを身につけていますが、新しく加わった人がそれを身につけるものしんどいし、レビューでもチェックしきれません。実際のところ、バージョン依存の問題をポロポロと出してしまっています。

そこで、検査と開発者へのフィードバックの機械化を進めています。

PHP Unit による自動テスト

WebClass ではアジャイルな開発スタイルを採用しています。開発作業の流れとしては、社内の GitLab を使って Issue => Merge Request => Merge を繰り返しています。そして、このイテレーションでは GitLab の継続的インテグレーション機能を使って、テストとパッケージングを機械化し、素早く完全な変更を繰り返していくことを意識しています。自動テストの一部は PHP Unit を使っており、継続的インテグレーション機能(Pipelie)は次のような感じです。

GitLab のPipeline

PHP Unit の実行環境として、それこそ少し前までは Debian のサーバを構築していました。その後にテスト用のサーバの維持などの問題で CI での自動テストの運用が止まってしまいましたが、そうするとやっぱり開発者が自分でテストをするというのは、どうしてもおろそかになります。そこで、CI-Runner が使う Php Unit 実行用の docker イメージを作り、メンテナンスが後手に回ったテストを直し、今では Merge Request を発行するたびに自動的に Php Unit の結果がフィードバックされるようになっています。

GitLab の MergeRequest で Pipeline の結果部分

PHP のバージョンごとにコンテナイメージを追加することで、複数のPHPバージョンで並行して自動テストを行うことができます。今は PHP5.6 と PHP 7.3 の環境で自動テストが走っています。

一方、Php Unit さえあればいいかと言うとそうでもありません。自動テストが実装されていないところはテストされないのが何より問題です。WebClass は2000年から開発スタートして、今まで完全な作り直しはありません。ですので、フレームワークのない状態から今まで拡張の傍らで必死にリファクタリングを進めて構造化し、後から自動テストスイートを加えて、そこからテスト可能な実装にリファクタリングしてテスト実装して、と走ってきました。まだテスタブルでないコードも、自動テストが実装されていないコードも残っています。

CI に PHP Linter 追加

Php Unit は自動テストが実装されないとコードを検査できませんが、PHP のコードの文法レベルであればすぐにスキャンできます。文法レベルの問題としては、例えば以下があります。

// PHP 5.4 から使える。それ以前のバージョンでは文法エラー
$array = [ 1, 2, 3];
// この書き方はどのバージョンでも使える
$array = array( 1, 2, 3 );

この程度と思いきや、文法エラーは PHP をロードした時点でエラーになって落ちるので、初期の require() などで1つでも混入すると機能が全く動かなくなります。ですので、検査する価値はあります。

PhpLinter のライブラリもいくつか公開されていますが、中を除くと基本は PHP に実装されている Lint 機能を使っているようです。この機能はコマンドラインで php -l test.php と実行して検査できます。ということは、 PhpUnit を動かせるコンテナであれば、composer とか phing を使うまでもなく以下のコマンドでチェックできます。

find $SRCDIR  \( -type f -name "*.php" -o  -name "*.inc" \)  -exec php -l {} \; | grep -v 'No syntax errors detected' 

ということで、準備しました!

PHP 7.4 で早速 PhpUnit が落ちています。また、UnitTest のJob に 7.0 などがいないのは、まだ調整中のためテストJob を入れたり外したりしているからです。

自動テストに失敗していれば、Merge Request 画面でもその結果が表示されて、マージできなくなります。

PhpUnit 失敗

テストターゲットのチューニング

Lint を実行するコマンドは、実は PHP のバージョンごとに用意してあります。

これは、大学に提供しているカスタム機能などがその大学での実行環境に依存していることが有るためです。このように、もともと特定の環境で動くことを想定しているコードがどうしてもあるのと、新しいバージョンに対応させていく優先順位はありますので、全てを一気にとは行きません。このタイムラグを解決するための苦肉の策です。

composer の overtrue/phplint などのツールもあり、phplint ツールのコンフィグファイルを書き分けるても有るのですが、それはやめました。理由は2つ。1つは、PHP バージョンごとに実行環境を分けているので、PHP バージョンが古いと composer の使いたいライブラリやバージョンを使えなかったりして返ってややこしい。1つは、php -l は Deprecated などの情報もバリバリ書き出してくれるので、将来のバージョン対応を見据えて Error ではない情報もチェックするときに便利なため。こういうときには良くも悪くも Bash は単純で、除外リストなど開発者がそのまま読んだり調整しやすいと思ったからです。

まとめと今後

PHP のバージョンごとのテスト用 Docker がひとまず揃いました。CI でも動かしますが、Docker なので開発者の手元でもすぐに動かせます。まずはこれで PHP の文法レベルの問題は未然に防げるようになると期待できます。

さらにテストの内容を充実させるために PHPUnit のテストはどんどん書き足していきます。そのため、Merge Request のレビューではテスト可能な設計になっているか、テストの内容は十分か、なども引き続き突っ込んでいきます。

また、レビューにおける脆弱性の検査もなかなか難しいため、GitLab の SAST 機能あたりに興味を持っています。よく見ると PHPについては PHP-CS の拡張パターンのようですが。

https://git.lan.datapacific.co.jp/help/user/application_security/sast/index

小さなチームでレガシーコードも抱えていますが、うまくやりくりして良くしていきます。

Dockerコンテナ内で make: /bin/sh: Operation not permitted

GitLab CI のジョブ実行環境として Docker イメージ composer:2 を使っていたのですが、 make: /bin/sh: Operation not permitted というエラーによりジョブの途中で停止していました。単純な make コマンドが実行できないようです。

この問題に関係するのは以下のようでした。

https://github.com/alpinelinux/docker-alpine/issues/146
https://wiki.alpinelinux.org/wiki/Release_Notes_for_Alpine_3.14.0#faccessat2

まとめると以下の状況です。

  • Alpine 3.14.0 からシステムコール faccessat2 を使うようになったため、ホストOSで以下を必要とするようになった
    • runc v1.0.0-rc93
      • containerd.io 1.4.3-2 (DebianリポジトリのDockerの場合)か、Docker Desktop 3.3.0 (WindowsかMacのDockerの場合)に含まれる
    • Docker 20.10.0 以上
    • libseccomp 2.4.4 以上

弊社では、GitLab runner が動作しているサーバーの Docker バージョンを最新にしたらエラーが起きなくなりました。