セキュリティ重視の大企業に勤めたことがある方なら、スピードを求める開発者とセキュリティを優先する組織の姿勢は相容れないものであるとご存知でしょう。双方は優先順位も視点も大きく異なっており、このような衝突が大事に発展してしまうケースも珍しくありません。

config-policies-image4.png

現実は上記の台本のようにドラマチックであるとは限りませんが、管理と権限委譲を巡る争いは現実に起きていることです。”職務分掌” や “信頼せよ、されど確認せよ” と言い換えてもいいでしょう。世知辛いようですが、どのような組織でも、重要システムへのアクセスは保護しなければならないのです。しかし、従来のように手作業で綿密なレビューを行ったり、全ユーザーを人間が厳しく見張ったりしていては、現在の環境についていけません。

この記事では、CircleCI の柔軟な Configuration As Code (権限委譲) と新公開の設定ファイルのポリシー (管理) 機能を組み合わせて、SecOps チームが予防策の確立、アクセスの制御、権限委譲の実現を進めるうえで必要なツールを整える方法について説明します。

承認体制の見直し

設定ファイルポリシーでは、全社レベルで施行されている証拠保全、厳格な変更管理、セキュアコーディング、および IT リソースの有効活用についての規則をコード内に定めることができます。たとえば、以下のような設定を行えます。

  • コードのレビューと変更の承認を必須にする
  • 機密情報へのアクセスを制限する
  • コンプライアンスまたはセキュリティ関連のジョブ (SAST や DAST など) を必須にする
  • SSH デバッグなどの機能を制限する
  • デプロイターゲットや特定のランナー、リソースクラスへのアクセスを管理する

最初の 2 つは、冒頭の台本に登場した Diana DevOps と Porter Protector の争いの核心でもあります。Porter が求めているのは、本番環境の認証情報の使用について、悪意のある行為や浅はかな行動を防止するためのレビューが必ず行われているかどうかを把握することです。Diana のチームは、Porter に対して、自分たちが責任を真剣に受け止めていると示さなくてはなりません。統括チームの手作業でのレビューを待っている時間はないからです。

こうした課題を、Porter と Diana が CircleCI の力を借りて協同で解決するにはどうすればよいのでしょうか?

最初にすべきことは、うわさや対人関係に左右され、間違いが起こりやすく時間もかかる手作業での承認プロセスを撤廃することです。そこで、Porter は Diana のチームの意見を基に、以下のような新しいポリシー案を作成しました。

本番環境の認証情報はすべて、一元的なシークレット管理システムに保存しなければならない。アプリケーションチームは、部署に割り当てられた `APPTEAM-prod` コンテキストを使用することで、これらの認証情報にアクセスできる。アクセスは、実行予定のリポジトリブランチが承認済みであり、書面による承認を受けているパイプラインのみに付与する。

このポリシー案は、Diana にとって好ましいものです。Porter とのやり取りが必要な場合でも、1 回だけで済むからです。プロセスを一貫して遵守するうえで、ツールや自動化を利用することも認められています。また、Porter のチームにとっては、自動化によってどのような時でもポリシーの遵守が徹底されていると確信できます。

  • コードのレビューと変更の承認を必須にする - 達成
  • 機密情報へのアクセスを制限する - 達成

4 つの手順で権限のシフトレフトを安全に実現する

CircleCI は、シフトレフトというアイデアを固く信じています。プロセスにおいて、早期にリスクに対処可能であるほど、低いコストでリスクを軽減しやすくなります。

シフトレフトを効果的に進めるには、リリース直前 (最終段階) になって価値実現を止める事態が生じないように、業務をデプロイから逆算して構築すること、およびリスクの解消を行えるチェックポイント候補をすべて検討することが必要です。

しかし、開発者やアプリケーションチームに付与する権限、アクセス権、責任を増やす行為には本質的なリスクや危険性があります。そのため、多くの場合、難しい議論を何度も交わさなければなりません。

実際の話し合いにおいては、たいていの組織にとっておなじみの権限委譲と管理を巡って論争が交わされます。SecOps チームが、後からプロセスにルールを適用することができなくなったとしたら、プロセスでルールが常に遵守されているかどうかを把握するにはどうすればよいのでしょうか?

以下のセクションでは、一元的な管理と検証の体制を崩すことなく、シフトレフトを実現するための具体的な 4 ステップを紹介します。各ステップは前のステップに基づいており、すべてのステップを組み合わせることで、ソフトウェアデリバリープロセスの権限委譲を大きく進めると同時にルールの遵守も徹底できます。

コンフィグポリシー フローチャート

ステップ 1: ブランチを保護する

重要な手順の 1 つは、まずコミットとマージに関する規律を定めることです。みなさんは、main/デフォルト (つまり本番用) ブランチへのプッシュを行えるメンバーと、プッシュに必要な手順を定めているでしょうか?

本番環境への直接のコミットを特定の個人や特権持ちのロールにまかせている場合、障害、遅延、侵害といった重大なリスクがパイプラインに潜んでいることになります。チームがどれほど信頼の厚い存在であったとしても、だれかを信じることは本質的に危険です。つまり、プロセスの構築が欠かせません。

  1. ブランチ権限を設定して、/production/main (デフォルト/本番用) ブランチを保護する。このブランチには、管理者も含めてだれもプッシュを行えないようにしましょう。
  2. /production/main (デフォルト/本番用) ブランチに対する変更は、マージリクエストまたはプルリクエストで行うことを義務づける。 これにより、変更の対象、実行者、時期、理由を記録できます。
  3. マージのチェックや自動検証を利用して、プロセスでの主要な要件の遵守を徹底する。たとえば、以下の要件が考えられます。
    • 承認者数が下限値を超えていること
    • ビルドとテストが成功していること
    • コンプライアンスやライセンスのスキャンで異常がないこと
    • マネージャーから承認を受けていること
    • Sセキュリティジョブを実装していること

このステップが 1 番目である理由は、これが管理の基礎、そして信頼関係の構築や権限委譲の実現を進めるための土台になるからです。

実際の手順

マージチェックとブランチ権限は、CI/CD パイプラインを活用して VCS レベルで実装します。

CircleCI は複数の変更ソースに対応していますが、これらの管理機能の使用方法は VCS プロバイダーごとに異なります。詳しくは、 GitLab, GitHub, Bitbucket の各ページをご覧ください。

ステップ 2: シークレット管理を一元化する

シークレットは、私も含めてだれもが利用しているでしょう。私の場合、パスワードも保護しています。規則に特に厳しいお客様に採用されているベストプラクティスとして、シークレットとパスワードを一元的なシークレット管理システム内のみに保存する、というものが挙げられます。つまり、更新対象も、ローテーションの対象も、保護の対象も一元化するということです。

しかし、こうしたシークレットが他のプロセスで必要な場合はどうすればよいのでしょう?残念ながら、こうした制限の厳しい開発環境でも、デプロイを行うには認証情報が必須になりつつあります。

とは言え、Diana DevOps のようにシークレットを求める相手全員に、情報を提供する必要はありません。代わりに、シークレットを求めるチームが、認可された場合のみに限ってシークレットへのセキュアな自動アクセスを行えるようにしましょう。

実際の手順

このステップの一部は、みなさんの会社のニーズによって異なるでしょう。どのシークレット管理システムを導入してもかまいませんが、1 つのシステムは利用することを強くお勧めします。企業向けおよびオープンソースの選択肢としては Hashicorp Vault の人気が高いため、本記事の実装例では Hashicorp Vault を使用します。どのシークレット管理システムでも、考え方は同じです。

ちょっと待ってください。パイプラインから一元的なシークレット管理システムへのアクセスを必須にした場合、そのアクセスの認証に必要な認証情報はどうすればよいのでしょうか?

これは「キーゼロ」問題と呼ばれており、解決策は少々特殊です。キーゼロである必要はありますが、パイプラインはその限りではないのです。

まず、従来のシステム管理者が社内にシークレット管理システムを構築する方法を考えてみましょう。

  • シークレット管理システムのルートアクセスにのみキーゼロを使用する
  • ルートアクセスは、以下のような根本となる設定にのみ使用する:
    • 社内の管理者ロール
    • 外部の ID プロバイダー
  • 管理者ユーザーの設定:
    • 事前設定済みの権限とロールを付与する
    • 事前構築した ID プロバイダー経由でログインして一時トークンを取得する
    • シークレットの生成やローテーションは通常許可するが、取得は禁止する

しかし、ソフトウェアパイプラインの場合、認証を担当するユーザーがいないので OAuth 認証を行えません。そこで、ビルドジョブでシークレット管理システムへのアクセス認可を受けるために、以下のように一部を変更します。

  • キーゼロは、シークレット管理システムへのルートアクセスおよび根本的な設定にのみ使用する
  • 管理者ロールに、CircleCI などの既知のプロバイダーとの OIDC 連携の設定を許可する
  • シークレット管理システムの検査対象となるスコープが設定されたトークンを、OIDC プロバイダーが発行して署名する
  • シークレット管理システムで、信頼関係を事前設定し以下を行えるようにする:
    • 提供されたトークンを検証および認証する
    • トークンに特定のロールを割り当てる

では、ルートキーはどう扱うべきでしょうか?

答えは、みなさんの判断ではなく、ソフトウェアパイプラインによって決まります。ただし、実際には、ロック解除時に提示すべきキーをまとめて “ルートトークン” とするのが一般的です。これらのトークンは、社内の上級職員 (一般には役員) に配布します。キーの保持者が 3 名以上揃わない限り、つまり保持者が 1 名だけの場合 (あるいは 2 名が共謀した場合) はシークレット管理システムのロックを解除できないように設定します。物理ハードウェアセキュリティモデル (HSM) デバイスを使用する場合は、これらのキーと社内データセンターなどへの物理的な存在とのペアリングも要件に加わります。

ステップ 3: 事前にシステム間の信頼関係を構築する

ステップ 1 と 2 と同様に、ステップ 3 のテーマもゼロトラストです。ゼロトラストとは、個人やグループを信頼するのではなく、十分に理解している強制的なプロセスを信頼する体制のことです。

CircleCI と一元的なシークレット管理システムとの間に信頼関係を構築することで、アクセス可否の判断をシークレット管理システムのクレーム/ポリシーメカニズムにまかせることができます。お使いのシークレット管理システムによっては、「foo-app の開発環境デプロイに DB 認証情報 XYZ へのアクセス権を付与すべきか?」のように、実行者ではなくアクションを調査対象にすることもできます。

CircleCI を利用する個々のユーザーは、ワークロードで必要なシークレットへのアクセスが不要なだけでなく、シークレットについて考える必要さえありません。OIDC クレームを使えば、実行するアクションに必要なロールをリクエストするだけで済みます。この場合でも、リクエストされたロールが有効かどうか、およびロールがどの権限 (またはシークレット) にアクセスするかについての判断は、シークレット管理システムが担当してくれます。

Hashicorp Vault と連携する場合、構成要素は JSON Web トークン (JWT) による認証方法とアクセス制御リスト (ACL) のポリシーの 2 つです。

OIDC と Hashicorp との連携設定は簡単です。詳しくは、「OIDC (OpenID Connect) を使用して CircleCI と HashiCorp Vault を連携する」を参照してください。

JWT 認証により、CircleCI から提供された Web 標準トークンを Hashicorp で調査します。検証のため、トークンが生成された場所と、トークンが有効な場所の情報が必要になります。今回の場合、どちらの情報にも、CircleCI の組織ごとに異なる一意の URL が該当します。

  • OIDC discovery URL: https://oidc.circleci.com/org/{YOUR_ORG_ID}
  • Bound issuer ID: https://oidc.circleci.com/org/{YOUR_ORG_ID}

Hashicorp Vault では、管理者が事前にした上記の URL を呼び出して、CircleCI に以下のように問い合わせます。

  • このトークンは本当に CircleCI で生成されたものか?
  • このペイロード (クレーム) は有効か?

質問に対して満足の行く答えが得られた場合のみ、Hashicorp Vault のポリシーで設定済みのロールにおいて CircleCI ジョブが信頼されます。

ステップ 4: ポリシーを自動化し権限を委譲する

このステップを完了すれば、組織として従来の時間のかかるポリシーを簡素化し協力関係を築いてきた努力が実り、組織全体で大きなメリットを得られます。ここまで 3 つの基本要素を用意してきたので、権限委譲と管理の自動化を極める準備は整っています。

CI 分野において斬新な機能である CircleCI の設定ファイルポリシーを活用します。このポリシーでは、CircleCI の組織が、全パイプラインユーザーに対して交通規則を細かく明示的に決められます。つまり、必須事項、禁止事項、推奨事項、強制事項を設定できます。

特筆すべきことに、この機能も CircleCI の枠組みに準拠しているので、すべての設定をコードで行えます。ただし、現時点では管理者専用です。

実際の手順

CircleCI の設定ファイルポリシーでは、組織の管理者が、業界標準の Open Policy Agent (OPA) で組織の規則を定義することができます。ポリシーの定義には Rego を使用します。Regoの簡単な概要は以下のとおりです。

Rego クエリは、OPA に保存されているデータに対するアサーションです。このクエリを使用することで、期待されるシステム状態に反するデータインスタンスを列挙したポリシーを作成できます。

簡単な例として、筆者おすすめの CircleCI 機能である Orb を使ってみましょう。Orb は、CircleCI の設定をカプセル化したものです。独自のプライベート Orb をチーム内で共有したり、Developer Hub のオープンソース Orb を活用したりすることで、設定ファイルを簡略化し開発の手間を大幅に削減できます。ただし、セキュリティに厳しい組織では、サードパーティ製コードの使用を制限または禁止する場合があります。

    Policy_name["ban_3rd_party"] 

# Each policy may declare 1:many rules 
# Rules must be unique across entire organization as well.
    ban_orbs = config.ban_orbs(["3rdParty/risky-orb"]) 

# Rules must be enabled to be enforces
    enable_rule["ban_orbs"] 

# Rules default to a warning, and can be set to HARD_FAIL
    hard_fail["ban_orbs"]

上記のルールでは、除外対象の 3rdParty/unvetted-orb の使用を組織全体のプロジェクトで禁止していますが、これではイタチごっこになりかねません。とは言え、許可リストに基づくポリシーも禁止ポリシーと同じく簡単に作成できます。そこで、以下のように、設定ファイル自体を検査する Rego ポリシーを作成してみました。

        policy_name["orbs_allowlist"]
        allow_approved_orbs = check_allowlist({"partner/useful","partner2/useful"})

        enable_rule["allow_approved_orbs"] 
        hard_fail["allow_approved_orbs"]

        #custom functions for the win!
        check_allowlist(allowed_orbs) = { orb: msg | orb := input["orbs"][_]
          [name, _] := split(orb, "@")
          not allowed_orbs[name]
          msg := sprintf("%s orb is not allowed in CircleCI configuration", [name])
        }

これなら、許可する Orb を “名前空間/名前” 形式で定義するだけで済み、未定義の Orb はすべて禁止できます。

しかし、冒頭における Eddie、Diana、Porter の争いの原因は Orb ではなく、本番環境用のシークレットへのアクセス権でした。

Diana チームの設定ファイルを見てみましょう。

version: 2.1
jobs:
  test:
   ...
  build-push:
     steps:
       - checkout
	...

  deploy:
     steps:
       - checkout
	...
workflows:
 main:
   jobs:
     - test
     - build-push:
         context: vault-oidc-artifacts
     - deploy:
         name: Deploy Dev
         requires: [ build-push, test ]
         context: [ vault-oidc-dev ]
     - deploy:
         name: Deploy Production
         requires: [ Deploy Dev ]
         context: [ vault-oidc-prod ]
         filters:
           branches:
             only: [ main ]        

この設定を人間の言葉にすると、以下のようになります。

  • テストを実行する
  • アーティファクトをビルドする
    • テストの成功が条件
    • vault-oidc-artifactコンテキスト に保存されているシークレットを使用
  • 開発環境へのデプロイを実行する
    • ビルドとテストの成功が条件
    • [vault-oidc-dev]コンテキストを使用
  • 本番環境へのデプロイを実行する
    • 開発環境へのデプロイの成功が条件
    • [vault-oidc-prod]コンテキストを使用
    • ブランチは main のみに限定

もし、Diana がブランチ限定フィルターを削除し、feature ブランチから本番環境のシークレットにアクセスしようとしたらどうなるでしょうか?

  1. ステップ 1 は、デプロイの実行対象ブランチを main のみに限定しているので問題ありません。
  2. ステップ 2 は、各ジョブでシークレット管理システムとの通信に OIDC 対応コンテキストを使用しているので問題ありません。
  3. ステップ 3 は、CircleCI 管理者が事前に定義しています。
  4. ステップ 4 が未解決の問題です。悪意のある人物は、パイプラインの設定を変更すればポリシーを回避できてしまいます。

Porter がこの “不適切な設定” 問題を防ぐにはどうすればよいのでしょうか?

解決策: 設定ファイルポリシーを活用する

ステップ 4 の問題を解決する鍵は、CircleCI の設定ファイルポリシー機能です。

Porter は、このポリシーを使用することで、組織の全パイプラインに社内規則をコードとして埋め込むことができます。今回は、以下の Rego ファイルを基に考えてみましょう。このファイルでは、本番用の OIDC コンテキストへのアクセスを本番用ブランチ (今回は main) のみに許可するというルールを定義しています。

package org

import future.keywords
import data.circleci.config

policy_name["production_context_protection"]

use_prod_context_only_on_main = config.contexts_reserved_by_branches(["main"],
 {"vault-oidc-prod"}
)

# This rule will apply to all projects subscribed in globals.rego under `restricted_context_access_projects`
enable_rule["use_prod_context_only_on_main"] 
hard_fail["use_prod_context_only_on_main"]

このポリシーを適用した場合、Diana のチームがフィルターを削除したり、実行対象のブランチを追加したりしようとすると、以下のメッセージが表示されます。

policy evaluation failed:
use_prod_context_only_on_main: You may not use production context: cera-boa-prod outside of main branch. Offending workflow.job: `main.Deploy Dev`   

つまり、ジョブは実行されません。Diana は、設定ファイルを元に戻すまで、シークレットを取得できないのです。

コンフィグファイルポリシーを使いこなすヒント

ヘルパー関数を活用する

設定ファイルポリシー機能で高度なポリシーを作成するには豊富な知識が必要ですが、一般的なユースケースであれば既定のヘルパー関数 で簡単に対応できます。たとえば、以下のような関数があります。

  • ban_orbs
    調査 (テスト) されていない Orb について、バージョンを問わず禁止します。
  • ban_orbs_version
    ban_orbs と似ていますが、指定したバージョンのみ禁止します。Orb を廃止する際に、”SOFT_FAIL” と組み合わせて使うと効果的です。
  • resource_class_by_project
    大きなリソースクラスや特別なリソースクラスの使用を、指定したプロジェクトのみに許可します。
  • contexts_alloweds_by_project_ids
    指定したコンテキストの使用を、指定したプロジェクトのみに許可します。
  • contexts_blocked_by_project_ids
    指定したプロジェクトに対し、コンテキストへのアクセスを禁止します。
  • contexts_reserved_by_project_ids
    指定したコンテキストへのアクセスを、指定したプロジェクトのみに許可します。

セットと変数を活用する

Rego はコードであるため、ポリシーの数が増えてもセットを定義することで整理できます。たとえば、project_id を毎回使用するのではなく、この ID を共通 Rego ファイルに定義し、複数のルールで参照することができます。応用として、ポリシーの適用対象としてプロジェクトをグループ化 (バンドル化) することも可能です。

# single application IDs. Can be automated.
low_risk_project_id := "abcdef-12345"
high_risk_project_id :="defabc-54321"
Company_website_id := …
Company_billing_id := …

# sets can group projects
Front_end_applications := {company_website_id}
Tier_one_applications :=  {company_website_id, company_billing_id}
code_freeze_apps :=  {company_website_id, company_billing_id}

こうすれば、グループの和や差分、共通部分など、グループ分けを指定してルールを適用できます。この有用性を確認するために、main へのアクセスをブロックするルールを考えてみましょう。本番用コンテキストが複数ある場合や、デフォルトブランチが複数ある場合でも、以下のように簡単に定義できます。

use_prod_context_only_on_main = config.contexts_reserved_by_branches(
 Set_of_main_branches,
 Set_of_production_contexts
)

また、特に重要なプロジェクトに対してのみ指定のルールを適用することもできます。

# This rule will apply to all projects defined in globals.rego under `tier_one_applications`
# AS LONG AS they are not under code freeze
enable_rule["use_prod_context_only_on_main"] {
  (tier_one_applications-code_freeze_apps)[data.meta.project_id]
}

上記の例では、- を使用して、適用対象のアプリケーションを限定しています。このルールの意味は、「コードが現在フリーズ状態のものを除き、すべてのティア 1 アプリケーションに対してこのルールを有効にする」です。そのため、フリーズ状態のアプリは、本番用コンテキストの使用許可リストから除外されます。したがって、メインや承認済みのビルドであっても、本番環境へのデプロイを行う場合にはブロックされます。

開発者体験を高める注記を付ける

はっきりとした原因なしにビルドが失敗すると、気分も生産性も大きく落ち込んでしまうものです。設定ファイルポリシーの重要な機能の 1 つが、すべてのエラーに (カスタマイズ可能な) 明確な理由を付記し、開発者に伝えられることです。

ビルドエラー

こうすれば、コミットの実行者に対して、 publish-docs ワークフローの build-html ジョブと deploy ジョブ ジョブを更新する必要があると伝えられます。開発環境へのデプロイと HTML のビルドには、本番用シークレット管理システムへのアクセスは必要ないでしょう。

管理者体験を高める注記を付ける

Diana にとっては、チームがポリシーに違反することがなくなり、パイプラインが油を塗ったソリのように軽快に進むようになったので、これで申し分ありません。では、コンプライアンス意識の薄いチームに悩まされていた Porter はどうでしょう?

設定ファイルポリシーの便利なツールのおかげで、Porter のチームも満足の行く結果となりました。

ポリシーのテスト

新しく作成したポリシーで業務が止まってしまう事態を防ぐために、CircleCI CLI にも新機能が追加されました。ポリシーを組織全体に適用する前に、結果がはっきりと示されるテストフレームワーク により、ローカルでポリシーのテストを行うことができます。

ポリシーのテスト

どうやら、上図の出力を見ると、Porter のポリシーにはルールの適用漏れがあり、そのために合格すべきところで HARD_FAIL が発生しているようです。

コンフィグファイルポリシーの監査ログと影響評価

開発者は、エラーに出会えば修正するものです (例外もありますが)。しかし、HARD_FAILS を組織全体に適用する前に、影響を評価しておきたい場合もあります。

Porter はこの解決策として、すべてのポリシーを SOFT_FAIL として実装することにしました。これにより、ビルドを停止せずにエラーを追跡できます。

ソフトフェイルとして実装した場合、プラットフォームオペレーターは、作業の流れを止めることなくポリシーの影響をモニタリングし評価できます。

Soft fail

評価後は、プロジェクトとパイプラインのメタデータを活用して、対応するうえで必要となる特定のプロジェクトや実行者の情報を収集できます (今回の例では、Diana のチームに対して 30 日以内に新しいポリシーに準拠するよう伝える)。

次の一歩

この記事では、開発者の生産性向上と組織のセキュリティの両立を実現するうえで役立つ 4 つのステップをご紹介しました。コードベースへのコミットの実行者に対する制限を強めると同時に、パイプラインにルールベースの制御ロジックを実装することで、開発が目に見えてスピードアップし、チームの満足度も大きく高まるでしょう。しかし、Diana と Porter 達が至った協調関係は、実際に得られる成果のほんの始まりに過ぎません。

ポリシーの自動適用には、組織の指示系統の効率化につながるユースケースがまだまだ数多く存在しています。実際、設定ファイルのポリシーを活用して困難な課題に対処しているお客様もいます。以下に、この機能で実現できるユースケースの一例をご紹介します。

  • デプロイジョブについて SSH での再実行を禁止する
  • ランナーやリソースクラスを使用できるチームを制限する (コストやセキュリティの観点)
  • チームのコンテキストやシークレットへのアクセスを、ブランチ単位およびプロジェクト単位で禁止する
  • コードやライセンスの検査を必須にする
  • 古い Docker イメージや Orb のバージョンの使用をやめるように周知する
  • 承認済みリストに基づいてサードパーティ製 Orb の使用を許可する
  • 社内で承認されたイメージのみ使用を許可する

本記事で興味を持たれた方は、ぜひ設定ファイルのポリシーに関するドキュメントをご覧ください。また、担当のアカウントチームにお問い合わせいただければ、みなさまの組織の規則に合わせたデモをご紹介いたします。なお、設定ファイルのポリシーを利用するには、Scale プランへのお申し込みが必要です。