1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

OPAを使ったSPIREのAPIアクセス制御について

Last updated at Posted at 2021-08-31

このエントリでは、SPIREに新しく実装されたOPAを使ったAPIのアクセス制御について、調査した結果をまとめています。

背景

現在、SPIRE Serverが提供するAPIはgRPC Serviceとして提供されており、Methodの処理前にリクエスト元のコンテキストによるアクセス制御が実装されています。

SPIREでの主なRole

Role 説明
Admin TCP経由でのAPI操作などで必要。RegistrationEntryで定義される。
Local UNIX Domain Socket経由でのアクセスで割り当てられる
Agent Node Attestationに成功したSPIRE Agentに割り当てられる
Downstream Nested構成の際のDownstream Serverに割り当てられる。RegistrationEntryで定義される。

各API毎のアクセス制御

API(gRPC Method) 許可されたRole
/spire.api.server.svid.v1.SVID/MintX509SVID Admin, Local,
/spire.api.server.svid.v1.SVID/MintJWTSVID Admin, Local,
/spire.api.server.svid.v1.SVID/BatchNewX509SVID Agent,
/spire.api.server.svid.v1.SVID/NewJWTSVID Agent,
/spire.api.server.svid.v1.SVID/NewDownstreamX509CA Downstream,
/spire.api.server.bundle.v1.Bundle/GetBundle Any,
/spire.api.server.bundle.v1.Bundle/AppendBundle Admin, Local,
/spire.api.server.bundle.v1.Bundle/PublishJWTAuthority Downstream,
/spire.api.server.bundle.v1.Bundle/CountBundles Admin, Local,
/spire.api.server.bundle.v1.Bundle/ListFederatedBundles Admin, Local,
/spire.api.server.bundle.v1.Bundle/GetFederatedBundle Admin, Local, Agent
/spire.api.server.bundle.v1.Bundle/BatchCreateFederatedBundle Admin, Local,
/spire.api.server.bundle.v1.Bundle/BatchUpdateFederatedBundle Admin, Local,
/spire.api.server.bundle.v1.Bundle/BatchSetFederatedBundle Admin, Local,
/spire.api.server.bundle.v1.Bundle/BatchDeleteFederatedBundle Admin, Local,
/spire.api.server.debug.v1.Debug/GetInfo Local,
/spire.api.server.entry.v1.Entry/CountEntries Admin, Local,
/spire.api.server.entry.v1.Entry/ListEntries Admin, Local,
/spire.api.server.entry.v1.Entry/GetEntry Admin, Local,
/spire.api.server.entry.v1.Entry/BatchCreateEntry Admin, Local,
/spire.api.server.entry.v1.Entry/BatchUpdateEntry Admin, Local,
/spire.api.server.entry.v1.Entry/BatchDeleteEntry Admin, Local,
/spire.api.server.entry.v1.Entry/GetAuthorizedEntries Agent,
/spire.api.server.agent.v1.Agent/CountAgents Admin, Local,
/spire.api.server.agent.v1.Agent/ListAgents Admin, Local,
/spire.api.server.agent.v1.Agent/GetAgent Admin, Local,
/spire.api.server.agent.v1.Agent/DeleteAgent Admin, Local,
/spire.api.server.agent.v1.Agent/BanAgent Admin, Local,
/spire.api.server.agent.v1.Agent/AttestAgent Any,
/spire.api.server.agent.v1.Agent/RenewAgent Agent,
/spire.api.server.agent.v1.Agent/CreateJoinToken Admin, Local,
/grpc.health.v1.Health/Check Local,
/grpc.health.v1.Health/Watch Local,

ただしそれらのルールはソースコード内に静的に定義されており、柔軟な運用が難しいという課題がありました。

例えば、SPIRE Serverとは別のノードからRegistrationEntryの一覧のみを取得したいとなった場合(/spire.api.server.entry.v1.Entry/ListEntries)には、リクエスト元にはAdmin Roleが必要になってしまい、不必要な権限まで与えてないといけないといった問題が出てきます。
実運用を考えると、APIを操作するクライアント毎に管理者権限を渡す必要があることは、導入にあたっての大きな課題となる場合もあります。

この問題に対してコミュニティでは以下のIssueで検討が進めら、OPAをベースにしたアクセス制御の実装案が採用されました。

Issue → spiffe/spire#1975

OPAを使ったアクセス制御

2021/08 現在のSPIREの最新バージョンは v1.0.1ですが、次のパッチバージョンのv1.0.2からはOPAベースのアクセス制御になります。(アクセス制御の挙動に変更はないのでパッチバージョンでリリースされている?)

cf. https://github.com/spiffe/spire/blob/main/doc/authorization_policy_engine.md

アーキテクチャとしては、別途OPAサーバを用意するのではなくライブラリとして組み込むパターンとなります。

cf. https://www.openpolicyagent.org/docs/latest/extensions/#custom-built-in-functions-in-go

SPIREからOPAへの入力には以下のようなデータが渡されます。

  • Caller: リクエスト元のSPIFFE ID
  • FullMethod: gRPC Service Methodの完全修飾名(e.g. /spire.api.server.svid.v1.SVID/MintX509SVID")
  • Req: リクエストBodyから受け取ったデータ
type Input struct {
	// Caller is the authenticated identity of the actor making a request.
	Caller string `json:"caller"`

	// FullMethod is the fully-qualified name of the proto rpc service method.
	FullMethod string `json:"full_method"`

	// Req represents data received from the request body. It MUST be a
	// protobuf request object with fields that are serializable as JSON,
	// since they will be used in policy definitions.
	Req interface{} `json:"req"`
}

SPIREは各APIリクエストに対して、上記のInputを引数としてOPAを呼び出します。

	rs, err := e.rego.Rego(rego.Input(input)).Eval(ctx)

OPAではRegoで記述されたPolicyにしたがってInput情報とDataを評価し、SPIREはその結果を使ってアクセス制御を行います。

SPIREに組み込まれているデフォルトの挙動は、DataにてgRPC Method毎に必要な権限が定義されており、PolicyではInputの値に含まれるMethod名からDataを参照して必要な権限を確認します。
つまり、これまでソースコード内で定義されていたAPIとRoleの紐付けのようなことを、OPAのDataを使って表現しています。

e.g. DataでのAPIと操作可能なRoleの紐付け例

{
  "full_method": "/spire.api.server.svid.v1.SVID/MintX509SVID",
  "allow_admin": true,
  "allow_local": true
}

この場合、/spire.api.server.svid.v1.SVID/MintX509SVIDのリクエストは、AdminまたはLocalのRoleに対して許可されている旨の結果がSPIREに返されます。
SPIREはリクエスト元のコンテキストからRoleを確認し、アクセス制御を行います。

デフォルトのData → https://github.com/spiffe/spire/blob/f8d3d833366dd2fc454c4cf10683eb53db4b976b/pkg/server/authpolicy/policy_data.json

デフォルトのPolicy → https://github.com/spiffe/spire/blob/f8d3d833366dd2fc454c4cf10683eb53db4b976b/pkg/server/authpolicy/policy.rego

デフォルトのアクセス制御の挙動は冒頭の表と変わりありませんが、(v1.0.2時点ではexperimentalな)拡張機能を使ってポリシーを拡張することも可能となっています。

cf. https://github.com/spiffe/spire/blob/main/doc/authorization_policy_engine.md#extending-the-policy

ポリシーの拡張

カスタムポリシーを使ってアクセス制御を行う場合には、自前でDataとPolicyを定義したファイルを用意する必要があります。

server {
    experimental {
        auth_opa_policy_engine {
            local {
                rego_path = "./conf/server/policy.rego"
                policy_data_path = "./conf/server/policy_data.json"
            }
        }
    }
}

PolicyはRegoで記述しますが、SPIREの実装は評価のresultとして以下のフィールドを返すことを期待しており、trueがセットされたフィールドに応じて、それぞれの内容でリクエストのコンテキストを確認します。allowtrueの場合、SPIREはリクエストを無条件に認可します。
また、SPIREではアクセス制御は各フィールドの論理和となっていることを理解しておく必要があります。resultにfalseのフィールドがあったとしても、他にリクエストが条件を満たしているtrueのフィールドが存在する場合には許可されます。

result = {
  "allow": true/false,
  "allow_if_admin": true/false,
  "allow_if_local": true/false,
  "allow_if_downstream": true/false,
  "allow_if_agent": true/false,
}
  • allow: a boolean that if true, will authorize the call
  • allow_if_local: a boolean that if true, will authorize the call only if the caller is a local UNIX socket call
  • allow_if_admin: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID with the Admin flag set
  • allow_if_downstream: a boolean that if true, will authorize the call only if the caller is a SPIFFE ID that is downstream
  • allow_if_agent: a boolean that is true, will authorize the call only if the caller is an agent.

cf. https://github.com/spiffe/spire/blob/main/doc/authorization_policy_engine.md#rego-policy

カスタムポリシーではresultが上記のルールに従ってさえいれば、他の部分は好きに定義できます。

例えばBatchCreateEntry APIに対してデフォルトのルールではAdminまたはLocalのコンテキストからの操作のみ許可されていますが、デフォルトのルールに以下のようなルールを追加すると、さらに特定のSPIFFE IDをもつユーザに対して特定のPathのEntryの登録を許可させることができます。

{
  "apis": [
    {
      "full_method": "/spire.api.server.svid.v1.SVID/MintX509SVID",
      "allow_admin": true,
      "allow_local": true
    },
    ...
    {
      "full_method": "/spire.api.server.entry.v1.Entry/BatchCreateEntry",
      "allow_admin": true,
      "allow_local": true,
      "entry_create_namespaces": [ # 追加したルール
        {
          "user": "spiffe://example.org/schedulers/finance",
          "path_namespace": "^/finance"
        }
      ]
    },
    ...
}

Policyではallowのブロックを使って追加した条件のチェックを行います。

result = {
  "allow": allow, 
  "allow_if_admin": allow_if_admin,
  "allow_if_local": allow_if_local,
  "allow_if_downstream": allow_if_downstream,
  "allow_if_agent": allow_if_agent,
}
   
# その他デフォルト部分は省略
...

# Any allow check
allow = true {
    r := data.apis[_]
    r.full_method == input.full_method

    r.allow_any
}

### ここから追加
allow = true {
    check_entry_create_namespace
}

check_entry_create_namespace {
​​    r := data.apis[_]
    r.full_method == input.full_method
    b = r.entry_create_namespaces[_]
    b.user == input.caller

    # spiffe id to be registered is in correct namespace
    re_match(b.path_namespace, input.req.entries[_].spiffe_id.path)
}
### ここまで

カスタムポリシーの動作確認

上記のポリシーの確認をするために、APIにTCP経由で接続してEntryを作成する適当なアプリケーションを用意して検証してみました。
テストアプリケーション → https://gist.github.com/hiyosi/82bd11f64a9634bf6742124df35bf4a5

まずは、テストアプリケーションで利用するためのSPIFEF IDのSVIDを発行します。(Policy Dataで定義したSPIFFE IDのもの)

root@spire-dev:/spire# ./bin/spire x509 mint \
  -spiffeID spiffe://example.org/schedulers/finance \
  -write finance
    
root@spire-dev:/spire# openssl x509 -in finance/svid.pem  -noout -text |grep "URI:"
                URI:spiffe://example.org/schedulers/finance            

発行したSVIDでは、特定のPathのEntryのみ登録できます。

root@spire-dev:/spire# ./create-entry \
  --bundle-path finance/bundle.pem \
  --key-path finance/key.pem \
  --svid-path finance/svid.pem \
  --parent-id spiffe://example.org/test \
  --spiffe-id spiffe://example.org/finance/workload-00 \
  --selector unix:uid:0 
Entry ID: 636ced63-697f-4fd4-b80c-0aca773da722
SPIFFE ID: trust_domain:"example.org" path:"/finance/workload-00"
Parent ID: trust_domain:"example.org" path:"/test"
    ​​
root@spire-dev:/spire# ./create-entry \
  --bundle-path finance/bundle.pem \
  --key-path finance/key.pem \
  --svid-path finance/svid.pem \
  --parent-id spiffe://example.org/test \
  --spiffe-id spiffe://example.org/test/workload-00 \  # /testから始まるPathは許可されていない
  --selector unix:uid:0 
2021/08/30 06:51:40 Error from Entry API: rpc error: code = PermissionDenied desc = authorization denied for method /spire.api.server.entry.v1.Entry/BatchCreateEntry

UDS経由のアクセス(=Local)の場合は、引き続きどのようなエントリも登録できます。

root@spire-dev:/spire# ./bin/spire-server entry create \
  -parentID spiffe://example.org/test \
  -spiffeID spiffe://example.org/finance/workload-01 \
  -selector unix:uid:0
Entry ID         : 16a3b9e4-6bf2-4558-b06a-0f165d46a34f
SPIFFE ID        : spiffe://example.org/finance/workload-01
Parent ID        : spiffe://example.org/test
Revision         : 0
TTL              : default
Selector         : unix:uid:0
    
root@spire-dev:/spire# ./bin/spire-server entry create \
  -parentID spiffe://example.org/test \
  -spiffeID spiffe://example.org/test/workload-01 \
  -selector unix:uid:0
Entry ID         : fbffb9dc-e0be-44e6-a8c4-899dbdea4e29
SPIFFE ID        : spiffe://example.org/test/workload-01
Parent ID        : spiffe://example.org/test
Revision         : 0
TTL              : default
Selector         : unix:uid:0

その他に公式ドキュメントでは例が提示されています。

cf. https://github.com/spiffe/spire/blob/main/doc/authorization_policy_engine.md#extending-the-policy

所感

Admin, Local, Agent, Downstream, Any といったこれまでのRoleの考え方はそのまま引き継がれており、またOPAを使ったアクセス制御になったからといって独自でRoleを定義したりできるような感じでは無さそうでした。

SPIREの仕組みやRegoによる記述がある程度理解できると、基本のRoleをベースに完全に独自ルールのポリシーを定義することもできると思いますが、漏れがないようにテストするのは大変かなという印象です。。
いきなりゼロからポリシーを定義するよりは、デフォルトのポリシーをベースに不要なAPIへのアクセスを閉じたり、制限を強めたりしていくところから始めていくのが良さそうかなと思いました。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?