SaaS でも Policy as Code ができるかやってみた - # SCuBaGear, # OPA/Rego
LayerX Fintech 事業部から三井物産デジタル・アセットマネジメント(以下、MDM)へ出向している piroshi です。
私の所属するコーポレートシステム部 / CorpOps チームは、組織内で使用するクラウドサービス (laaS から SaaSまで)に対して適切なセキュリティポリシーを策定・適用し、ITガバナンスを保証する責任を持っています。将来的には Policy as Code のアプローチで組織のセキュリティポリシーをコードとして管理し、複雑化し拡大する ITシステムのガバナンスを実現したいと考えています。
対サービス・プロダクトにおける Policy as Code の事例を目にしていますが、組織内で使用される SaaS ではまだ馴染みがないように思います。そんな中、CISA が開発・公開したツール「SCuBaGear」をはじめとする新しい取り組みがあります。
今回は、CISAが提供する SCuBaGear を使った Entra ID の監査、及び Terraform を介した Entra ID リソースのデプロイ時におけるポリシー準拠のチェックの検証をやってみたので、その過程を共有します。
Policy as Code とは
Policy as Codeとは、セキュリティポリシーをコードで表現・管理する考え方です。コード化することで、ソフトウェアエンジニアリングの恩恵が受けられるようになります。
自動化による一貫性の確保:
ポリシーをコードとして管理することで、人的ミスを減らし、一貫したセキュリティ基準の適用を保証します。これにより、組織全体でセキュリティポリシーの遵守が強化されます。文書化と透明性の向上:
ポリシーがコード形式で明記されることにより、誰でも容易にその内容を理解し、文書化することが可能になります。曖昧さの除去と明確性の向上:
ポリシーをコード化する過程で、不明瞭な表現がクリアになり、より明確で理解しやすいルールが形成されます。これは、異なる解釈による混乱を避け、ポリシー適用の一貫性を保つのに役立ちます。デプロイ時の自動チェックと頻繁な監査:
コード化されたポリシーはデプロイ時に自動的にチェックされ、違反が即座に検出されます。これにより、セキュリティ違反のリスクを最小限に抑えることができます。さらに、自動化された監査機能により、セキュリティの維持とコンプライアンス状態の常時監視が可能になります。
キャッチアップのために以下が参考になりました。
SCuBaGear を使った監査
Secure Cloud Business Applications (SCuBA)プロジェクトは、米政府機関のクラウドサービス環境を保護するためのガイダンスと機能を提供しています。現在は Microsoft 365 と Google Workspace (Preview 版) に対するセキュリティベースラインを策定し、それらの準拠状況を検証するツールが公開されています。(https://www.cisa.gov/resources-tools/services/secure-cloud-business-applications-scuba-project)
このうち、Microsoft 365 用のツールが SCuBaGear (https://github.com/cisagov/ScubaGear) です。SCuBaGear は Web API を通じてサービスの設定情報を取得し、OPA/Rego を使ってベースライン(ポリシー)への準拠状況をチェックします。結果は Human friendly な HTML ファイルのレポート、Machine readable な JSON・CSV ファイルで出力されます。ポリシーへの準拠チェックには OPA/Rego が採用されています。
評価対象サービスとベースライン
SCuBaGear で評価可能なサービスは、Microsoft Entra ID、Defender、Exchange Online、Power BI、Power Platform、SharePoint & OneDrive、Teamsです。Entra ID に対しては 8分類 30個のベースラインが用意されており、これらは GitHubページ で参照可能です。
比較対象として Micorosft Secure Score が思い浮かびますが、実際に見てみると思ったより Entra ID に関する項目は少ないんですね。SCuBaGear は政府機関での利用を想定しているため、より詳細で厳格なポリシーを提供している印象です。
では実際にツールを動かしてみましょう。今回は個人テナントの Entra ID を評価してみます。
実行環境の準備
Getting Started をみながら進めます。実行までに以下の準備をしました。
ライセンスの準備
PowerShell 5 系の実行環境用意 (-> Windows PC)
PowerShell Execution Policies の変更
モジュールのインストール
OPA 実行ファイルの取得
認証情報を含む構成ファイルの用意
詳細はリンク先を確認いただき、手間取ったポイントをこちらで共有します。
PowerShell 5 系の実行環境用意 (-> Windows PC)
ScubaGear は PowerShell スクリプトで構成されています。サポート対象がバージョンが PowerShell 5.1 のみで (マルチプラットフォーム対応の 7 系は未サポート)、Windows 上での実行が前提になっていたようだったので (パス指定で ~.exe とかでてくる)、素直に Windows PC を用意しました。
Windows 11 (AMD プロセッサ)
PowerShell 5.1 (プリインストールされていたもの)
OPA 実行ファイルの取得
ツール実行時に自動で OPA もインストールされるようなのですが、手元の環境ではそれが失敗しました。そのため手動で OPA の実行ファイルを取得しました。(手動対応手順が整備されているあたり、再現性は高そうです。)
認証の設定
SCuBaGear では「対話型認証モード (ユーザ認証 &「アクセスを許可しますか?」と承認画面が開くアレ)」と「非対話型認証モード」の認証機能が利用できます。ツールの自動実行・定期実行を想定して、非対話型認証モードを試しました。具体的には Entra ID にサービスプリンシパルを作成して証明書ベースの認証を行います。
まずはオレオレ証明書を生成 & Entra ID に登録する証明書をエクスポートします。以下の手順がそのまま使えました。
その後 Entra ID でアプリを登録し、証明書をアップロードします。
Service Principal Application Permissions & Setup で指定されているとおり、API へのアクセス権限を付与します。
設定ファイルの作成
サンプルにしたがって以下の設定ファイル(full_config.yaml)を作成しました。各パラメータの意味は README で説明されているため詳細は割愛します。
Description: YAML Configuration file with all parameters
ProductNames:
# - teams
# - exo
# - defender
- aad
# - sharepoint
M365Environment: commercial
OPAPath: .
LogIn: true
DisconnectOnExit: true
OutPath: .
OutFolderName: M365BaselineConformance
OutProviderFileName: ProviderSettingsExport
OutRegoFileName: TestResults
OutReportName: BaselineReports
Organization: {ORGANIZATION DOMAIN}
AppID: {APP ID}
CertificateThumbprint: {THUMBPRINT}
監査対象を絞るため、ProductNames には aad (= Entra ID)のみセットしています。App ID, CertificateThumbprint は 先ほどの App registrations 画面から取得できます。
ツールの実行
ダウンロードした SCuBaGear のルートディレクトリで、Invoke-SCuBA コマンドを実行します。-ConfigFilePath で設定ファイルのパスを指定しています。
Invoke-SCuBA -ConfigFilePath full_config.yaml
5 分ほどで結果が出力されました。HTML ファイルのレポート、JSON・CSV ファイルが出力されています。
PS C:\Users\hoge\Project\ScubaGear> Get-ChildItem .\M365BaselineConformance_2024_03_01_14_39_57\
ディレクトリ: C:\Users\hoge\Project\ScubaGear\M365BaselineConforman
ce_2024_03_01_14_39_57
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2024/03/01 14:40 IndividualReports
-a---- 2024/03/01 14:40 22378 BaselineReports.html
-a---- 2024/03/01 14:40 365888 ProviderSettingsExport.json
-a---- 2024/03/01 14:40 8065 TestResults.csv
-a---- 2024/03/01 14:40 19217 TestResults.json
※ サンプルファイルも公開されているので、実ファイルが見たい方はこちらからどうぞ … https://github.com/cisagov/ScubaGear/tree/main/PowerShell/ScubaGear/Sample-Reports
JSON ファイルの中身はこんな感じです。ポリシー ID や設定をチェックするために実行したコマンドとその結果が含まれています。
[
{
"ActualValue": "",
"Commandlet": [
"Get-MgBetaSubscribedSku",
"Get-PrivilegedUser"
],
"Criticality": "Shall",
"PolicyId": "MS.AAD.7.3v1",
"ReportDetails": "0 admin(s) that are not cloud-only found",
"RequirementMet": true
},
{
"ActualValue": [
],
"Commandlet": [
],
"Criticality": "Shall/Not-Implemented",
"PolicyId": "MS.AAD.3.3v1",
"ReportDetails": "This product does not currently have the capability to check compliance for this policy. See \u003ca href=\"https://github.com/cisagov/ScubaGear/blob/v1.1.1/PowerShell/ScubaGear/baselines/aad.md#msaad33v1\" target=\"_blank\"\u003eSecure Configuration Baseline policy\u003c/a\u003e for instructions on manual check",
"RequirementMet": false
},
...
]
Critycality が SHOULD かつ非準拠だったベースラインを取得してみます。構造化データは扱いやすいですね、嬉しみ。
PS C:\Users\hotge\Project\ScubaGear> $json | Where-Object { $_.Criticality -eq 'Should' -and $_.RequirementMet -eq $false }
ActualValue : @{all_allow_invite_values=System.Object[]}
Commandlet : {Get-MgBetaPolicyAuthorizationPolicy}
Criticality : Should
PolicyId : MS.AAD.8.2v1
ReportDetails : Permission level set to "everyone" (authorizationPolicy)
RequirementMet : False
ActualValue : {}
Commandlet : {Get-MgBetaIdentityConditionalAccessPolicy}
Criticality : Should
PolicyId : MS.AAD.3.7v1
ReportDetails : 0 conditional access policy(s) found that meet(s) all requirements. <a href='#caps'>View all CA poli
cies</a>.
RequirementMet : False
ActualValue : {}
Commandlet : {Get-MgBetaIdentityConditionalAccessPolicy}
Criticality : Should
PolicyId : MS.AAD.3.8v1
ReportDetails : 0 conditional access policy(s) found that meet(s) all requirements. <a href='#caps'>View all CA poli
cies</a>.
RequirementMet : False
ActualValue : {Application Administrator, Cloud Application Administrator, Exchange Administrator, Hybrid Identity
Administrator...}
Commandlet : {Get-MgBetaSubscribedSku, Get-PrivilegedRole}
Criticality : Should
PolicyId : MS.AAD.7.9v1
ReportDetails : 7 role(s) without notification e-mail configured for role activations found:<br/>Application Adminis
trator, Cloud Application Administrator, Exchange Administrator, Hybrid Identity Administrator, Priv
ileged Role Administrator, SharePoint Administrator, User Administrator
RequirementMet : False
個人環境とはいえ、ちょっと設定見直そうかなと思う契機になりました。この後めちゃくちゃ管理コンソールをポチポチしました。
Terraform と OPA/Rego によるポリシー準拠チェック
SCuBaGear は対象サービスの「現在の状態」を監査するツールでした。加えて、システム管理者は「設定の変更時」にもポリシーへの準拠を気にする生き物です。資産に対するガバナンス施策を考えるときには「Scheduled Governance」と「Event Driven Governance」の両方が必要です(これらはチーム内の造語です)。ここでも OPA/Rego を使ったチェックができると嬉しいです。
ということで、今度は Entra ID を Terraform で管理する運用を想定し、デプロイ前に OPA/Rego を使ってOPA/Regoを使用してポリシー準拠をチェックできるか試してみました。ポリシー違反を未然に防止できれば、セキュリティとガバナンスの向上が期待できます。具体的な手順としては、Terraform での設定変更を計画し、変更内容を OPA によるチェックにかけることで、ポリシーに準拠しているかを確認します。
なお、OPA/Rego の詳細・使い方には触れらていません。こちらの記事が大変参考になったので紹介させていただきます。
また Entra ID の Terraform Custome Provider の使い方はこちらで確認できます。
① 認証要素の指定
SCuBaGear のベースライン MS.AAD.3.5v1 をサンプルにします。SMS, 音声通話、メールによる OTP による認証は無効化せねばならぬ、というものです。
Terraform では azuread_authentication_strength_policy で利用可能な認証方式を宣言できます。allowed_combinations に sms, voice を含めておきます。(Email OTP を指すパラメータは見つからなかったので割愛しています。)
terraform {
required_providers {
azuread = {
source = "hashicorp/azuread"
version = "~> 2.47"
}
}
}
provider "azuread" {
tenant_id = "{TENANT ID}"
}
resource "azuread_authentication_strength_policy" "example2" {
display_name = "Example Authentication Strength Policy"
description = "Policy for demo purposes with all possible combinations"
allowed_combinations = [
"fido2",
"password",
"deviceBasedPush",
"temporaryAccessPassOneTime",
"federatedMultiFactor",
"federatedSingleFactor",
"hardwareOath,federatedSingleFactor",
"microsoftAuthenticatorPush,federatedSingleFactor",
"password,hardwareOath",
"password,microsoftAuthenticatorPush",
"password,sms",
"password,softwareOath",
"password,voice",
"sms",
"sms,federatedSingleFactor",
"softwareOath,federatedSingleFactor",
"temporaryAccessPassMultiUse",
"voice,federatedSingleFactor",
"windowsHelloForBusiness",
"x509CertificateMultiFactor",
"x509CertificateSingleFactor",
]
}
terraform plan を実行、OPA で検査するために結果を plan file (tfplan)を出力します。OPA に input するデータは JSON フォーマットである必要があるためデータ形式を変換しておきます。
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
tfplan.json には以下のように変更内容が記録されています。resource_changes に適用されるパラメータを見つけました。
cat tfplan.json
{
...
"resource_changes": [
{
"address": "azuread_authentication_strength_policy.example2",
"mode": "managed",
"type": "azuread_authentication_strength_policy",
"name": "example2",
"provider_name": "registry.terraform.io/hashicorp/azuread",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"allowed_combinations": [
"deviceBasedPush",
"federatedMultiFactor",
"federatedSingleFactor",
"fido2",
"hardwareOath,federatedSingleFactor",
"microsoftAuthenticatorPush,federatedSingleFactor",
"password",
"password,hardwareOath",
"password,microsoftAuthenticatorPush",
"password,sms",
"password,softwareOath",
"password,voice",
"sms",
"sms,federatedSingleFactor",
"softwareOath,federatedSingleFactor",
"temporaryAccessPassMultiUse",
"temporaryAccessPassOneTime",
"voice,federatedSingleFactor",
"windowsHelloForBusiness",
"x509CertificateMultiFactor",
"x509CertificateSingleFactor"
],
....
}
Rego ファイルで SMS, Voice, Email OTP の有効化を禁止するポリシーを書きます。input される JSON データからチェック対象のデータをフィルタし、password または sms の文字列が含まれていればテストに失敗します。
package main
deny[msg] {
some i
input.resource_changes[i].type == "azuread_authentication_strength_policy"
password_included := input.resource_changes[i].change.after.allowed_combinations[_] == "password"
sms_included := input.resource_changes[i].change.after.allowed_combinations[_] == "sms"
password_included
sms_included
msg = "ポリシー違反: 'azuread_authentication_strength_policy' リソースの 'allowed_combinations' に 'password' と 'sms' が含まれています。"
}
OPA コマンドで検査します。Fail と
opa eval --data policy/policy.rego --input tfplan.json --explain full "data.main" -f pretty
...
{
"deny": [
"ポリシー違反: 'azuread_authentication_strength_policy' リソースの 'allowed_combinations' に 'password' と 'sms' が含まれています。"
]
}
また、Conftest (https://www.conftest.dev/) によるチェックも可能です。こちらの方が結果がわかりやすいです。
conftest test tfplan.json
FAIL - tfplan.json - main - ポリシー違反: 'azuread_authentication_strength_policy' リソースの 'allowed_combinations' に 'password' と 'sms' が含まれています。
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
tf ファイルから sms, voice の設定をコメントアウトして terraform plan を再実行します。
resource "azuread_authentication_strength_policy" "example2" {
display_name = "Example Authentication Strength Policy"
description = "Policy for demo purposes with all possible combinations"
allowed_combinations = [
"fido2",
"password",
"deviceBasedPush",
"temporaryAccessPassOneTime",
"federatedMultiFactor",
"federatedSingleFactor",
"hardwareOath,federatedSingleFactor",
"microsoftAuthenticatorPush,federatedSingleFactor",
"password,hardwareOath",
"password,microsoftAuthenticatorPush",
# "password,sms",
"password,softwareOath",
# "password,voice",
# "sms",
# "sms,federatedSingleFactor",
"softwareOath,federatedSingleFactor",
"temporaryAccessPassMultiUse",
"voice,federatedSingleFactor",
"windowsHelloForBusiness",
"x509CertificateMultiFactor",
"x509CertificateSingleFactor",
]
}
今度は検証に pass しました。
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json
1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions
② ユーザが所属するグループの制限
「開発部門は本番データへのアクセスを禁止」という組織内のルールを想定し「Ops-Division 部門のユーザのみ、本番データへのアクセ権限付与を目的とした prd-access グループに追加 ok」というポリシーを考えます。
Terraform でユーザとグループを宣言します。開発部門の user01, 運用部門の user02 を prd-access グループのメンバとして定義します。
resource "azuread_user" "example01" {
user_principal_name = "user01@example.com"
display_name = "user01"
department = "Dev-Division"
}
resource "azuread_user" "example02" {
user_principal_name = "user02@example.com"
display_name = "user02"
department = "Ops-Division"
}
resource "azuread_group" "prd_access" {
display_name = "prd-access"
security_enabled = true
members = [
azuread_user.example01.object_id,
azuread_user.example02.object_id,
]
}
terraform plan を実行して結果を確認します。configuration.root_module.resources.members に、グループのメンバ情報を見つけました。
...
"root_module": {
"resources": [
{
"address": "azuread_group.prd_access",
"mode": "managed",
"type": "azuread_group",
"name": "prd_access",
"provider_config_key": "azuread",
"expressions": {
"display_name": {
"constant_value": "prd-access"
},
"members": {
"references": [
"azuread_user.example01.object_id",
"azuread_user.example01",
"azuread_user.example02.object_id",
"azuread_user.example02"
]
},
"security_enabled": {
"constant_value": true
}
},
"schema_version": 0
},
...
Rego で以下のとおりポリシーを書きます。
deny[msg] {
user := input.configuration.root_module.resources[_]
user.type == "azuread_user"
not user.expressions.department.constant_value == "Ops-Division" # 'Ops-Division' 以外の部門に属する
user_display_name := user.expressions.display_name.constant_value
group := input.configuration.root_module.resources[_]
group.type == "azuread_group"
group.address == "azuread_group.prd_access"
group_display_name := group.expressions.display_name.constant_value
# ユーザーがグループのメンバーであるかどうかを確認
user_id := user.address
member_ref := group.expressions.members.references[_]
user_id == member_ref
msg := sprintf("User '%s' does not belong to the 'Ops-Division' department and should not be a member of the '%s' group.", [user_display_name, group_display_name])
}
conftest を実行し、user01 が prd-access グループのメンバであるため失敗することを確認しました。
% conftest test tfplan.json
FAIL - tfplan.json - main - User 'user01' does not belong to the 'Ops-Division' department and should not be a member of the 'prd-access' group.
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
user01 を同グループのメンバーから除外して、再度 terraform plan を実行します。
resource "azuread_group" "prd_access" {
display_name = "prd-access"
security_enabled = true
members = [
# azuread_user.example01.object_id,
azuread_user.example02.object_id,
]
}
今度は pass しました。
% conftest test tfplan.json
1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions
まとめ
SaaS への Policy as Code の取り組みとして、SCuBaGear, Terraform と OPA/Rego を使ってみました。SCuBaGear はセキュリティベースラインの策定もありがたいですが、Policy as Code を採用した取り組みを一歩進めた感があります。PowerShell 5.1 以外での動作対応が普及を加速させると思います。また Terraform でもポリシー準拠のチェックが可能とわかったので、CI/CD に組み込んで自動化できる未来が見えてきました。そのためにも、一日も早く全ての設定項目が provider に反映されてほしいです。
「ポリシーはシンプルに」が望ましいですが、現実には業界のガイドラインや法令対応など多数の制約が存在します。人間の認知機能による準拠チェックは限界があるので、ポリシーもデジタル化していきたいものですね。Policy as Code のキーワードは今後も追っていきます。