OPAでSSHとsudoの認可をする
前回 に引き続きOPAの勉強をしています。今回の記事では公式チュートリアルの「SSH and sudo Authorization」を見ます。
ゴール
このチュートリアルではLinux-PAM(Pluggable Authentication Modules)とOPAを連動させています。
Linux-PAM(以下PAM)について僕は今回初めて知ったのですが、Linuxに認証の仕組みをプラグインとして追加できるものという理解です。ここでOPAと連動できるPAMを使うことで、認証に加えてPolicyに沿った 認可 も行えるようにします。OPA公式のリポジトリにこのチュートリアル用のOPAのPAMがあります。
これを使って次のことを実現します!
さらに、次の登場人物がいると仮定します。
frontend-dev
はfrontend
ホストで動いているコードにコントリビュートしている開発者backend-dev
はbackend
ホストで動いているコードにコントリビュートしている開発者ops
は組織内のAdmin
図示すると次のようになります。
準備
まずは作業場所を用意します。
$ mkdir ssh_sudo_auth $ cd ssh_sudo_auth
上の図の状況を作り上げるために frontend
, backend
ホストとOPAを docker-compose
で立ち上げます。
# 好きなエディタで作ります
$ vim tutorial-docker-compose.yaml
yaml
の中身は次の通り。
version: '2' services: opa: image: openpolicyagent/opa:0.10.5 ports: - 8181:8181 # WARNING: OPA is NOT running with an authorization policy configured. This # means that clients can read and write policies in OPA. If you are # deploying OPA in an insecure environment, be sure to configure # authentication and authorization on the daemon. See the Security page for # details: https://www.openpolicyagent.org/docs/security.html. command: - "run" - "--server" - "--log-level=debug" frontend: image: openpolicyagent/demo-pam ports: - "2222:22" volumes: - ./frontend_host_id.json:/etc/host_identity.json backend: image: openpolicyagent/demo-pam ports: - "2223:22" volumes: - ./backend_host_id.json:/etc/host_identity.json
WARNING
の部分が何を言っているかというと、 OPA自身をPolicyで守っていないのでセキュアじゃないということです。
例えばREST APIは誰でも叩けてしまうので、OPAに対してリクエストを投げられる人がいれば、誰でもPolicyに変更を加えられちゃいます。
本来どうOPA自身を守るべきかという点については以下の公式ドキュメントにまとめられています。
それでは、このチュートリアル用のPAMがホストを識別するための host_id
情報を含んだ json
を作ります。
PAMはこのJSONを必要に応じて読み込んで、自分が frontend
なのか backend
なのか判別しています。
$ echo '{"host_id": "frontend"}' > frontend_host_id.json $ echo '{"host_id": "backend"}' > backend_host_id.json
準備が整ったので docker-compose
で立ち上げます!
$ docker-compose -f tutorial-docker-compose.yaml up
OPA連携するPAMの概要
このチュートリアルで用意されているPAMとOPAの関係は下図のようだと理解しました。
- PAMがOPAの
pull
エンドポイントに、システムから取得するファイル一覧を聞く - PAMがOPAの
sshd/authz
エンドポイントに、SSHしていいか聞く - PAMがOPAの
sudo/authz
エンドポイントに、sudoしていいか聞く
PAMの設定を実際に見てみましょう。 frontend
コンテナに入ります。
$ docker-compose -f tutorial-docker-compose.yaml exec frontend bash
sudo
のPAM設定を覗いてみます。
$ cat /etc/pam.d/sudo # Enable the Authz PAM module # At image build time, the value of HOST_UUID is replaced with the role of the image (for example, webapp) auth required /lib/security/pam_authz.so url=http://opa:8181 authz_endpoint=/v1/data/sudo/authz display_endpoint=/v1/data/display pull_endpoint=/v1/data/pull log_level=debug
auth required
に/lib/security/pam_authz.so
を指定している- OPAのエンドポイントを
url
でhttp://opa:8181
と指定している pull_endpoint
に/v1/data/pull
を指定しているauthz_endpoint
に/v1/data/sudo/authz
を指定している
のがわかります。 sudo
するときOPAに認可を問い合わせしている雰囲気がなんとなくですけど伝わってきますね!
注意:
display_endpoint
は本記事では扱わないので説明は割愛します。
Policyの作成
pull Policy作成
それでは、まずは pull
エンドポイント用のPolicyを作ります。PolicyはRegoで書きます。
$ cat >pull.rego <<EOF package pull # Which files should be loaded into the context? files = ["/etc/host_identity.json"] # Which environment variables should be loaded into the context? env_vars = [] EOF
上記を用意しておくことで、PAMが pull
エンドポイントに問い合わせをしたときに、どんなファイルを読み込む必要があるかを知ることができます。(この場合は /etc/host_identity.json
を読み込みます)
では、このPolicyをOPAにREST API経由で入れます。
$ curl -X PUT --data-binary @pull.rego \ localhost:8181/v1/policies/pull
sshdの認可Policy作成
では、SSHするときに適用する認可Policyを作りましょう。
$ cat >sshd_authz.rego <<EOF package sshd.authz import input.pull_responses import input.sysinfo import data.hosts # By default, users are not authorized. default allow = false # Allow access to any user that has the "admin" role. allow { data.roles["admin"][_] = input.sysinfo.pam_username } # Allow access to any user who contributed to the code running on the host. # # This rule gets the `host_id` value from the file `/etc/host_identity.json`. # It is available in the input under `pull_responses` because we # asked for it in our pull policy above. # # It then compares all the contributors for that host against the username # that is asking for authorization. allow { hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_] = sysinfo.pam_username } # If the user is not authorized, then include an error message in the response. errors["Request denied by administrative policy"] { not allow } EOF
ポイントとしては以下のとおり。
- 問い合わせしてきたユーザー(
sysinfo.pam_username
)がroles
のadmin
に含まれていれば許可 - 問い合わせしてきたユーザー(
sysinfo.pam_username
)がpull
で指定されたファイル/etc/host_identity.json
のホストにコントリビュートした人(contributors
)であれば許可 - 上に該当しなければエラーとする
input
に関連する情報はPAMがOPAにリクエストするときに渡ってくるデータです。次のようなJSONがリクエストに含まれてくるとイメージしてください。
{ "input": { "display_responses": { "last_name": "<ユーザーが入力した情報。本記事では扱いません。>", "secret": "<ユーザーが入力した情報。本記事では扱いません。>" }, "pull_responses": { "files": { "/etc/host_identity.json": { "host_id": "<JSON内に指定されたhost_id>" } }, "env_vars": {} }, "sysinfo": { "pam_username": "<PAMセッション内の値>", "pam_service": "<PAMセッション内の値>", "pam_req_username": "<PAMセッション内の値>", "pam_req_hostname": "<PAMセッション内の値>" } } }
また、Policy内に登場する roles
やcontributors
のデータはこの後OPAに投入します!
まずは上のPolicyをOPAに投入しましょう。
$ curl -X PUT --data-binary @sshd_authz.rego \ localhost:8181/v1/policies/sshd/authz
sudoの認可Policy作成
sudoするときのPolicyも同じ要領で作ります。
$ cat >sudo_authz.rego <<EOF package sudo.authz # By default, users are not authorized. default allow = false # Allow access to any user that has the "admin" role. allow { data.roles["admin"][_] = input.sysinfo.pam_username } # If the user is not authorized, then include an error message in the response. errors["Request denied by administrative policy"] { not allow } EOF
これはシンプルで、
- 問い合わせしてきたユーザー(
sysinfo.pam_username
)がroles
のadmin
に含まれていれば許可 - 上に該当しなければエラーとする
というPolicyになります。 これもOPAに投入します。
$ curl -X PUT --data-binary @sudo_authz.rego \ localhost:8181/v1/policies/sudo/authz
その他必要なデータの投入
roles
roles
のデータを投入します。これで ops
ユーザーだけが admin
となります。
$ curl -X PUT localhost:8181/v1/data/roles -d \ '{ "admin": ["ops"] }'
hosts
ホストの情報と、そのホストに対するコントリビューターの情報を投入します。
frontend
のコントリビューターとして frontend-dev
を入れて、 backend
のコントリビューターとして backend-dev
を定義します。
$ curl -X PUT localhost:8181/v1/data/hosts -d \ '{ "frontend": { "contributors": [ "frontend-dev" ] }, "backend": { "contributors": [ "backend-dev" ] } }'
実践!
準備ができました!では、Policyがちゃんと動作するか確認していきましょう。
opsユーザーとしてSSHとsudo
まずは ops
ユーザーでSSHとsudoをしてみましょう。 ops
は admin
なので成功するはずです。
ローカルのポート2222が frontend
のホストにつながるはずなので試してみます。
$ ssh -p 2222 ops@localhost \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ops@f55968aabd47:~$
SSHはできました!では、sudoはどうでしょう?
$ sudo ls / OPA-PAM[49]: Session log level is set to debug OPA-PAM[49]: Commencing display cycle. OPA-PAM[49]: Initializing HTTP request GET /v1/data/display OPA-PAM[49]: HTTP request body: (null) OPA-PAM[49]: HTTP request complete, libcURL returned with 0. OPA-PAM[49]: HTTP response body: {} OPA-PAM[49]: Value of field 'result' does not have type object in JSON response. Please ensure that your endpoint flag '/v1/data/display' matches your package path. OPA-PAM[49]: Commencing pull cycle. OPA-PAM[49]: Initializing HTTP request GET /v1/data/pull OPA-PAM[49]: HTTP request body: (null) OPA-PAM[49]: HTTP request complete, libcURL returned with 0. OPA-PAM[49]: HTTP response body: {"result":{"env_vars":[],"files":["/etc/host_identity.json"]}} OPA-PAM[49]: Loaded JSON from file : { "host_id": "frontend" } OPA-PAM[49]: Collecting system information. OPA-PAM[49]: Loaded sysinfo pam_username: ops OPA-PAM[49]: Loaded sysinfo pam_service: sudo OPA-PAM[49]: Loaded sysinfo pam_req_username: ops OPA-PAM[49]: Loaded sysinfo pam_req_hostname: OPA-PAM[49]: Commencing authz cycle. OPA-PAM[49]: Initializing HTTP request POST /v1/data/sudo/authz OPA-PAM[49]: HTTP request body: {"input":{"display_responses":{},"pull_responses":{"files":{"/etc/host_identity.json":{"host_id":"frontend"}},"env_vars":{}},"sysinfo":{"pam_username":"ops","pam_service":"sudo","pam_req_username":"ops","pam_req_hostname":""}}} OPA-PAM[49]: HTTP request complete, libcURL returned with 0. OPA-PAM[49]: HTTP response body: {"result":{"allow":true,"errors":[]}} OPA-PAM[49]: Freeing allocated data. OPA-PAM[49]: Application invoked pam_sm_setcred. bd_build bin boot create_user.sh dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var OPA-PAM[49]: Application invoked pam_sm_setcred.
sudo ls /
してみると、ドカッとログがいっぱい出てきました!これはPAMが debug
モードで走ってるのでログがたくさん出るようになっているからです。
1行1行見ていくとなんとなく何をしているかというのがわかります。ポイントとしては次の行です。
Initializing HTTP request GET /v1/data/pull
Loaded sysinfo pam_username: ops
Initializing HTTP request POST /v1/data/sudo/authz
HTTP response body: {"result":{"allow":true,"errors":[]}}
上で用意したPolicyが動作しているのがわかりますね!最終的に allow: true
になっているので、 ls
の内容も出力されています。
bd_build bin boot create_user.sh dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
SSHから抜けておきましょう。
$ exit
adminじゃないユーザーでsudo
それでは、adminじゃないユーザーでsudoをしてみましょう。
frontend-dev
ユーザーとして frontend
ホストにSSHします。
$ ssh -p 2222 frontend-dev@localhost \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null frontend-dev@f55968aabd47:~$
SSHは成功します。ではsudoしたらどうなるでしょう?
$ sudo ls / OPA-PAM[81]: Session log level is set to debug OPA-PAM[81]: Commencing display cycle. OPA-PAM[81]: Initializing HTTP request GET /v1/data/display OPA-PAM[81]: HTTP request body: (null) OPA-PAM[81]: HTTP request complete, libcURL returned with 0. OPA-PAM[81]: HTTP response body: {} OPA-PAM[81]: Value of field 'result' does not have type object in JSON response. Please ensure that your endpoint flag '/v1/data/display' matches your package path. OPA-PAM[81]: Commencing pull cycle. OPA-PAM[81]: Initializing HTTP request GET /v1/data/pull OPA-PAM[81]: HTTP request body: (null) OPA-PAM[81]: HTTP request complete, libcURL returned with 0. OPA-PAM[81]: HTTP response body: {"result":{"env_vars":[],"files":["/etc/host_identity.json"]}} OPA-PAM[81]: Loaded JSON from file /etc/host_identity.json: { "host_id": "frontend" } OPA-PAM[81]: Collecting system information. OPA-PAM[81]: Loaded sysinfo pam_username: frontend-dev OPA-PAM[81]: Loaded sysinfo pam_service: sudo OPA-PAM[81]: Loaded sysinfo pam_req_username: frontend-dev OPA-PAM[81]: Loaded sysinfo pam_req_hostname: OPA-PAM[81]: Commencing authz cycle. OPA-PAM[81]: Initializing HTTP request POST /v1/data/sudo/authz OPA-PAM[81]: HTTP request body: {"input":{"display_responses":{},"pull_responses":{"files":{"/etc/host_identity.json":{"host_id":"frontend"}},"env_vars":{}},"sysinfo":{"pam_username":"frontend-dev","pam_service":"sudo","pam_req_username":"frontend-dev","pam_req_hostname":""}}} OPA-PAM[81]: HTTP request complete, libcURL returned with 0. OPA-PAM[81]: HTTP response body: {"result":{"allow":false,"errors":["Request denied by administrative policy"]}} OPA-PAM[81]: Received authz error log from OPA: Request denied by administrative policy OPA-PAM[81]: Freeing allocated data. Sorry, try again. OPA-PAM[81]: Application invoked pam_sm_authenticate. OPA-PAM[81]: Session log level is set to debug ... OPA-PAM[81]: HTTP response body: {"result":{"allow":false,"errors":["Request denied by administrative policy"]}} OPA-PAM[81]: Received authz error log from OPA: Request denied by administrative policy ... Sorry, try again. ... OPA-PAM[81]: Session log level is set to debug ... OPA-PAM[81]: HTTP response body: {"result":{"allow":false,"errors":["Request denied by administrative policy"]}} OPA-PAM[81]: Received authz error log from OPA: Request denied by administrative policy ... sudo: 3 incorrect password attempts
失敗しているのがわかります。エラー文言もPolicyで設定していた Request denied by administrative policy
になっています。
3回挑戦した結果、 sudo: 3 incorrect password attempts
と言ってあきらめています。
SSHから抜けておきます。
$ exit
コントリビューターじゃないホストへSSH
では、最後にコントリビュートしていないホストへのSSHがちゃんと拒否されるか確認しましょう。
frontend-dev
ユーザーとして backend
ホストにSSHしてみます。( backend
はローカルのポート2223で待ち受けています)
$ ssh -p 2223 frontend-dev@localhost \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null frontend-dev@localhost: Permission denied (keyboard-interactive).
ちゃんと拒否されます! docker-compose
のログをみると下のようにエラーを返しているのも確認できます。
opa_1 | time="2019-03-23T07:45:53Z" level=info msg="Sent response." client_addr="172.18.0.3:48434" req_id=39 req_method=POST req_path=/v1/data/sshd/authz resp_body="{\"result\":{\"allow\":false,\"errors\":[\"Request denied by administrative policy\"]}}" resp_bytes=79 resp_duration=2.7604 resp_status=200
準備したPolicyに対して、想定した動きになっているのが確認できました!
おわりに
本記事ではOPAとLinux-PAMを連携することで、SSHとsudoにOPAの認可Policyを適用する方法について見ました! 公式チュートリアルでは最後に 動的にユーザーの権限を昇格させる方法 について述べています。この記事では割愛していますが、ぜひそちらもチャレンジしてみてください。
「どんな人」が「何を」していいかというのをPolicyで制御できる
OPAの可能性の1つを学ぶことができました。