シンデレラのように魔法がとけないうちは本番環境にアクセスできるようにしてみた

この記事では、できるだけアクセスを絞るべき本番環境に対して、かのシンデレラのように時間制限つきの承認性アクセスができるようにした事例を紹介します。

目次

はじめに

こんにちは、NeWork 開発チームの藤野です。普段はオンラインワークスペースサービス NeWork のエンジニアリングマネジメントをしており、最近では実際にコードを書く機会も増えてきています。

この記事では、これまで手動 + ガッツで運用していた本番環境へのアクセス管理の工程のほとんどを自動化した内容をご紹介します。

背景

複数の環境

他のよくあるWebサービスと同様に、NeWork においてもサービスの運用・開発をするにあたって以下の3種類の環境を用意しています。

  • 開発環境
  • ステージング環境
  • 本番環境

開発環境・ステージング環境については開発メンバーは普段からフルアクセスできるようにしていますが、本番環境についてはセキュリティ等の観点からアクセスできるメンバーやそれぞれの権限は最小限にしています。

単純なセキュリティ以外の観点でも、開発者が本番環境までアクセスできてしまうと環境取り違えによる誤操作でトラブル発生につながる可能性があります。 開発作業を余計な心配なく集中してできるようにする観点でも、環境の分離と余計なアクセス権限をなくすことが重要です。

一方で普段は本番環境に一切アクセスできない開発メンバーであっても、以下のようないくつかのシーンでアクセスが必要になることがあります。

  • 申告やアラートにもとづくトラブルシューティング
  • 新機能のリリースに伴う設定の更新や DB のマイグレーション作業

これまでの運用

NeWork では Google Cloud を主に利用してサービス提供しています。

上記のような本番環境へのアクセスが必要になったシーンでは、以下の運用で対応していました。

  1. 開発メンバー. : 特権アカウント所持者 (≒ 藤野) に理由を添えてアクセス権限付与依頼
  2. 特権アカウント所持者 : (依頼に気づいたタイミング以降で) アクセス権限を付与
  3. 開発メンバー : 作業完了後に権限の削除依頼
  4. 特権アカウント所持者 : アクセス権限を削除

課題

しかし 2 年以上この運用を続けてきていて以下の課題があると感じていました。

  1. 特権アカウント所持者の状況によっては、対応が遅れタイムリーに処理できなくなりうること
  2. 特権アカウント所持者 (≒ 藤野) の業務上の役割が変化し、実際に開発をする頻度が向上することによって、開発環境と取り違えて誤操作をする可能性が浮上
  3. 緊急のトラブルシューティング等の場合、対応完了と素直に判断できなくなるシーンがあり、権限削除依頼を失念し、不必要に長期間権限が付与された状態になることがある

上記の問題に対処するため、普段は一切開発等をしない複数のメンバーにも特権を付与し、代わりにアクセス権限付与作業をしてもらうことも考えられましたが、以下の懸念がありました。

  • そもそもの操作に慣れていない状況でセンシティブな作業をお願いしづらい
  • 複数依頼対象がいると、誰にお願いすればいいかわからず手間が増える

実現方法

上述の背景を考慮して、2024 年 5 月から以下のワークフローを用意し、運用を開始しました。

  1. 開発メンバー : GitHub Actions を以下の情報を入力して起動して権限昇格申請

    • 環境 (基本的には本番環境)
    • 権限付与対象アカウントのメールアドレス
    • 権限
    • 権限付与期間 : 1h/2h/5h/1d から選択
    • 目的
  2. 承認可能メンバー : 申請内容に問題なければ承認

  3. 自動 : GitHub Actions が期間限定の権限を対象かアカウントに付与
  4. 自動 : 設定した期間を過ぎると自動で権限が無効になる

これにより、開発メンバーであっても本番環境の特権アカウントを持つことなく承認可能メンバーになることもできるようになりました。

実装 - Google Cloud IAM 設定スクリプト

期限付きの権限付与は Google Cloud の 一時的なアクセス権付与 の仕組みを利用します。

これを GitHub Actions から実行できるように bibbidi-bobbidi-boo.sh という名前の以下のシェルスクリプトを用意しました。

#!/bin/bash
#
# bibbidi-bobbidi-boo.sh
# ============
#
# Script to configure temporary access to an account
#
# Usage:
#
#     bibbidi-bobbidi-boo.sh $PROJECT_ID $USER_EMAIL $ROLE $DURATION

# パラメータ
PROJECT_ID=$1  # GCPプロジェクトID
USER_EMAIL=$2  # ユーザーのメールアドレス
ROLE=$3        # 付与するロール
DURATION=$4    # ロールを付与する期間(日または時間)

# DURATIONを秒に変換
if [[ $DURATION == *d ]]; then
  DURATION_SECONDS=$(( ${DURATION%d} * 24 * 60 * 60 ))
elif [[ $DURATION == *h ]]; then
  DURATION_SECONDS=$(( ${DURATION%h} * 60 * 60 ))
else
  echo "Invalid duration format. Use <number>d for days or <number>h for hours."
  exit 1
fi

# 現在の日時を取得
CURRENT_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# 期限日時を計算
EXPIRATION_DATE=$(date -u -d "$DURATION_SECONDS seconds" +%Y-%m-%dT%H:%M:%SZ)

# ロールを付与
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$USER_EMAIL" \
  --role=$ROLE \
  --condition="expression=request.time < timestamp('${EXPIRATION_DATE}'),title=Cinderella_${ROLE}_access,description=Temporary ${ROLE} access until ${EXPIRATION_DATE}"

これにより、指定したプロジェクト・アカウント・ロールを期間限定で付与できるようになります。

実行すると以下のように期間限定で権限付与ができます。

なお、仕様により オーナー / 編集者 / 閲覧者 等の基本ロールには条件設定ができない点は注意が必要です。

設定 - GitHub Environments

GitHub Environments は以下の目的で利用できます。

  • 変数を variables / secrets に設定
  • アクション実行ブランチの制限

それ以外にも特定のメンバーによるアクション実行を承認性にすることもできます。

今回のアクションはリポジトリにアクセスできる人が誰でも実行できてしまうと困るので、この用途でも活用するようにしました。

これにより、ワークフローを実行すると指定されたメンバーの承認がないと実行できなくなります。

また、 Prevent self-review にチェックをいれることで自作自演による環境アクセス承認もできなくなります。

実装 - GitHub Actions

上記のスクリプトを実行できる GitHub Actions を用意します。

name: Cinderella Request

on:
  workflow_dispatch:
    inputs:
      environment:
        type: environment
        required: true
      email:
        description: "User Email"
        required: true
      role:
        description: "Role"
        type: choice
        # 付与するロールはプロジェクトにあわせて調整必要
        default: "roles/firebase.admin"
        options:
          - roles/firebase.admin
          - roles/run.admin
          - roles/cloudfunctions.admin
      duration:
        description: "Duration"
        required: true
        type: choice
        default: "1h"
        options:
          - 1h
          - 2h
          - 5h
          - 1d
      purpose:
        description: "Purpose"
        required: true

run-name: Request ${{ inputs.role }} during ${{ inputs.duration }} to ${{ inputs.email }} for ${{ inputs.purpose }}

jobs:
  cinderella:
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.environment }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Authenticate Google Cloud
        uses: "google-GitHub-actions/auth@v2"
        with:
          workload_identity_provider: ${{ vars.IDENTITY_PROVIDER }}
      - name: Run a magic
        run: ./scripts/bibbidi-bobbidi-boo.sh ${{ vars.GC_PROJECT_ID }} ${{ inputs.email }} ${{ inputs.role }} ${{ inputs.duration }}

基本的にはスクリプト実行に必要なパラメータに関する値を指定して実行できるようにしています。

スクリプトとは関係なく purpose も取得するようにしており、これは実行時のタイトルに設定しています。

これにより、前述の承認判断をより簡単にできるようにしています。

なお、スクリプトの実行にはresourcemanager.projects.setIamPolicy の権限が必要なので、Project IAM 管理者 (resourcemanager.projectIamAdmin) あたりを認証に利用します。

その他細かな工夫点

ゴミ掃除

権限の観点では、指定した時間を過ぎると権限は無効になります。ただ、Google Cloud の設定上は不要な権限が残ってしまいます。

このゴミが溜まってしまわないように、以下のスクリプトを GitHub Actions で定期実行することでゴミが削除されるようにします。

#!/bin/bash -e
#
# tremaine-family.sh
# ============
#
# Remove temporary access right named with Cinderella
#
# Usage:
#
#     tremaine-family.sh $PROJECT_ID

# パラメータ
PROJECT_ID=$1  # GCPプロジェクトID

# プロジェクトのIAMポリシーを取得
CURRENT_POLICY=$(gcloud projects get-iam-policy $PROJECT_ID --format=json)

# `Cinderella` がつく condition を削除
NEW_POLICY=$(echo $CURRENT_POLICY | jq 'del(.bindings[] | select(.condition) | select(.condition.title | startswith("Cinderella_")))')

# 更新後のIAMポリシーを整形してJSONファイルに出力
echo $NEW_POLICY | jq . -M > updated_iam_policy.json

# 差分があった場合に更新
if [ "$CURRENT_POLICY" != "$NEW_POLICY" ]; then
  gcloud projects set-iam-policy $PROJECT_ID updated_iam_policy.json
fi
name: Cleaner

on:
  workflow_dispatch:
    inputs:
      environment:
        required: true
        type: environment
  # 平日 24:00 JST
  schedule:
    - cron: "0 15 * * 1-5"

run-name: IAM Cleaning @ ${{ inputs.environment || 'prod' }}

jobs:
  clean:
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.environment || 'prod' }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Authenticate Google Cloud
        uses: "google-GitHub-actions/auth@v2"
        with:
          workload_identity_provider: ${{ vars.IDENTITY_PROVIDER }}
      - name: Run cleaner
        run: |
          ./scripts/tremaine-family.sh ${{ steps.vars.outputs.GC_PROJECT_ID }}

これにより、24時の鐘が鳴ると Cinderella_ がつく権限を削除します。

日を跨ぐ作業がこれまでもなかったので、権限が賞味期限切れになっているかに関わらず削除してしまっていますが、 jq 構文を頑張ることで期限切れの権限のみの削除もできるはずです。

Slack 連携

承認依頼は GitHub の設定に応じてメール等でも承認者に飛びますが、利便性を考慮して Slack と連携しました。

これにより、以下の全てが Slack へ通知等できるようにしています。

  • ワークフローの起動時の通知
  • ワークフローに対する承認処理
  • ゴミ掃除結果 (上述のスクリプトに +α の追加をして実現しています)

Slack の連携時に以下の方法でスレッドに連携しないとワークフローの承認ができないのでご注意ください。

/github subscribe ${REPO_NAME} workflows:{name: "${WORKFLOW_NAME}"}

単純な通知としても便利ですが、ログの意味でも役立っています。必要あればさらにワークフローを改良して、監査用のスプレッドシートなどに実行履歴を保存することなども考えられます。

サービスアカウントキーの発行

Firebase Admin SDK を利用するスクリプトをローカルで実行する場合は、権限を持つサービスアカウントキーを読み込んだ上でスクリプトを実行する必要があります。 1

これについても一時的な権限をふったサービスアカウントキーを発行できるようにしています。

#!/bin/bash
#
# pumpkin-carriage.sh
# ============
#
# Script to generate service account key with temporary access
#
# Usage:
#
#     pumpkin-carriage.sh $PROJECT_ID $DURATION

# パラメータ
PROJECT_ID=$1  # GCPプロジェクトID
DURATION=$2    # ロールを付与する期間(日または時間)

# DURATIONを秒に変換
if [[ $DURATION == *d ]]; then
  DURATION_SECONDS=$(( ${DURATION%d} * 24 * 60 * 60 ))
elif [[ $DURATION == *h ]]; then
  DURATION_SECONDS=$(( ${DURATION%h} * 60 * 60 ))
else
  echo "Invalid duration format. Use <number>d for days or <number>h for hours."
  exit 1
fi

# サービスアカウントを作成
SERVICE_ACCOUNT_NAME="firebase-admin-cinderella"
SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

# サービスアカウントが存在するか確認
if ! gcloud iam service-accounts describe $SERVICE_ACCOUNT_EMAIL > /dev/null 2>&1; then
  gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \
    --description="Temporary Firebase Admin" \
    --display-name="Temporary Firebase Admin"
fi

# 現在の日時を取得
CURRENT_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# 期限日時を計算
EXPIRATION_DATE=$(date -u -d "$DURATION_SECONDS seconds" +%Y-%m-%dT%H:%M:%SZ)

# ロールを付与
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
  --role="roles/firebase.admin" \
  --condition="expression=request.time < timestamp('${EXPIRATION_DATE}'),title=Cinderella_roles/firebase.admin_access,description=Temporary roles/firebase.admin access until ${EXPIRATION_DATE}"

# サービスアカウントのキーを作成
gcloud iam service-accounts keys create ./${PROJECT_ID}-firebase-admin-key.json \
  --iam-account $SERVICE_ACCOUNT_EMAIL
name: Firebase Key Request

on:
  workflow_dispatch:
    inputs:
      environment:
        type: environment
        required: true
      duration:
        description: "Duration"
        required: true
        type: choice
        default: "1h"
        options:
          - 1h
          - 2h
          - 5h
          - 1d
      purpose:
        description: "Purpose"
        required: true

run-name: Key Request during ${{ inputs.duration }} for ${{ inputs.purpose }}

jobs:
  cinderella:
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.environment }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Authenticate Google Cloud
        uses: "google-GitHub-actions/auth@v2"
        with:
          workload_identity_provider: ${{ vars.IDENTITY_PROVIDER }}
      - name: Run a magic
        run: ./scripts/pumpkin-carriage.sh ${{ vars.GC_PROJECT_ID }} ${{ inputs.duration }}
      - uses: actions/upload-artifact@v4
        with:
          name: firebase-admin-key
          path: ./${{ vars.GC_PROJECT_ID }}-firebase-admin-key.json
          if-no-files-found: error
          retention-days: 1
          overwrite: true

このワークフローも実行には承認が必要です。 承認され実行されると、サービスアカウントキーのファイルが生成され、GitHub Actions のページからキーファイルをダウンロードできます。

運用を変えてみて

2024年5月からこの運用を導入してみました。

変更以来、月平均で10回ほど実行されており、運用に欠かせないツールとなりました。

運用していくなかで、以下のような場面に遭遇し、改善も日々しています。

  • 設定に複数のロールが必要であったり、そもそもの必要なロールを割り出したりするのが手間 => 全般的な操作をできるカスタムロールを追加
  • 新機能の追加時に伴い、ワークフローのロールリストも追随するのが手間 => ロールを選択肢から選ぶのではなく、自由記述式に変更
  • スクリプトの想定実行時間の見積もりが甘く、期限間際にハラハラしながら完了を待つ => ちょっと長めに期間設定をする運用対処に現状は逃げているが、延長機能の追加も検討中

メンバーもこの運用になって以下のようなポジティブな反応をしてくれており、今後も継続予定です。

  • 一部の開発メンバーは承認者も兼任できるようになり、各チームに一人は承認できるメンバーを含むようになった。そのため、承認者への相談を忖度したり、不用意に長い待ち時間の発生がなくなって楽になった
  • 本番環境へのリリース前に考慮すべきことや実施すべきことが減り、楽になった

おわりに

この記事では、本番環境へのアクセス管理の運用を改善した事例について紹介しました。

今回の内容とは関係が薄いですが、NeWork はどなたでも無料でお試しできますので、もしプロダクトや使われている技術に興味を持っていただけたらぜひ触ってみてください。


  1. https://github.com/firebase/firebase-admin-node/pull/2466 がマージされればこの状況も改善が見込まれています。
© NTT Communications Corporation 2014