アドベントカレンダー5日目は、Policy as Codeを実現する話題のOSS「Open Policy Agent」の使い道(ユースケース)をいくつか紹介します。
Open Policy Agentとは
クラウドネイティブの台頭により、昨今のシステムの構成は大規模化&複雑化しています。
そのようなシステムをセキュアな状態にするには、システムを構成するサービス1つ1つに対して適切な権限設定をしていく必要があります。
そんなときに便利なのが、Open Policy Agent(OPA:おーぱ)です。
Open Policy Agentは様々なサービスのポリシー設定を同じ書き方(Rego)で表現することができます。
また、システム全体のポリシー管理を、サービス自体のコードから切り離すことになります。
これにより、システム全体のポリシーの管理(例:どのロールのユーザにどのような権限をあたえるのか)が容易になります。
ユースケース1(SSH、sudo)
ここからはユースケースを紹介していきます。
一つ目は、Linux環境にてSSHやsudoが行われた場合の制御です。
LinuxにはPAM (Pluggable Authentication Modules for Linux) と呼ばれる認証モジュールが組み込まれており、これを用いてユーザ認証が行われています。
そして、PAMとOpen Policy Agentを用いることでポリシー管理ができます。
今回は、サービスA開発者とサービスB開発者はそれぞれの開発しているサービスのみ、管理者は両方のサービスにSSHできるケースを考えます。(sudoも同じ要領で記述できます。)
ユーザから仮想マシンにアクセスがあった際、各サービスはOpen Policy Agentにアクセスし、ポリシーとデータを確認します。
今回のポリシーは下記のソースのようになります。
package sshd.authz
import input.pull_responses
import input.sysinfo
import data.hosts
# デフォルトでは、すべてのユーザを拒否
default allow = false
# 管理者(admin)の場合にSSHを認可
allow {
data.roles["admin"][_] == input.sysinfo.pam_username
}
# 該当サービスの開発者であった場合にSSHを認可
allow {
hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_] == sysinfo.pam_username
}
# 認可されなかった場合に、エラーメッセージを出力
errors["Request denied by administrative policy"] {
not allow
}
このとき、data.roles["admin"][_]
でシステム管理者(admin)を判別できるよう、下記データを読み込みます。
{
"admin": ["admin"]
}
同じように、hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_]
でサービス開発者(serviceA-developer、serviceB-developer)を判別できるよう、下記データを読み込みます。
{
"serviceA": {
"contributors": [
"serviceA-developer"
]
},
"serviceB": {
"contributors": [
"serviceB-developer"
]
}
}
※OPAの責任範囲は「認可」のみであり、「認証」は対象外であるためSSH時のauthorized_keysによる認証は、ほかのID管理システムに任せることになります。
ユースケース2(Docker)
【その1: seccomp=unconfined
オプションの無効化】
Dockerでは、seccomp(secure computing mode)というLinuxカーネルの機能を用いることで、コンテナ内で利用可能なDockerコマンドを限定することができます。
しかし、この機能はユーザがseccomp=unconfined
というオプションをつけることで、seccompプロファイルの内容を無視してDockerコマンドを使うことができてしまいます。
そのため、ユーザがInsecureなコンテナを取り扱わないように、Open Policy Agentを用いて、seccomp=unconfined
オプションを無効にすることが推奨されます。
package docker.authz
default allow = false
# denyの条件に該当しなければ、認可を許可する
allow {
not deny
}
# seccomp_unconfinedで定義されているルールに該当する場合はdeny
deny {
seccomp_unconfined
}
# コンテナを実行する際に`seccomp=unconfined`オプションを用いていた場合は該当
seccomp_unconfined {
input.Body.HostConfig.SecurityOpt[_] == "seccomp:unconfined"
}
seccompプロファイルは、mobyのGithubリポジトリ上にあるdefault.jsonを編集して作成します。
デフォルトでは、300以上あるシステムコールのうち、下記URLのものが制限されています。(Dockerコマンドで使えないようにされています。)
https://matsuand.github.io/docs.docker.jp.onthefly/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile
【その2: 特定ユーザのみ認可】
ユースケース1(SSH、sudo)と同様に、特定ユーザのみがDockerコマンドを使えるように設定することもできます。また、readOnly(write権限無し)ユーザを定義することも可能です。
この例では、ユーザAはDockerコンテナに対するすべてのコマンド、ユーザBはreadOnlyなコマンドのみが認可されています。
package docker.authz
default allow = false
# readOnlyでない(read/write権限のある)ユーザがすべてのメソッドを実施することを認可
allow {
user_id := input.Headers["Authz-User"]
user := users[user_id]
not user.readOnly
}
# readOnlyと定義されたユーザは、readOnlyなコマンドのみ認可
allow {
user_id := input.Headers["Authz-User"]
users[user_id].readOnly
input.Method == "GET"
}
# readOnlyのユーザと、readOnlyでない(read/write権限のある)ユーザを定義
users = {
"userA": {"readOnly": false},
"userB": {"readOnly": true},
}
ユースケース3(Kubernetes)
※ Open Policy Agent 公式ドキュメントより引用
Open Policy AgentをKubernetesのAdmission Controllersとして導入すると下記のようなことが可能になります。
- 特定のラベルがついていないリソースへのアクセスのみを認可
- 取得可能なコンテナイメージを、企業が所持しているイメージレジストリのみに限定
- すべてのPodに対してリソースのリクエストとリミットを特定できるように定義
- すでに存在するIngressと競合しないように新規Ingressの設定を限定
ここでは2つのみ、Regoの記述方法を記載します。
【その1:取得可能なコンテナイメージを、企業が所持しているイメージレジストリのみに限定】
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
# すでに存在するコンテナのイメージをすべて洗い出す
some i
image := input.request.object.spec.containers[i].image
# コンテナイメージの名前が、"hooli.com/"からはじまっていない場合に、
#「信用していないレジストリから取得したイメージ」とメッセージを出力
not startswith(image, "hooli.com/")
msg := sprintf("image '%v' comes from untrusted registry", [image])
}
【その2:すでに存在するIngressと競合しないように新規Ingressの設定を限定】
package kubernetes.admission
# Ingressの設定が、すでに存在するIngressと競合した場合に、認可を行わない
deny[msg] {
input.request.kind.kind == "Ingress"
# インプット対象のIngressをすべて洗い出す
some i
newhost := input.request.object.spec.rules[i].host
# すでに存在するIngressをすべて洗い出す
some namespace, name, j
oldhost := ingresses[namespace][name].spec.rules[j].host
# インプット対象のIngressのホストと、すでに存在するIngressのホストが一致するかどうかを確認
newhost == oldhost
msg := sprintf("ingress host conflicts with ingress %v/%v", [namespace, name])
}
Admission ControllersとしてOpen Policy Agentを利用すると、Kubernetes環境の自動修正も行うこともできます。
例えば、下記のような修正が可能です。
- 特定のアノテーションを、すべてのリソースにいれる
- サイドカーコンテナをPodsの中に入れる
- 企業が所持しているイメージレジストリを使うようにコンテナの設定を変える
- デプロイメントにNodeとPodのアフィニティ・セレクタを含める
また、KubernetesとOpen Policy Agentの統合に特化したOPA Gatekeeperというモジュールがあります。
OPA Gatekeeperを使うことで、よりOpen Policy AgentがKubernetes上で使いやすくなります。
ユースケース4(Envoy、Istio)
サービスメッシュに対しても、Open Policy Agentは有効です。
今回は簡略化のため、サイドカーとしてEnvoyを利用しているケースについてのせます。
このケースでは、開発者からのリクエストをEnvoyが受け取った後に、Open Policy Agentがポリシーを確認します。(①)
そして認可がおりた場合、Envoyはサービスにアクセスして情報を取得します。(②)
①②両方を行うことで、Envoyは開発者にレスポンスを返すことができます。
package envoy.authz
import input.attributes.request.http as http_request
default allow = false
# action_allowedで定義されているルールに該当する場合はallow
allow {
action_allowed
}
# パスが/pattern1であり、メソッドがGETの場合は該当
action_allowed {
http_request.method == "GET"
glob.match("/pattern1", ["/"], http_request.path)
}
# パスが/pattern3であり、メソッドがPOSTの場合は該当
action_allowed {
http_request.method == "POST"
glob.match("/pattern3", ["/"], http_request.path)
}
/pattern1はallowされているため求めていたレスポンスが返ってきますが、
/pattern2はallowされていないため、Envoyが②の処理を実施せず、求めていたレスポンスは
返ってこない仕組みとなっています。
さいごに
他にもKong(API Gateway)やTerraform、Kafkaといった様々なものと組み合わせて使うことができます。Open Policy Agentは使い道が多くてとても便利なOSSですので、ぜひ皆さんご活用ください!