0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

mod_auth_openidcを使ってメンバーだけ閲覧可能な静的コンテンツをk8sにデプロイしてみた

Posted at

はじめに

組織のID管理はLDAPで行われているので、参照権限を貰って手元でDexをk8sで動かしてOIDCのIPを運用しています。

これまでWebの静的コンテンツについては組織内で限られたグループにアクセスを許可するため、apache2(apache httpd)とmod_auth_ldapを使って認証していました。

静的コンテンツを表示するだけであれば、他の仕組みがあるだろうという気もするのですが、いにしえのApache2とBASIC認証による制御はシンプルで分かりやすい反面、セキュリティ的には少し脆弱なのとUXもあまり良くないと思われたのでOIDC認証に切り替えることにしました。

OIDCは少し面倒な気持ちがしていたのですが、クリスマスも近いので時間を使って挑戦してみることにしました。

最終的にはalpine linuxのコンテナで稼動させますが、とりあえずubuntu 24.04を利用して慣れてからalpine linuxに載せ換えていきます。

参考資料

mod_auth_openidcで検索した次の記事を参考にしました。

また今回テストしたDexはPostgreSQLをstorageに指定し、API経由で動的に構成を変更できるようにしたものをテストを兼ねて利用しています。こちらの情報は別の記事にまとめています。

mod_auth_openidc用のRequire行の書式については次の公式ドキュメントが参考になりました。

特にClaimsのJSONオブジェクトに配列が含まれている場合にはセパレーターが":"になる点だけ注意すれば、ほぼほぼClaims JSONの構造が分かっていれば柔軟な指定ができると思います。

準備作業

VMware上のUbuntu 24.04 Serverを使ってテスト環境を構築しました。

まず必要なパッケージを入れます。

$ sudo apt update
$ sudo apt install apache2 libapache2-mod-auth-openidc

ファイルが配置されているPATHを確認します。

$ dpkg -L libapache2-mod-auth-openidc
...
/etc/apache2/conf-available/auth_openidc.conf
/etc/apache2/mods-available/auth_openidc.load
...
/usr/lib/apache2/modules/mod_auth_openidc.so
...

/etc/apache2/*-enabledディレクトリに配置されているファイルを確認します。

$ ls -l /etc/apache2/*-enabled/*idc*
lrwxrwxrwx 1 root root 35 Dec 20 00:46 /etc/apache2/conf-enabled/auth_openidc.conf -> ../conf-available/auth_openidc.conf
lrwxrwxrwx 1 root root 35 Dec 20 00:46 /etc/apache2/mods-enabled/auth_openidc.load -> ../mods-available/auth_openidc.load

必要な設定は既に終っているので差分の設定だけを行っていきます。

設定ファイルの準備

/etc/apache2/conf-enabled/auth_openidc.confに接続するための情報を追加していきます。

参考資料ではEndpointの情報を個別に指定していますが、..//.well-known/openid-configuration を通して情報が得られるので出来るだけ省略していくことにします。

image.png

最終的に記述したのは次の情報です。

/etc/apache2/conf-enabled/auth_openidc.confの記述内容
OIDCRedirectURI https://httpd.example.com/redirect_uri
OIDCCryptoPassphrase "exec:/bin/bash -c \"head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32\""
OIDCProviderMetadataURL https://example.com/dex/.well-known/openid-configuration
OIDCScope "openid email groups profile"
OIDCRemoteUserClaim preferred_username
OIDCClientID dex-client
OIDCClientSecret c925b0e48ba5722521b1cbadc0cf6559
OIDCClientName LDAP-ID

Alpine Linuxに載せ換えるところでも説明していますが、実行時に動的にOIDCCryptoPassphraseを設定する方法はhttpdが1プロセスで動作する場合にのみ利用可能です。

クラスタリング構成の場合には全プロセスで同一の文字列を指定する必要がありますので注意してください。

OIDCRedirectURIに指定するURLは、このWebサーバー配下にある存在しないページを指定します。

OIDCRemoteUserClaimに指定したフィールドが、Require userで指定できます。httpdのログファイルにもREMOTE_USERとして記録されます。これを指定しない場合にはLDAPのDNがbase64でエンコードされたと思われる文字列がログに残るので障害対応なども考えて適切に変更することをお勧めします。

.htaccessファイルを利用する際の留意点

OIDC*で始まる各種設定を.htaccessファイルに含めることはできません。

例えば次のように、Auth*とRequire行から構成される.htaccessファイルを実行時に配置することになります。

/var/www/html/protected/.htaccessファイル
AuthName "LDAP-ID Authentication"
AuthType openid-connect

Require claim "groups:webadmin"
Require user "user01"

Require valid-userの代わりに、claimを利用すればIDから返却されるclaimによって柔軟な認可ポリシーが提供できます。

個人名の指定にRequire user <username>を使っていますが、これはOIDCRemoteUserClaimで指定したフィールドを利用することができます。

/var/www/html/protected/index.htmlなどを配置してから、このWebブラウザからアクセスすると自動的にOIDCの認証画面が表示され、適切な権限を持っているかチェックされ、返却されます。

.htaccessを利用する場合、ファイルを配置したディレクトリ直下のファイルをOIDCRedirectURIに指定する必要があります。

/protected/.htaccessに対応するOIDCRedirectURIの指定はhttp://localhost:8080/protected/redirect_uriなどを指定します。

Ubuntuでの経験を活かしてAlpine Linuxを使ったコンテナを動かしていきます。

Alpineコンテナの準備

元々 mod_ldap を利用していたコンテナがあるので、今回はそれを改造してmod_auth_openidcを有効にしたコンテナを準備します。

Alpine linuxはやや特殊なところがあるので、内部の状態を確認しながらDockerfileを作成していきます。

alpineを動かしながら設定ファイルの変更箇所や内容を確認しながら進める
$ podman run -it --rm alpine:latest sh
/ # apk add tzdata bash ca-certificates
/ # apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ apache-mod-auth-openidc

ここからコンテナの内部でhttpdの設定ファイルを編集していくと、いくつか方針が立っていきます。

  • "LoadModule"命令が/etc/apache2/conf.d/mod-auth-openidc.confの1行目に記述されているが、共有ライブラリへのpathにmodules/を追加しないと正しくロードできない (alpine/issue#15999)
  • とはいえhttpd:2.4-alpineコンテナは/usr/local/apache2以下に導入されているため、/etc/apache2/以下のファイルを参照しない
  • mod_auth_openidc.soは利用できるので、LoadModuleを含めて設定ファイルを配置する必要がある
  • .htaccessにOIDC*設定を含められないため、コンテナの実行時に動的に構成を変更する必要がある

これらの点に配慮しつつ、Dockerfileを作成していきます。

httpd-foreground ENTRYPOINT スクリプト

動的に設定を変更するための作業はhttpd:2.4-alpine公式イメージのCMDに指定されているhttpd-foregroundスクリプトを改造します。

httpd:2.4-alpineコンテナのために次のようなhttpd-foregroundスクリプトを準備しました。

/usr/local/bin/httpd-foreground
#!/bin/bash
set -e

# Apache gets grumpy about PID files pre-existing
rm -f /usr/local/apache2/logs/httpd.pid

CONF_FILEPATH=/usr/local/apache2/conf/openidc.conf

echo "Include ${CONF_FILEPATH}" >> /usr/local/apache2/conf/httpd.conf

cat <<EOF | tee "${CONF_FILEPATH}"
<Directory /usr/local/apache2/htdocs>
    AllowOverride All
</Directory>
LoadModule auth_openidc_module /usr/lib/apache2/mod_auth_openidc.so
OIDCRedirectURI ${OIDCRedirectURI:-https://httpd.example.com/redirect_uri}
OIDCCryptoPassphrase "${OIDCCryptoPassphrase:-exec:/bin/bash -c \\\"head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 \\\"}"
OIDCProviderMetadataURL ${OIDCProviderMetadataURL:-https://example.com/dex/.well-known/openid-configuration}
OIDCScope "${OIDCScope:-openid email groups profile}"
OIDCRemoteUserClaim preferred_username
OIDCClientID ${OIDCClientID:-dex-client}
OIDCClientSecret ${OIDCClientSecret:-c925b0e48ba5722521b1cbadc0cf6559}
OIDCClientName ${OIDCClientName:-LDAP-ID}
EOF
## for session cookies
if test -n "${OIDCCookiePath}"; then
    cat <<EOF | tee -a "${CONF_FILEPATH}" 
OIDCCookiePath ${OIDCCookiePath:-/}
EOF
fi
if test -n "${OIDCCookieHTTPOnly}"; then
    cat <<EOF | tee -a "${CONF_FILEPATH}"
OIDCCookieHTTPOnly ${OIDCCookieHTTPOnly:-On}
EOF
fi
if test -n "${OIDCCookieSameSite}"; then
    cat <<EOF | tee -a "${CONF_FILEPATH}"
OIDCCookieSameSite ${OIDCCookieSameSite:-On}
EOF
fi
if test -n "${OIDCXForwardedHeaders}"; then 
    cat <<EOF | tee -a "${CONF_FILEPATH}"
OIDCXForwardedHeaders ${OIDCXForwardedHeaders:-X-Forwarded-Host X-Forwarded-Port X-Forwarded-Proto} 
EOF
fi
## for redis 
if test "${OIDCCacheType}" = "redis"; then
    cat <<EOF | tee -a "${CONF_FILEPATH}"
## for redis
OIDCCacheType redis
OIDCRedisCacheServer ${OIDCRedisCacheServer:-localhost:6379}
EOF
fi
if test -n "${OIDCRedisCacheUsername}"; then
    cat <<EOF | tee -a "${CONF_FILEPATH}"
OIDCRedisCacheUsername ${OIDCRedisCacheUsername:-user}
OIDCRedisCachePassword ${OIDCRedisCachePassword:-password}
EOF
fi
if test -n "${OIDCRedisCacheDatabase}"; then
    cat <<EOF | tee -a "${CONF_FILEPATH}"
OIDCRedisCacheDatabase ${OIDCRedisCacheDatabase:-0}
EOF
fi

exec httpd -DFOREGROUND "$@"

Dockerfile

これらのファイルを含めるためのDockerfileを準備しますが、実際に作成したものから余計な部分を落としたファイルの内容は次のようになっています。

Dockerfile
FROM docker.io/library/httpd:2.4-alpine

RUN apk update && \
    apk add --no-cache tzdata bash ca-certificates
RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ apache-mod-auth-openidc

ENV OIDCRedirectURI https://httpd.example.com/redirect_ur
ENV OIDCProviderMetadataURL https://example.com/dex/.well-known/openid-configuration
ENV OIDCScope "openid email groups profile"
ENV OIDCClientID dex-client
ENV OIDCClientSecret c925b0e48ba5722521b1cbadc0cf6559
ENV OIDCClientName LDAP-ID

COPY httpd-foreground /usr/local/bin/httpd-foreground
RUN chmod a+rx /usr/local/bin/httpd-foreground
ENTRYPOINT ["/usr/local/bin/httpd-foreground"]

コンテナのビルド

Dockerfileとhttpd-foregroundスクリプトファイルを配置したディレクトリで次のコマンドでコンテナをbuildしています。

$ podman build . --pull --tag httpd-oidcauth:latest

コンテンツの配置

コンテンツは/usr/local/apache2/htdocs/以下に配置することで閲覧可能です。

この際にOIDCで認証をかけたい場所に.htaccessファイルを配置してください。

例えば、次のようなディレクトリ構造を準備します。

$ tree -a html
html
├── index.html
└── protected
    ├── .htaccess
    └── index.html

次のようなコマンドで作成したコンテナ内部を起動します。

httpd-oidcauthコンテナの起動
$ podman run -it --rm -d \
        -p 8080:80 \
        -e OIDCRedirectURI=http://localhost:8080/protected/redirect_uri \
        -e OIDCClientSecret=62284f8ae382a5fc9e4d3de420403143 \
        -e OIDCProviderMetadataURL=https://example.com/dex/.well-known/openid-configuration \
        -v `pwd`/html:/usr/local/apache2/htdocs \
        --name httpd-oidcauth \
        httpd-oidcauth:latest

http://localhost:8080/protected/にアクセスすることでOIDC認証が発生し、.htaccessの情報を元にアクセスが許可されます。

実際にクラスタリングする際にはRedisやMemcachedについても設定できるようにコンテナを作成する必要があります。

Kubernetesでの利用

これらを踏まえてKubernetesでの利用を検討してみます。

この時に外部のnginxと内部のingressへの接続はTLS化されています。

Dexは実際にはK8s内部に配置されていますが、アクセスはフロントエンドのReverse Proxyサーバーを経由して、その所在は問題ではありませんので図からは省略しています。

こうしてみると静的コンテンツをサービスするのに大袈裟なインフルが必要になりそうですが、k8sであれば簡単に配置できるので試してみます。

YAMLファイルの準備

共有ファイルシステムとコンテンツ配置用rsyncサーバーの配置

とりあえずrsync serverを経由してコンテンツファイルを転送できるようにしていきます。

まずコンテンツ配置用のPVCを準備します。

01.pvc-data.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
spec:
  accessModes: [ "ReadWriteMany" ]
  storageClassName: rook-cephfs
  resources:
    requests:
      storage: 1Gi

次にrsync接続を受け入れるsshdサーバーを立てていきます。

基本的なコンセプトは以下の記事をご覧ください。

$ conf
$ ssh-keygen -t ed25519 -f conf/id_ed25519
$ ssh-keygen -t ed25519 -f conf/ssh_host_ed25519_key
$ sudo kubectl create secret generic ssh-host-keys --from-file=conf/ssh_host_ed25519_key
$ sudo kubectl create secret generic ssh-auhorized-keys --from-file=conf/id_ed25519.pub
02.svc-rsync.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: rook-backup
spec:
  loadBalancerIP: 192.168.10.22
  type: LoadBalancer
  ports:
     -  port: 22
        protocol: TCP
        targetPort: 22
  selector:
    app: rook-backup
03.deploy-rsync.yaml
---
apiVersion: apps/v1
kind: Deployment         
metadata:                                                       
  name: rook-backup  
spec:       
  selector:                                                     
    matchLabels:                                                
      app: rook-backup                                          
  strategy:                                                     
    type: RollingUpdate                                         
  replicas: 2                                                                                                                   
  template:          
    metadata:          
      labels:             
        app: rook-backup
    spec:                
      imagePullSecrets:        
      - name: regcred   
      containers:       
      - name: rook-backup      
        image: docker.io/yasuhiroabe/rook-backup:1.1.4
        ports:
        - containerPort: 22
        env:                  
        - name: ROOK_SSH_AUTHKEYS_FILEPATH
          value: /root/.ssh/authorized_keys
        - name: ROOK_SSH_PUBKEY_FILEPATH
          value: /conf/keys/id_ed25519.pub
        - name: ROOK_SSH_SERVERKEYS_FILEPATH_LIST
          value: "/conf/sshd/ssh_host_ed25519_key"
        volumeMounts:
        - name: data-pv
          mountPath: /data
          readOnly: false
        - name: sshd-keys
          mountPath: /conf/sshd
          readOnly: true
        - name: ssh-keys
          mountPath: /conf/keys
          readOnly: true
      volumes:
      - name: data-pv
        persistentVolumeClaim:
          claimName: data
      - name: sshd-keys
        secret:
          secretName: ssh-host-keys
          defaultMode: 0400
      - name: ssh-keys
        secret:
          secretName: ssh-auhorized-keys
          defaultMode: 0400

/dataにmountしているPVCはreadOnly: falseに設定していますが、もしこのコンテナをバックアップの取得用に利用する場合にはreadOnly: trueにして不用意にコンテンツが変更されるのを防いでください。

コンテンツの配置

ここまでのYAMLファイルなどの反映が無事に終わると、作成してssh_ed25519ファイルでサーバーに接続できるはずです。

sshコマンドによる接続確認
$ ssh -i conf/id_ed25519 root@192.168.10.22

静的コンテンツはこのアドレスを経由してrsyncなどで配置してください。

rsyncを利用したコンテンツの配置
$ rsync -av -e "ssh -i conf/id_ed25519" html/. root@192.168.10.22:/data/.

Redisの配置

キャッシュするためのRedisサーバーを配置します。

04.svc-redis.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: redis-svc
spec:
  type: ClusterIP
  ports:
    - port: 6379
      targetPort: 6379
      protocol: TCP
  selector:
    app: redis
05.pvc-redis.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis
spec:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: rook-ceph-block
  resources:
    requests:
      storage: 1Gi
06.deploy-redis.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  labels:
    app: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:4.0.7
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: redis-pvc
          mountPath: /data
          readOnly: true
      volumes:
      - name: redis-pvc
        persistentVolumeClaim:
          claimName: redis-pvc

httpd本体の配置

次に先に作成しておいたhttpdサーバーを配置します。

07.svc-httpd.yaml
apiVersion: v1
kind: Service
metadata:
  namespace: uoa-class-interview
  name: httpd
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  selector:
    app: httpd

最後にhttpd本体を起動します。

08.deploy-httpd.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: uoa-class-interview
  name: httpd
  labels:
    app: httpd
spec:
  replicas: 3
  selector:
    matchLabels:
      app: httpd
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: httpd
    spec:
      imagePullSecrets:
      - name: regcred
      containers:
      - name: httpd
        image: docker.io/yasuhiroabe/httpd-oidcauth:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          name: httpd
        env:
        - name: OIDCRedirectURI
          value: "https://www.example.com/protected/redirect_uri"
        - name: OIDCClientSecret
          value: "62284f8ae382a5fc9e4d3de420403143"
        - name: OIDCProviderMetadataURL
          value: "https://example.com/dex/.well-known/openid-configuration"
        - name: OIDCCacheType
          value: redis
        - name: OIDCRedisCacheServer
          value: redis:6379
        - name: OIDCRedisCacheDatabase
          value: "0"
        - name: OIDCCookiePath
          value: /labs/opm/report/class_interview/
        - name: OIDCCookieHTTPOnly
          value: "On"
        - name: OIDCCookieSameSite
          value: "On"
        - name: OIDCXForwardedHeaders
          value: "X-Forwarded-Host X-Forwarded-Port X-Forwarded-Proto"
        - name: OIDCCryptoPassphrase
          value: d034a0dbf430aac85f902617784ed8c8
        volumeMounts:
        - name: data-pvc
          mountPath: /usr/local/apache2/htdocs
      volumes:
      - name: data-pvc
        persistentVolumeClaim:
          claimName: data

遭遇した不具合

以下の不具合については、YAMLファイルに反映されて既に解決していますが、まとめておきます。

OIDCXForwardedHeadersが未設定で表示されるエラー

httpdのログメッセージ
[Thu Dec 26 01:40:21.723949 2024] [auth_openidc:warn] [pid 20:tid 24] [client 10.233.82.217:58256] oidc_check_x_forwarded_hdr: header X-Forwarded-Host received but OIDCXForwardedHeaders not configured for it
...
...

X-Forwarded-Hostだけでなく、X-Forwarded-PortX-Forwarded-Protoが渡されている場合にもログにエラーが記録されています。

ログメッセージのとおりOIDCRedirectURIのschemeと自身へのリクエストのschemeが一致していないことが原因です。

Ingressからの接続はTLS化していないので、Reverse Proxy ServerまではTLS化されていることを伝えるために、X-Forwarded-*ヘッダーを利用することをOIDCXForwardedHeadersに明示的に指定する必要がありました。

Session CookieのSecureフラグ

HttpOnlyはtrueですが、Secureフラグはfalseになっています。

httpdのログメッセージ
[Thu Dec 26 01:40:22.965667 2024] [auth_openidc:error] [pid 20:tid 26] [client 10.233.78.30:58712] oidc_request_check_cookie_domain: the URL scheme (https) of the configured OIDCRedirectURI does not match the URL scheme of the URL being accessed (http): the "state" and "session" cookies will not be shared between the two!

このメッセージは前述のOIDCXForwardedHeadersを設定することで解消されます。

ERR invalid expire time in 'setex' command

このエラーメッセージはmod_auith_openidcの2.4.16.6で修正されています。(Release Notes) 2.4.16.2で再発し、2.4.16.5までは影響を受けるので現在のAlpineのモジュールは影響を受けます。

将来的に修正されるはずですが、現状では特に不具合は発生していません。

2.6.16.6をコンパイルして入れ替えるとメッセージ自体が消えることは確認しています。

Redisからデータが取得できていない

httpdのログメッセージ
[Thu Dec 26 04:46:25.087947 2024] [auth_openidc:error] [pid 38:tid 82] [client 10.233.78.30:53308] oidc_response_match_state: unable to restore state, referer: https://example.com/
[Thu Dec 26 04:46:25.087953 2024] [auth_openidc:error] [pid 38:tid 82] [client 10.233.78.30:53308] oidc_response_process: invalid authorization response state and no default SSO URL is set, sending an error..., referer: https://example.com/

これはクラスター間でRedisに格納された情報が復号化できない事に起因します。

サーバー間で復号鍵は共有しなければいけないため、必ずOIDCCryptoPassphraseを設定してください。

CookieのSameSite設定をStrictに変更できない

mod_auth_openidcのバージョンが古い事に起因します。現在の2.4.16.4では"On"か"Off"のみを受け付けます。

ソースコードから2.4.16.6のmod_auth_openidc.soを作成し、置き換えることでStrictに設定することができます。

現状のコンテナはtestingに入っているパッケージをそのまま利用していますので、YAMLファイル中では"On"を設定しています。

さいごに

静的コンテンツを提供する仕組みを1つのサービスに統合すると、誰がどのファイルを変更するのか、責任分岐点が課題になりそうなので汎用的に使える認証機能付きの静的コンテンツ提供サービスの骨組みを作りました。

これまではad-hocにコンテンツを含むコンテナを構築していたので、セキュリティ上少し堅牢にしたかったのと、必要に応じてコンテンツを含むコンテナも簡単なステップの追加で作成できるようになりました。

これまでの仕組みを整理する良い機会だったと思います。

一連の作業の目玉はgRPCを使ってDEX APIを操作することなので、もう少し作業を進めたいと思います。

以上

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?