LoginSignup
6
2

More than 3 years have passed since last update.

Open Policy Agentの活用事例 (AWS上での稼働)

Posted at

Hitachi America, Ltd. R&D プラットフォームラボラトリの 大崎 裕之です。現在サンタクララで活動しております。

オープンソースのポリシーエンジンであるOpen Policy Agent (以下OPA) は、近年普及が進み、Cloud Native Computing Foundation (CNCF) でGraduationとなりました。OPAは、Kubernetesなどのインフラのアクセス制御に活用できるだけでなく、Webサービスの認可にも活用できます。本記事では、Webサービス向けのOPAの活用事例について紹介します。

本事例の特徴

OPAをAmazon Web Services (以下AWS) 上で稼働し、Webサービスの認可処理を行わせるという活用事例を紹介します。
本事例では、AWS Lambda/API Gatewayを使って稼働しているWebサービスを題材とします。

  • 事前にOPAをAWS Lambdaで稼働させておきLambda Authorizerに設定しておく
  • Webサービスは、ユーザからリクエストを受信。Webサービスへのリクエストをフックし、OPAで認可の処理を開始
  • OPAを稼働させるために、Lambda内のGo言語のプログラムでrego.Newrego.PrepareForEvalなどのOPAのAPIを使用

上記の構成を用いて、Webサービスへのリクエストに応じOPAの認可処理が実現できました。

1. OPAを用いたWebサービスの認可

AWS上の構成について説明する前に、Webサービスの認可とOPAの働きについて触れます。

1-1. WebサービスとOPAの処理の流れ

まず、Webサービスへのリクエストの処理とその間に呼び出されるOPAの処理の全体の流れを説明します。

ユーザは、Webサービスのアプリケーションに対してリクエストを送付します。通信手段としては、https://<ホスト名>/<パス>といったアドレスにJSONなどのデータを送信します。Webサービスは、このリクエストを実行してもよいか、OPAに認可の判断を委ねます。

OPAは、どのようなユーザがアクセスしようとしているのかに応じて、認可の判断を実施し、結果としてtrueないしfalseを返答します。

Webサービスは、この返答を受け取り、元のユーザリクエストに対する返答を送信します。もし認可の結果がfalseであれば、何も処理をせずに「許可されませんでした」という返答を送信します。また、認可の結果がtrueであれば、リクエストで要求されたアプリケーションの処理を実行し、その結果を返答します。

1-2. OPAの働き

次に、OPAの視点で処理を説明します。OPAは、以下の図のように、データなどを読み込んで結果を返すことで処理を行うことができます。処理は大きく2つのフェーズがあります。

  • フェーズ(a) 初期化 (上図の右下)
    • 事前に定義してある[A]ポリシーと、必要なら[B]JSONデータを参照し、フェーズ(b)の準備をします。
  • フェーズ(b) リクエストごとの処理 (上図の左)
    • [1~4]ユーザからのリクエストに基づいて、アプリケーションからインプットをもらいます。OPAはインプットに応じて認可のアウトプット(truefalse)を返答します。

2つのフェーズのうち、フェーズ(a)初期化は1度だけ実行し、その後、フェーズ(b)リクエストごとの処理をリクエストのたびに繰り返し実行します。
各フェーズをAWS上で実行する方法は、後ほど2-3にてソースコードに照らしながら説明します。

1-3. OPAで使用するポリシーファイル(サンプル)

前の「OPAの働き」で登場した、OPA実行に必要な[A]ポリシーファイルについて説明します。

以下のexample.regoファイルは、OPA公式ドキュメントの例に記載されたポリシーです。最後の検証でもこのポリシーファイルを使用します。ポリシーファイルの中身を追ってみます。

package authz

# This was orignally published on https://www.openpolicyagent.org/docs/latest/policy-testing/

allow {
    input.path == ["users"]
    input.method == "POST"
}

allow {
    some profile_id
    input.path = ["users", profile_id]
    input.method == "GET"
    profile_id == input.user_id
}

上記のポリシーにはallow {}が複数あることがわかります。これが、OPAで 「ルール」 と呼ばれるもので、関数のように内部で定義された処理に基づいてtrueもしくはfalseを返します。ルールは、true/false以外の値、配列やオブジェクトを返すこともできます。
OPAは同一名称の複数のルールを定義可能 です。一般的なプログラムの関数では同一の名称で複数定義を記述できないので、この点はOPAの重要な特徴です。allow {}を定義した複数のルールのうち いずれかがtrueを返すと、allowの返り値はtrue になります。
この特性により、OPAは後からどんどんルールを足すことができるようになっています。

allowの中身は、inputで表される入力JSONオブジェクトからpath, method, user_idを取り出し、条件判定します。2つ目のallowの中では、以下2つの条件をアンドで判定しています。

  • input.method == "GET"によりmethodの値が"GET"であるか確認
  • input.path = ["users", profile_id]profile_idを取り出し、profile_id == input.user_idによりそれがuser_idと一致するかを確認

このポリシーファイルを後でLambdaから使用できるよう、S3に格納します。例として、バケット名を"opa-lambda-authorizer-test"として説明します。

2. AWS上の構成

ここからは、OPAをAWSで稼働するのに必要なコンポーネントの構成について説明します。

2-1. OPAの実行方法のバリエーション

AWS上でOPAを稼働するには、以下のような方法が考えられます。

  • EC2上でVMを稼働する
  • EKSなどのコンテナサービス上でコンテナを稼働する
  • Lambdaなどのサーバレスサービス上でプログラムを起動する

このうちOPAをAWS Lambda上で稼働する方法今回は採用します。また、プログラム言語にはGo言語を用います。

2-2. 使用するAWSの各コンポーネントと設定

AWS Lambdaを使った構成は上図のようになります。

AWS API GatewayはWebサービス向けのエンドポイントを持ち、そのAuthorizerとしてLambda Authorizerを設定します。Lambda AuthorizerとはAPI Gatewayの機能の一つで、API Gateway上にあるAPIへのアクセス制御をLambdaに委託するというものです。この機能のメリットは、自由にアクセス制御ロジックを変えられることです。

AWS Lambdaには、以下の2つのプログラムを動かします。

  • i) Webサービス用
  • ii) Go+OPA用 (前述のLambda Authorizerで使う)

AWS S3には、OPAが必要とする[A]ポリシーファイルを格納しておきます。
この構成を実現するには、Serverless Frameworkを使って、以下のような設定ファイル(serverless.yaml)をデプロイすることができます。

service: opa-lambda-authorizer

provider:
  name: aws
  runtime: go1.x
  stage: dev
  iamRoleStatements:
    - Effect: 'Allow'
      Action:
        - 's3:GetObject'
      Resource: "arn:aws:s3:::*/*"

package:
  exclude:
    - ./**
  include:
    - ./bin/**

functions:
  getuser:
    handler: bin/getuser
    events:
    - http:
        path: users/{user_id}
        method: get
        authorizer:
          type: request
          name: auth
          resultTtlInSeconds: 0
          identitySource: method.request.header.Authorization
  auth:
    handler: bin/auth

この例では、以下の2つを構築する設定が含まれています。

  • Webサービス向け getuser
    • パスが/users/{user_id}であるエンドポイントがAPI Gateway上に作成される
    • そこから呼び出されるWebサービスの実態はbin/getuserファイルを実行したLambda。
  • OPA向け auth
    • authというLambda関数が作成される
    • 内部ではbin/authが実行される。
    • これはエンドポイントが無くユーザからは直接呼び出せない。代わりに、先ほど説明したLambda Authorizerの機能が呼び出す。

このgetuserの設定の中で、authorizerの部分でLambda関数authが前述のLambda Authorizerとして設定されています。さらに、Authorizerの結果のキャッシングを無効化するためのresultTtlInSeconds: 0や、アイデンティティ情報としてLambda Authorizerが使用するヘッダを指定するidentitySourceが設定されています。これによって、Webサービスgetuserがユーザから呼び出されるたびに、authに認可の問い合わせを行います。

2-3. OPAを呼び出すGo言語プログラム

では、OPAを実行するGo言語のプログラムbin/auth.goの中身を見ていきます。

フェーズ(a) 初期化

Go言語のソースコードにおいて、以下の通りOPAを初期化していきます。実例を以下に示します。

メインポイント: S3からポリシーファイルを読み出し、rego.Newで初期化し、rego.Prepare関数で準備処理をします

package main

import (
    "strings"
    "context"
    "log"
    "io/ioutil"
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/open-policy-agent/opa/rego" // STEP 1 **********
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/aws"
)

var query rego.PreparedEvalQuery

func main() {
    prepare() 
    lambda.Start(handleRequest)
}

func prepare() {
    ctx := context.Background()
    // step 2 **********
    policyfile := fetchs3("opa-lambda-authorizer-test", "example.rego")

  // step 3 **********
    var err error
    query, err = rego.New( // <-- MAIN POINT ****************
    rego.Query("x = data.authz.allow"),
    rego.Module("example.rego", policyfile),
    ).PrepareForEval(ctx) // <-- MAIN POINT ****************

    if err != nil {
        panic(err)
    }
}

func fetchs3(bucketName string, objectName string) (string) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    svc := s3.NewFromConfig(cfg)
    ctx := context.Background()
    resp, err := svc.GetObject(ctx, &s3.GetObjectInput{
            Bucket: aws.String(bucketName),
            Key:    aws.String(objectName),
    })
    if err != nil {
        panic(err)
}
    s3objectBytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
            panic(err)
    }
    return string(s3objectBytes)
}

まずは、"github.com/open-policy-agent/opa/rego"をimportします (ステップ1)。これによって、このソースコード内でOPAの各APIが呼び出せます。

次に、Lambdaの初期化時に、prepare()を実行し、その中で前節で説明したポリシーファイルexample.regoをS3からダウンロードします (ステップ2)。上の例では、この処理をfetchs3という名で関数化してあります。
最後に、ポリシーファイルを使いOPAを初期化していきます (ステップ3)。

  • rego.NewによりOPAのインスタンスを作成する
  • rego.Moduleでポリシーファイルexample.regoをOPAに読み込ませる
  • PrepareForEvalを呼ぶことで、ポリシーファイルを内部でパースして実行準備ができた状態のrego.PreparedEvalQueryインスタンスを作成する

フェーズ(b) リクエスト処理時

リクエスト処理時のGo言語アプリケーションの実装は、以下のようになります。

メインポイント: Eval関数でポリシーファイルを使った認可処理の結果を取得する

// STEP 4 **********
func handleRequest(event events.APIGatewayCustomAuthorizerRequestTypeRequest) (events.APIGatewayCustomAuthorizerResponse, error) {
    user_id := event.Headers["Authorization"]
    path := event.Path
    method := event.RequestContext.HTTPMethod
    log.Println("UserID: "+user_id)
    log.Println("Path: "+path)
    log.Println("Method: "+method)

    effect := "Deny" // Default value
    if checkOpaExample(path, method, user_id) {
        effect = "Allow"
    }
    return generateIAMPolicy(user_id, effect), nil

}

// STEP 5 **********
func checkOpaExample(path string, method string, user_id string) (result bool) {
    ctx := context.Background()
    paths := strings.Split(path, "/")[1:] // /users/bob -> ["users", "bob"]
    input := map[string]interface{}{
        "method": method,
        "path": paths,
        "user_id": user_id,
    }
    results, err := query.Eval(ctx, rego.EvalInput(input)) // <-- MAIN POINT ****************

    if err != nil {
        log.Println("Error:", err)
    } else if len(results) == 0 {
        log.Println("Error: results are empty")
    } else if result, ok := results[0].Bindings["x"].(bool); !ok {
        log.Println("Error: binding result is not bool")
    } else {
        log.Println("Result:", result)
        return result
    }
    return false
}

// STEP 6 **********
func generateIAMPolicy(profileId string, effect string) events.APIGatewayCustomAuthorizerResponse {
    rtn := events.APIGatewayCustomAuthorizerResponse{
        PrincipalID: profileId, 
        Context: map[string]interface{}{},
    }

    if effect != "" {
        rtn.PolicyDocument = events.APIGatewayCustomAuthorizerPolicy{
            Version: "2012-10-17",
            Statement: []events.IAMPolicyStatement{
                {
                    Action:   []string{"*"},
                    Effect:   effect,
                    Resource: []string{"*"},
                },
            },
        }
    }
    return rtn
}

上記ソースコードでは以下を順に実行します。

  • Webサービスがユーザから呼び出されるたびに、bin/auth内ののハンドラhandlerRequest()が呼び出されます。この中で、OPAの認可判定処理を呼び出します(ステップ4)。具体的には、初期化時に作成したqueryEval関数を呼び出します。その時の引数に、OPAの認可処理のインプットを入れる必要があります。上記ソースコードでは、handleRequestの引数で渡されるevents.APIGatewayCustomAuthorizerREquestTypeRequestからインプットに必要な情報を取り出します。たとえば、Authorizationヘッダを取り出してuser_idに格納しています。また、Pathフィールドを取り出しpathに格納しています。同様にmethodも格納します。
  • これらを関数checkOpaExample内でinputというJSONに変換し、これを引数に指定し Eval関数 を呼び出します(ステップ5)。
  • OPAからの結果はresult[0].Binding["x"]に格納されているので、これがtruefalseかに応じてgenerateIAMPolicy(ステップ6)にてLambda Authorizerのフォーマットで出力を返答し、このハンドラhandlerRequest()を終了します。

以上のファイルをauth.goとして保存し、go buildを用いてコンパイルし、bin/authを生成します。

2-4. AWS上へのデプロイメント

これまで説明したファイル群を以下のように同じフォルダに格納します。

├── bin
│   ├── auth (バイナリ)
│   └── getuser (バイナリ、割愛)
├── policy
│   └── example.rego (1-3で説明。これはS3にアップロード済み)
├── serverless.yml (2-2で説明)
└── src
    ├── auth.go (2-3で説明)
    └── getuser.go (割愛)

これまで説明していなかったsrc/getuser.goはWebサービスのソースコードであり、それをコンパイルしてbin/getuserを生成します。今回このサンプルのWebサービスの処理は非常にシンプルで、{"id": <ユーザのID>}というJSONを返すだけとなっております。このWebサービス自体のソースコードには認可処理は含まれません。OPAとLambda Authorizerがその役割を担ってくれるためです。ソースコードの内容は割愛させていただきます。

このフォルダ内で、以下のコマンドを実行します。

serverless deploy

これにより、AWS上のAPI Gateway/Lambda が自動で作成されます。

作成できたWebサービスのURLは以下のコマンドで確認することができます。

serverless info
(中略)
endpoints:
  GET - https://<xxxxxxxx>.execute-api.<region>.amazonaws.com/dev/users/{user_id}

WebサービスのURLは、<xxxxxxxx>.execute-api.<region>.amazonaws.com/devのような形となります。

3. 検証: Webサービスの認可処理の確認

ここまで説明したAWSの構成により、ユーザから見てOPAがどのように機能するかを検証してみます。

3-1. リクエストが認可されるケース(OPAがtrueを返す場合)

以下のコマンドを実行し、Webサービスに実際にアクセスすることで、OPAの認可処理を検証することができます。

curl -s -X GET -H "Authorization:bob" https://<xxxxxxxx>.execute-api.<region>.amazonaws.com/dev/users/bob
{"id":"bob"}
curl -s -X GET -H "Authorization:abcde" https://<xxxxxxxx>.execute-api.<region>.amazonaws.com/dev/users/abcde
{"id":"abcde"}

一回目のリクエストは、/users/bobというパスに"GET"リクエストを送信しており、そのときのヘッダにユーザの情報Authorization:bobを同封しています。このリクエストの結果、正しくアプリケーションの出力{"id":"bob"}を取得できている事がわかります。これは、OPAのポリシーファイルがtrueを返答したためです。二回目のリクエストも同様です。共通点は、/users/より後のIDがAuthorization:に付記したユーザ名と同一であることです。この条件を満たせば、1-3で説明したexample.regoポリシーファイルの最後の行profile_id == input.user_idの判定結果がtrueとなります。

3-2. リクエストが拒否されるケース(OPAがfalseを返す場合)

以下のコマンドを実行し、別のリクエストを送信してみます。

curl -s -X GET -H "Authorization:abcde" <xxxxxxxx>.execute-api.<region>.amazonaws.com/dev/users/bob
{"Message":"User is not authorized to access this resource with an explicit deny"}

今度は返事が変わり、アプリケーションの出力の代わりにエラーメッセージが返ってきています。これは、OPAのポリシーファイルがfalseを返したことを意味します。/users/bobなのに対し、ヘッダはAuthorization:abcdeとなっており、example.rego内のprofile_id == input.user_idの条件に合致しないためです。このように、正しいヘッダやパスをリクエストしなければ、上記のエラーメッセージが出力されるようになります。

まとめ

本記事では、Webサービスを題材とし、AWS上で稼働するOPAをLambda Authorizerに設定し、Webサービスへのリクエストをフックし認可処理する事例を紹介しました。
OPAのポリシーファイルで判定した認可処理結果に基づいてWebサービスへのリクエストが続行ないし拒否されました。

今回のAWS上の構成のメリットは、Webサービスのアプリケーション開発者が認可処理を実装しなくても、そのAPI Gatewayの層でその認可機能を後から付け足せばよく、アプリケーション開発者がコアロジック実装に集中できるということだと考えます。また、同じ認可処理を複数のアプリケーションから利用することも容易になります。

また、OPAを使った効果については、1-3で説明したallow{}のようなポリシーのロジックを柔軟に拡張できる特徴から、利用シーンに合わせてポリシーを適宜追加したり差し替えたりでき、認可処理側の運用も柔軟性が向上することが考えられます。
今後も、OPAなどのオープンソースのツールを活用したデジタルソリューションの構築方法を発信して参りたいと考えております。

参考文献

[1] Open Policy Agent, https://www.openpolicyagent.org/
[2] OPAのGo言語向けAPI, https://pkg.go.dev/github.com/open-policy-agent/opa/rego
[3] OPA公式ドキュメント "Integrating with the Go API", https://www.openpolicyagent.org/docs/latest/integration/#integrating-with-the-go-api
[4] OPA公式ドキュメント "Policy Testing", https://www.openpolicyagent.org/docs/latest/policy-testing/
[5] AWS, "Use API Gateway Lambda authorizers", https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html

謝辞

本記事執筆にあたっては、以下の皆様をはじめとしたくさんの方のご協力をいただきました。この場を借りて御礼申し上げます。

・株式会社日立製作所 サービス&プラットフォームビジネスユニット ITプロダクツ統括本部 富田隆彦さん
・株式会社日立製作所 サービス&プラットフォームビジネスユニット ITプロダクツ統括本部 早川裕志さん
・株式会社日立製作所 研究開発グループ 肥村洋輔さん

6
2
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
6
2