Article

退職者のアカウント処理を漏れなく行う手順

高田晃太郎
退職者のアカウント処理を漏れなく行う手順

BetterCloudの調査では、企業が利用するSaaSは平均で約130本、退職時に必要なITタスクは40項目前後にのぼると報告されています。¹ さらに、Verizon Data Breach Investigations Reportは不正侵入の主要要因として資格情報の悪用を繰り返し指摘しており、退職者アカウントの取り扱いはセキュリティの基礎でありながら、もっともヒューマンエラーが起きやすい領域です。² 公開レポートやベンダー資料を踏まえると、属人的な手続きとSaaSスプロールの組み合わせが漏れを生み、ライセンスの無駄と監査指摘の温床になっていることが見えてきます。¹ 退職時のアクセス権剥奪と証跡確保は、NISTの人事終端管理(PS-4)でも基本統制として求められています。³

この状況で有効なのは、HRを起点としたイベントドリブンな自動化と、IdP(アイデンティティプロバイダ)を中核に据えた一括制御です。言い換えれば、退職日と雇用区分の一点管理、SSO(シングルサインオン)の即時遮断、二段階での業務継続対応、そして強い監査証跡という、シンプルで再現性の高いパイプラインを実装することが核心になります。以下では、現実に起こりがちな落とし穴を整理したうえで、OktaやGoogle Workspace、Slack、GitHub、AWSを例に実装可能なコード運用SLAまでを具体的に示します。

なぜ退職者アカウント処理は漏れるのか——現実とリスク

退職連絡が人事側のワークフローにとどまり、ITに通知されるまでにメールやチケットの中で分散することが最初の断絶です。人手での依頼は優先度の解釈が揺れ、タイムゾーンの違いに弱く、連休中に処理が持ち越されるだけで、認証済みのセッション(ログイン状態)とトークン(APIアクセス許可)が長時間残存することになります。IdP非連携のSaaSやシャドーITが混在すると、SSO無効化だけでは鎖が断ち切れず、局所的に生き残ったアカウントが後日発見される事例が後を絶ちません。

もう一つの典型は、単一の「退職」という言葉に複数の意味が折り畳まれている点です。即日退職と将来日付の退職、懲戒や情報持ち出し疑義のような高リスクケース、委託・派遣・アルバイトといった雇用区分に応じて、求められる反応速度とログの厳密さは異なります。これを一枚岩のチェックリストで処理しようとすると、遅すぎる遮断か、逆に早すぎる停止が起き、現場の業務継続を損ないます。結果として、現場が独自判断で例外を積み増し、運用はさらに壊れていきます。

リスクはセキュリティだけに留まりません。アカウントの生存はライセンス費の過払いに直結し、SaaSの年額契約では1件の削除漏れが年間数万円〜数十万円の無駄を生みます。監査では、退職日と無効化日時の不整合、作業者のなりすまし、不十分な証跡が指摘されがちで、是正までの工数はそのまま機会損失になります。したがって、技術的解はイベントの一点起点、強い自動化、可監査性に集約されます。³

標準オフボーディングSOPの設計——HR起点、IdP中核、二段階の現実解

現実的に堅いSOPは、HRIS(人事情報システム)を真のソース・オブ・トゥルースに据え、退職イベントをIdPへ即時伝搬させるところから始まります。人事システムで退職日が確定した時点で、将来日付のディアクティベーションをスケジュールし、同時に物理資産の回収と業務引継ぎのタスクを紐づけます。高リスクケースでは、予定日前でも一時停止を発火できるフラグを人事スキーマに用意し、IdPがそれを解釈して早期のセッション無効化とアクセス停止を行えるようにしておくと事故耐性が高まります。³

IdP側の最初の対応は、当人のSSOを止めるだけでは不十分で、既存セッションとトークンの一斉失効が要になります。Oktaならユーザのステータス変更とセッションリボーク、Google WorkspaceならユーザのサスペンドとOAuthトークン全失効、SlackやGitHubではSCIM(ユーザ管理の標準仕様)やOrg APIでの強制無効化を連鎖させます(実装例は後述)。Googleのトークン失効やSlackのSCIM、GitHub組織メンバーの削除、AWS IAMユーザの無効化は、それぞれ公式APIでサポートされます。⁶⁴⁷⁹

この段階を退職当日0分〜5分のSLAに置き、その後の0分〜30分で高リスク系(プロダクション環境、ソースコード、機密ファイル共有)の停止を完了させます。ライセンス解放や二要素装置の解除、メール転送設定や自動返信、ボックスフォルダ移管など、ビジネス継続に絡む作業は24時間以内の第2ステージで完了する設計にすると、緊急遮断と平時運用の両立がしやすくなります。

例外処理は、許可された短期の延命や、委託先の契約延長など、ドメイン知識が色濃く反映されます。延命は期限付きのタイムロックとしてモデル化し、IdPのユーザ属性に延長終了日時を保持して、バッチもしくはイベントで自動失効させます。人が延長を忘れて永続化する構造を、システムがそもそも持たないことが大切です。最後に、すべてのアクションは監査ログとして一元保管し、誰が・いつ・どの根拠で・何を止めたかを、監査証跡と復旧時のルンブックに再利用します。³

主要SaaS/クラウドの実装例——コードで見る自動化

Okta Event Hookで退職イベントを受け取り、一斉停止を起動する

OktaではEvent Hookでユーザ更新を捕捉し、退職属性の変化をトリガーに外部ワーカーを起動できます。以下はNode.js/Expressの最小構成例です。検証トークンのハンドシェイク、署名検証(X-Okta-SignatureのHMAC検証)、リトライに注意します。

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

const OKTA_HOOK_SECRET = process.env.OKTA_HOOK_SECRET;

function verifySignature(req) {
  // 実運用では X-Okta-Signature のHMACを OKTA_HOOK_SECRET で検証する
  const sig = req.get('X-Okta-Signature');
  return !!sig;
}

app.post('/okta/events', async (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('invalid signature');
  }
  const events = req.body.data?.events || [];
  for (const e of events) {
    const profile = e.target?.[0]?.detailEntry || {};
    const employment = profile.employmentStatus || profile.customProfile?.employment;
    const isTerminated = employment === 'terminated' || profile.customProfile?.offboardFlag === true;
    const userId = e.target?.[0]?.id;
    if (isTerminated && userId) {
      try {
        // 下流のワーカーへ非同期キュー投入(例:SQS/Kafka)
        await enqueueOffboardingJob({ userId, reason: 'hr_event' });
      } catch (err) {
        console.error('enqueue failed', err);
      }
    }
  }
  res.status(200).json({ received: true });
});

async function enqueueOffboardingJob(payload) {
  // 実装はメッセージキューに依存。ここではダミー
  return Promise.resolve();
}

app.listen(3000, () => console.log('okta hook listening'));

パイプラインの末端では、セッション失効とユーザ無効化をOkta APIで強制します。Oktaはレート制限があるため、HTTP 429に対する指数バックオフと冪等性キーを実装しておきます。

import fetch from 'node-fetch';

const OKTA_ORG = process.env.OKTA_ORG; // https://xxx.okta.com
const OKTA_TOKEN = process.env.OKTA_TOKEN;

async function revokeSessions(userId) {
  const url = `${OKTA_ORG}/api/v1/users/${userId}/sessions`; // list & revoke
  const res = await fetch(url, { headers: { Authorization: `SSWS ${OKTA_TOKEN}` } });
  if (!res.ok) throw new Error(`list sessions failed: ${res.status}`);
  const sessions = await res.json();
  for (const s of sessions) {
    const u = `${OKTA_ORG}/api/v1/sessions/${s.id}`;
    const r = await fetch(u, { method: 'DELETE', headers: { Authorization: `SSWS ${OKTA_TOKEN}` } });
    if (!r.ok && r.status !== 404) throw new Error(`revoke failed: ${r.status}`);
  }
}

async function deactivateUser(userId) {
  const url = `${OKTA_ORG}/api/v1/users/${userId}/lifecycle/deactivate`;
  const res = await fetch(url, { method: 'POST', headers: { Authorization: `SSWS ${OKTA_TOKEN}` } });
  if (!res.ok && res.status !== 404) throw new Error(`deactivate failed: ${res.status}`);
}

Slack SCIMで即時無効化し、再招待を防ぐ

SlackはSCIMでユーザのactive属性を切り替えられます。レート制限はおおむね1分あたり数十リクエストなので、並列数を控えめに設計します。⁴

#!/usr/bin/env bash
set -euo pipefail
SLACK_TOKEN="${SLACK_TOKEN}" # SCIM権限付き(scim:write)
USER_ID="$1" # Slack User ID (UXXXX)

# ユーザ無効化(active=false)
curl -sS -X PATCH \
  -H "Authorization: Bearer ${SLACK_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"active": false}' \
  "https://slack.com/scim/v1/Users/${USER_ID}" | jq .

# 再招待防止のためのemail domain allowlistや外部ゲストの棚卸しは別途運用で担保

Google Workspace Directory APIでサスペンドし、トークンを失効

Google Admin SDKを使えば、ユーザのサスペンドとOAuthトークンの失効が可能です(サービスアカウントにドメインワイド委任を構成)。Users.updateとTokens.deleteを組み合わせます。⁵⁶

import google.auth
from googleapiclient.discovery import build
from google.oauth2 import service_account
from googleapiclient.errors import HttpError

SCOPES = [
    'https://www.googleapis.com/auth/admin.directory.user',
    'https://www.googleapis.com/auth/admin.directory.user.security'
]

def suspend_and_revoke(sa_json_path, subject, user_key):
    creds = service_account.Credentials.from_service_account_file(sa_json_path, scopes=SCOPES)
    delegated = creds.with_subject(subject)
    try:
        service = build('admin', 'directory_v1', credentials=delegated)
        # サスペンド
        service.users().update(userKey=user_key, body={'suspended': True}).execute()  # ⁵
        # 発行済みOAuthトークンの列挙と失効
        tokens = service.tokens().list(userKey=user_key).execute().get('items', [])  # ⁶
        for t in tokens:
            client_id = t.get('clientId')
            if client_id:
                service.tokens().delete(userKey=user_key, clientId=client_id).execute()  # ⁶
        return True
    except HttpError as e:
        if e.resp.status in (404, 410):
            return True
        raise

GitHub組織からの脱退とチーム権限の剥奪

GitHubでは、組織メンバーの削除とチーム退出をAPIで自動化できます。個人PATの完全な強制失効はできないため、SSO必須設定と外部コラボレーターの棚卸しを合わせて運用します。⁷⁸

import requests
import time

GITHUB_TOKEN = 'ghp_xxx'  # org admin token
ORG = 'your-org'
HEADERS = { 'Authorization': f'Bearer {GITHUB_TOKEN}', 'Accept': 'application/vnd.github+json' }

def remove_member(username):
    r = requests.delete(f'https://api.github.com/orgs/{ORG}/members/{username}', headers=HEADERS)  # ⁷
    if r.status_code not in (204, 404):
        raise RuntimeError(f'remove failed {r.status_code}: {r.text}')

def remove_from_all_teams(username):
    t = requests.get(f'https://api.github.com/orgs/{ORG}/teams', headers=HEADERS)
    t.raise_for_status()
    for team in t.json():
        slug = team['slug']
        u = f'https://api.github.com/orgs/{ORG}/teams/{slug}/memberships/{username}'  # ⁸
        r = requests.delete(u, headers=HEADERS)
        if r.status_code not in (204, 404):
            time.sleep(1)

if __name__ == '__main__':
    user = 'retiring-user'
    remove_from_all_teams(user)
    remove_member(user)

AWS IAMの無効化——アクセスキーとセッションの遮断

AWSはキーローテーションと同じ文脈で、退職者のアクセスキーを即時無効化し、ログインプロファイルや付与ポリシーを剥奪します。AWS IAMユーザが残る環境では確実な自動化が必要です。⁹

import boto3
from botocore.exceptions import ClientError

iam = boto3.client('iam')

def deactivate_user(username):
    try:
        # アクセスキー無効化
        for k in iam.list_access_keys(UserName=username)['AccessKeyMetadata']:
            try:
                iam.update_access_key(UserName=username, AccessKeyId=k['AccessKeyId'], Status='Inactive')
            except ClientError as e:
                if e.response['Error']['Code'] != 'NoSuchEntity':
                    raise
        # 管理ポリシーのデタッチ
        for p in iam.list_attached_user_policies(UserName=username)['AttachedPolicies']:
            iam.detach_user_policy(UserName=username, PolicyArn=p['PolicyArn'])
        # インラインポリシーの削除
        for pn in iam.list_user_policies(UserName=username)['PolicyNames']:
            iam.delete_user_policy(UserName=username, PolicyName=pn)
        # コンソールログイン無効化
        try:
            iam.delete_login_profile(UserName=username)
        except ClientError as e:
            if e.response['Error']['Code'] != 'NoSuchEntity':
                raise
    except ClientError as e:
        if e.response['Error']['Code'] != 'NoSuchEntity':
            raise

グループ駆動の割当で「付け外し」を無くすTerraform

アプリの付与と剥奪を手作業にしないために、グループを単位にTerraformで宣言管理します。人の所属が変われば、割当が自動で変わるのが理想です。

# Oktaアプリ割当をグループ駆動に
resource "okta_group" "eng" { name = "grp_eng" }
resource "okta_app_saml" "confluence" { label = "Confluence" /* 省略 */ }
resource "okta_app_group_assignment" "confluence_eng" {
  app_id   = okta_app_saml.confluence.id
  group_id = okta_group.eng.id
}

# 退職者は自動的にどの業務グループからも外れるモデルに(HR > IdPの属性マッピング)

運用SLA、監査、リスク低減——コードの外側を固める

現場で効く運用は、時間基準での約束と、測れるメトリクスを伴います。退職イベントの検知からSSO遮断までをTTFZ(Time To First Zero-Trust)5分以内、高リスク系の停止をMTTR(平均復旧時間)30分以内、全SaaSのディアクティベーションを**24時間以内の完了率99.9%**といった形で定義し、ダッシュボードで可視化します。イベント受領、キュー投入、各プロバイダ呼び出し、結果の記録の各ステップにトレースIDを配し、ログを集中管理することで、監査に対して「退職日」「停止開始」「停止完了」「作業主体」「根拠」の突合せを数クリックで提示できます。³

エラーは前提として織り込みます。SaaSごとの429や5xx、ネットワーク断、IDの表記揺れ、すでに手動で削除済みのケースなど、予期しうる失敗はすべて冪等な再試行で吸収します。ユーザをキーに同一ジョブの並列実行を抑止し、リトライは指数バックオフとジッターで設計します。Slack SCIMは分あたりの呼び出し上限が低いため、ワーカーを広げるのではなくキューの滞留許容を調整します。GitHubはトークンのレート制限が時間あたりに設定されているため、組織全体のオフボーディングが集中する日にはあらかじめスロットリングの閾値を引き下げてピークをならします。

ビジネス継続の観点では、メール自動返信と転送の設定、ドライブやナレッジのオーナー移管、アプリケーション内の担当者再割当を、停止と同じパイプラインで扱います。Google Workspaceではドライブファイルの所有者移管API、ConfluenceやJiraではスペース管理者の再設定、Salesforceでは所有レコードの再割当プロセスを自動化し、作業証跡を同じストレージに残します。これにより、現場は個別のお願いベースではなく、事前に合意されたSLAに乗った確定的な体験を得られます。

ROIは明確です。人が1件あたり20〜30分を費やしていた停止作業が、イベント到達からワーカー完了まで3〜5分の機械時間に置き換わると、月間オフボーディングが20件の規模で月10時間以上の削減になり、年額のライセンス無駄払いが継続的に抑止されます。公開事例でも、SaaS資産管理の徹底により無駄なライセンス費の削減が報告されています。¹⁰ 加えて、監査応答の準備時間や是正工数が削られるため、ITが攻めの改善に振り向けられる時間が増えます。

検証と展開——段階導入で「止めすぎ」と「止め足りない」を避ける

導入はサンドボックスからの段階展開が失敗を減らします。人事システムに仮想ユーザを作成し、未来日退職・高リスクフラグ・撤回などのシナリオを流し込みます。各シナリオでIdPが意図したイベントを発火し、ワーカーが対象SaaSを正しく止めるか、監査ログに欠落がないかを検証します。次に、実データを含むステージングに移り、社内の一部部門でシャドーモードを実施します。シャドーモードでは、実際の停止は人手で行いながら、自動化パイプラインは提案と記録だけを行い、差分をレビューします。この期間に、延命フローや委託先の扱い、物理入退室カードの停止連携など、組織特有の例外をモデリングします。

本番切替後も、モニタリングを緩めないことが成功の鍵です。TTFZやMTTRの四半期ベンチマークを取り、SLO(サービス目標)の逸脱が続く箇所は機能ではなくフローの見直しから着手します。例えば、HRからIdPへの同期が夜間に限定されていて初動が遅いのであれば、Webhook即時通知を前提に人事側の設定を変える方が、IT側のワーカーを速くするより高い効果を生みます。逆に、高リスク系の停止が24時間を超える場合は、SaaS側の管理範囲の見直しや、未連携アプリの棚卸しを優先して、SSO非対応の島を無くしていきます。

計測指標の例と合意形成

運用を説明責任と結びつけるために、三つの数値を定例で共有します。退職イベント検知から最初のSSO遮断までの時間、全ての対象SaaSの停止完了までの時間、停止作業の自動化率です。これらを部門別にも切り、どこで例外が多発しているかを可視化することで、感覚の議論ではなくデータに基づく合意形成が進みます。経営に対しては、ライセンス過払いの削減額、監査是正工数の削減時間、インシデント発生の抑止という、費用・時間・リスクの三軸で成果を提示します。

まとめ——「人に優しく、機械に厳しく」のオフボーディング

退職者のアカウント処理は、やるべきことが多く、感情や事情のグラデーションも大きい現場です。だからこそ、処理の中核は人ではなく機械に任せ、機械が間違えない構造を作ることが、長期的に現場を楽にします。HRを起点にIdPで一括制御し、セッションの一斉失効と高リスク系の早期停止を最初の数分で終える。そのうえで、業務継続に関わる移管や通知を24時間の第2段階で確実に片付け、すべての足跡をログに残す。ここで示した実装例は、退職者アカウント処理の手順(オフボーディングの自動化)を、自社環境にすぐ適用できるように骨格化しています。

まずはHRとIdPの属性スキーマに「退職日時」と「高リスクフラグ」を追加し、Webhookでイベントを発火させるところから始めてください。 その一歩で、止めすぎと止め足りないの両方からチームを守れます。次に、対象SaaSを優先度順に接続し、ダッシュボードでSLAを可視化しましょう。三ヶ月後に数字で振り返ったとき、退職者処理は「緊張の瞬間」から「静かな定常」に変わっているはずです。

参考文献

  1. BetterCloud. 2025 State of SaaS Trends. https://www.bettercloud.com/monitor/2025-state-of-saas-trends/#:~:text=licenses%20are%20driving%20SaaS%20consolidation,and%20secure%20the%20SaaS%20stack
  2. Delinea. 2024 Verizon DBIR: Credential Compromise Dominates. https://delinea.com/blog/2024-verizon-dbir-credential-compromise-dominates#:~:text=1,1%20data%20breach%20entry%20point
  3. NIST SP 800-53 Rev.5 PS-4 Personnel Termination. https://nist-sp-800-53-r5.bsafes.com/docs/3-14-personnel-security/ps-4-personnel-termination/#:~:text=,formerly%20controlled%20by%20terminated%20individual
  4. Slack SCIM API Reference. https://docs.slack.dev/reference/scim-api#:~:text=%60%7B%20,other_username
  5. Google Admin SDK Directory API Users.update. https://developers.google.com/workspace/admin/directory/reference/rest/v1/users#:~:text=,value
  6. Google Admin SDK Directory API Tokens.delete. https://developers.google.com/admin-sdk/directory/reference/rest/v1/tokens/delete#:~:text=Deletes%20all%20access%20tokens%20issued,a%20user%20for%20an%20application
  7. GitHub REST API: Organizations - Remove a member. https://docs.github.com/en/rest/orgs/members?apiVersion=2022-11-28#:~:text=If%20the%20specified%20user%20is,email%20notification%20in%20both%20cases
  8. GitHub REST API: Teams - Remove team membership for a user. https://docs.github.com/rest/teams/members#:~:text=Note
  9. AWS IAM User Management - Remove an IAM user. https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_remove.html#:~:text=2,if%20the%20user%20has%20them
  10. Money Forward Admina 導入事例(HENNGE/henry等). https://admina.moneyforward.com/jp/case/henry#:~:text=