Googleスプレッドシートだけで実現!GASで作る鉄壁のID・パスワード管理術

自動化事例

「あのサービスのパスワード、どこに保存したっけ?」
「退職した人が、重要な情報にアクセスできてしまわないか不安…」

サービスのIDやパスワードの管理は、事業を行う上で避けては通れない業務です。多くの企業がExcelやGoogleスプレッドシートで管理リストを作成していますが、その手軽さの裏には情報漏洩や管理の属人化といった大きなリスクが潜んでいます。
かといって、高機能なID管理システムを導入するのはコストも手間もかかります。

そこで本記事では、多くの企業で既に導入されているGoogleスプレッドシートと、少しのプログラミング(GAS)を組み合わせることで、コストをかけずに安全なID・パスワード管理システムを構築する方法を、具体的なコードを交えて徹底解説します。

この記事を読み終える頃には、誰が・いつ・どの情報にアクセスしたのかが一目瞭然になり、情報漏洩のリスクを大幅に削減できる仕組みが手に入ります。

なぜ、ただのスプレッドシート管理は危険なのか?

はじめに、なぜ一般的なスプレッドシートでのID・パスワード管理が推奨されないのか、その理由を簡単におさらいしましょう。

  1. ファイル漏洩のリスク: ファイル自体をコピーされたり、誤って共有設定を「全員に公開」にしてしまったりすると、リストにある全てのID・パスワードが一瞬で流出してしまいます。
  2. 曖昧な閲覧権限: 「AさんはA事業部の情報だけ、BさんはB事業部の情報だけ見る」といった細かい権限設定が難しく、全社員が全ての情報を見られる状態になりがちです。
  3. 追跡不能な閲覧履歴: 「誰が」「いつ」パスワードを確認したのか履歴が残らないため、問題が発生した際に原因の特定が困難です。
  4. ヒューマンエラー: 誤って重要なパスワードを削除・上書きしてしまう可能性があります。

これらのリスクは、事業の信頼性を揺るがしかねない重大な問題につながります。しかし、ご安心ください。これからご紹介する方法は、これらの問題をまとめて解決します。

GASで実現するセキュアな管理システムの全体像

今回構築するシステムのコンセプトは「情報と権限の分離」そして「完全な履歴記録」です。

具体的には、以下の2つのスプレッドシートを用意します。

  • 操作用スプレッドシート: ユーザーがID・パスワードを取得するために開く、いわば「受付窓口」です。ここには実際のパスワード情報は一切保存しません。
  • 管理用スプレッドシート: 実際のID・パスワードを保管しておくための、厳重に管理された「金庫」です。このファイルは管理者しか開けません。

利用者は「受付窓口」から申請を出し、GAS(Google Apps Script)というプログラムが利用者の権限を確認した上で、「金庫」から必要な情報だけを一時的に取り出して表示します。

この仕組みにより、利用者は実際のパスワードが保存されたファイルに触れることなく、許可された情報にのみアクセスできるようになります。

それでは、具体的な構築手順を見ていきましょう。

Step 1: 2つのスプレッドシートを用意する

まずは、Google Drive上で新規に2つのスプレッドシートを作成します。

  1. 操作用スプレッドシート: ファイル名を「ID・パスワード管理コンソール」など、分かりやすい名前にします。このファイルは、実際にID・パスワードを確認したい社員がアクセスします。
  2. 管理用スプレッドシート: ファイル名を「【管理者限定】ID・パスワード台帳」など、重要性が分かる名前にします。

作成したら、「管理用スプレッドシート」の共有設定を開き、アクセスできるユーザーを自分(管理者)のみに限定してください。ファイルのURLを知っていても、管理者以外は閲覧すらできないようにすることが重要です。

Step 2: 各シートを設計する

次に、作成した2つのスプレッドシートに、それぞれ必要なシート(タブ)を作成し、項目を設定していきます。

① 操作用スプレッドシートの設計

操作用スプレッドシートには、以下の2つのシートを作成します。

1. 権限管理シート:アクセスコントロールリスト

誰が、どのツールの、どの権限レベルの情報まで閲覧できるかを定義するシートです。

A1セルから順に、以下の見出しを入力してください。

A列B列C列D列E列
ツール最小ロール種別対象有効
  • ツール: サービス名(例: crm, 会計ソフト)
  • 最小ロール: 許可する最小ロール(viewer < editor < admin の3段階)
  • 種別: user(個人)かgroup(Googleグループ)かを指定
  • 対象: ユーザーのメールアドレス、またはGoogleグループのメールアドレス
  • 有効: この設定を有効にするか (TRUE / FALSE)

2. 監査ログシート

誰が、いつ、どの情報を、何の目的で閲覧したかを自動で記録するためのシートです。

このシートはGASが自動で作成・追記しますが、手動で作成する場合はA1セルから以下の見出しを入力してください。

A列B列C列D列E列F列G列H列I列J列
タイムスタンプユーザーツール要求ロール資格情報名理由操作ステータス前のハッシュ現在のハッシュ

② 管理用スプレッドシートの設計

管理者のみがアクセスできる管理用スプレッドシートには、1つだけシートを作成します。

秘密情報シート

実際のID・パスワード情報を格納します。

A1セルから順に、以下の見出しを入力してください。

A列B列C列D列E列F列
ツール資格情報名最小ロール更新日備考
  • ツール: サービス名(権限管理シートと対応)
  • 資格情報名: 具体的なID/パスワード名(例: 管理者パスワード, APIキー)
  • 最小ロール: この情報を閲覧するために必要な最小ロール
  • : 実際のIDやパスワード、APIキーなどをここに入力します
  • 更新日: 更新日
  • 備考: 備考

これでシートの準備は完了です。いよいよGASを実装していきます。

Step 3: GASを実装する(コピー&ペーストでOK)

ここからは、操作用スプレッドシートでGASの設定を行います。専門的に見えるかもしれませんが、ほとんどコピー&ペーストで完了しますので、ご安心ください。

GASの基本的な使い方については、こちらの記事で詳しく解説しています。

1. スクリプトを貼り付ける

スクリプトエディタを開いたら、元からあるコードを全て削除し、以下のスクリプトをそのまま貼り付けてください。

const ROLE_ORDER = ['viewer', 'editor', 'admin'];

/**
 * スプレッドシートを開いたときにカスタムメニューを追加します。
 */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('秘密情報')
    .addItem('コンソールを開く', 'showSidebar')
    .addToUi();
}

/**
 * サイドバーを表示します。
 */
function showSidebar() {
  const html = HtmlService.createHtmlOutputFromFile('sidebar')
    .setTitle('資格情報コンソール');
  SpreadsheetApp.getUi().showSidebar(html);
}

/**
 * ユーザーのロールが必要なロール以上か判定します。
 * @param {string} userRole ユーザーが持つロール
 * @param {string} required 必要なロール
 * @return {boolean} 権限があればtrue
 */
function hasRoleOrHigher(userRole, required) {
  return ROLE_ORDER.indexOf(userRole) >= ROLE_ORDER.indexOf(required);
}

// ===== 権限判定 =====
/**
 * 指定されたツールに対してユーザーが持つ最大のロールを解決します。
 * @param {string} email ユーザーのメールアドレス
 * @param {string} tool ツールの名前
 * @return {string|null} ユーザーの最大ロール、権限がなければnull
 */
function resolveUserMaxRole(email, tool) {
  const sh = SpreadsheetApp.getActive().getSheetByName('権限管理');
  if (!sh) throw new Error('「権限管理」シートが見つかりません');

  const rows = sh.getDataRange().getValues().slice(1);
  let maxRole = null;

  for (const r of rows) {
    const [t, minRole, typ, principal, enabled] = r;
    if (!enabled || String(enabled).toLowerCase() === 'false') continue;
    if (t !== tool) continue;

    let isAuthorized = false;
    if (typ === 'user' && principal === email) {
      isAuthorized = true;
    } else if (typ === 'group' && isMemberOfGroup_(email, principal)) {
      isAuthorized = true;
    }

    if (isAuthorized) {
      if (!maxRole || hasRoleOrHigher(minRole, maxRole)) {
        maxRole = minRole;
      }
    }
  }
  return maxRole; // null なら権限なし
}

/**
 * ユーザーが指定されたGoogleグループのメンバーであるかを確認します。(要: Admin Directory API)
 * @param {string} email ユーザーのメールアドレス
 * @param {string} groupEmail Googleグループのメールアドレス
 * @return {boolean} メンバーであればtrue
 */
function isMemberOfGroup_(email, groupEmail) {
  try {
    const res = AdminDirectory.Members.hasMember(groupEmail, email);
    return res && res.isMember === true;
  } catch (e) {
    // APIが有効でない場合などのエラーをハンドル
    console.error(`グループメンバーシップの確認に失敗: ${e.message}`);
    return false;
  }
}

// ===== 秘密取得(セルに出さない) =====
/**
 * サイドバーからの要求に応じて秘密情報を取得します。
 * @param {object} payload {tool, role, name, reason}
 * @return {string} 取得した秘密情報
 */
function fetchSecret(payload) {
  const { tool, role, name, reason } = payload;
  if (!tool || !role || !name) throw new Error('入力不足です');

  const email = Session.getActiveUser().getEmail();
  if (!email) throw new Error('ユーザーを識別できませんでした。組織のアカウントでログインしていることを確認してください。');

  const userMax = resolveUserMaxRole(email, tool);
  if (!userMax || !hasRoleOrHigher(userMax, role)) {
    logAudit_({ user: email, tool, role, name, reason, status: 'denied' });
    throw new Error('権限がありません');
  }

  const vaultId = PropertiesService.getScriptProperties().getProperty('VAULT_SPREADSHEET_ID');
  if (!vaultId) throw new Error('管理用スプレッドシートのIDが設定されていません。');
  
  const vss = SpreadsheetApp.openById(vaultId);
  const vsh = vss.getSheetByName('秘密情報');
  const rows = vsh.getDataRange().getValues().slice(1);

  let record = null;
  for (const r of rows) {
    const [t, n, minRole, value] = r;
    if (t === tool && n === name && hasRoleOrHigher(userMax, minRole)) {
      record = { value, minRole };
      break;
    }
  }

  if (!record) {
    logAudit_({ user: email, tool, role, name, reason, status: 'not_found' });
    throw new Error('対象が見つからないか、この情報にアクセスするロールがありません');
  }

  logAudit_({ user: email, tool, role, name, reason, status: 'success' });
  return record.value; // HTML側で10秒だけ表示
}

// ===== 監査(ハッシュ鎖で改ざん検知) =====
/**
 * 監査ログを記録します。
 * @param {object} ev ログイベント情報
 */
function logAudit_(ev) {
  const sh = SpreadsheetApp.getActive().getSheetByName('監査ログ') ||
             SpreadsheetApp.getActive().insertSheet('監査ログ', 0);

  if (sh.getLastRow() === 0) {
    sh.appendRow(['タイムスタンプ', 'ユーザー', 'ツール', '要求ロール', '資格情報名', '理由', '操作', 'ステータス', '前のハッシュ', '現在のハッシュ']);
  }
  
  const ts = new Date().toISOString();
  const prev = sh.getLastRow() >= 1 ? sh.getRange(sh.getLastRow(), 10).getValue() : '';
  const curr = computeAuditHash_(prev, [ts, ev.user, ev.tool, ev.role, ev.name, ev.reason, 'view', ev.status]);
  
  sh.appendRow([ts, ev.user, ev.tool, ev.role, ev.name, ev.reason, 'view', ev.status, prev, curr]);
}

/**
 * 監査ログのハッシュ値を計算します。
 * @param {string} prevHash 直前のハッシュ値
 * @param {Array<string>} fields ログのフィールド
 * @return {string} 計算されたハッシュ値
 */
function computeAuditHash_(prevHash, fields) {
  const key = PropertiesService.getScriptProperties().getProperty('AUDIT_CHAIN_KEY');
  if (!key) throw new Error('監査用のキーが設定されていません。');

  const msg = prevHash + '|' + fields.map(x => String(x ?? '')).join('|');
  const sig = Utilities.computeHmacSha256Signature(msg, key);
  return Utilities.base64Encode(sig);
}

2. sidebar.html を作成し、コードを貼り付ける

次に、利用者が操作するサイドバーの画面を作ります。

スクリプトエディタの左側にある「ファイル」の横の「+」をクリックし、「HTML」を選択します。

ファイル名を「sidebar」(拡張子なし)として作成し、元からあるコードを全て削除して、以下のコードを貼り付けてください。

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family: 'Helvetica Neue', Arial, sans-serif; padding: 10px; font-size: 14px; }
    label { display: block; margin-top: 12px; margin-bottom: 4px; font-weight: bold; }
    input, select, button {
      width: 100%;
      padding: 8px;
      margin-bottom: 10px;
      border-radius: 4px;
      border: 1px solid #ccc;
      box-sizing: border-box;
    }
    button {
      background-color: #4285F4;
      color: white;
      border: none;
      cursor: pointer;
      font-weight: bold;
    }
    button:hover { background-color: #357AE8; }
    #reveal { display: none; background-color: #f4b400; }
    #reveal:hover { background-color: #f0a800; }
    #secret {
      font-family: 'Courier New', monospace;
      padding: 10px;
      border: 1px solid #ddd;
      background-color: #f5f5f5;
      border-radius: 4px;
      min-height: 20px;
      word-wrap: break-word;
      filter: blur(6px); /* 初期状態ではぼかす */
      transition: filter 0.2s;
    }
  </style>
</head>
<body>
  <div>
    <label for="tool">ツール</label>
    <input id="tool" type="text" placeholder="crm など">

    <label for="role">必要ロール</label>
    <select id="role">
      <option>viewer</option>
      <option>editor</option>
      <option>admin</option>
    </select>

    <label for="name">資格情報名</label>
    <input id="name" type="text" placeholder="例: 管理者パスワード">

    <label for="reason">用途 (監査用 必須)</label>
    <input id="reason" type="text" placeholder="障害対応のため など">

    <button onclick="get()">取得</button>

    <pre id="secret"></pre>
    <button id="reveal" onclick="reveal()">10秒だけ表示</button>
  </div>

<script>
  let revealTimeout;

  function get() {
    // 以前のタイマーをクリア
    if (revealTimeout) clearTimeout(revealTimeout);
    
    const revealButton = document.getElementById('reveal');
    revealButton.style.display = 'none';

    const payload = {
      tool: document.getElementById('tool').value.trim(),
      role: document.getElementById('role').value,
      name: document.getElementById('name').value.trim(),
      reason: document.getElementById('reason').value.trim()
    };
    if (!payload.tool || !payload.role || !payload.name || !payload.reason) {
      alert('すべての項目を入力してください');
      return;
    }

    // 取得中はボタンを無効化
    document.querySelector('button').disabled = true;

    google.script.run
      .withSuccessHandler(secret => {
        const box = document.getElementById('secret');
        box.dataset.real = secret;
        box.textContent = '•'.repeat(secret.length - 4) + secret.slice(-4); // 末尾4文字以外マスク
        box.style.filter = 'blur(6px)'; // ぼかしを再度適用
        revealButton.style.display = 'inline-block';
        document.querySelector('button').disabled = false; // ボタンを有効化
      })
      .withFailureHandler(e => {
        alert(e.message);
        document.querySelector('button').disabled = false; // ボタンを有効化
      })
      .fetchSecret(payload);
  }

  function reveal() {
    // 以前のタイマーをクリア
    if (revealTimeout) clearTimeout(revealTimeout);

    const box = document.getElementById('secret');
    if (!box.dataset.real) return;

    box.style.filter = 'none'; // ぼかしを解除
    box.textContent = box.dataset.real;

    // 10秒後に再びマスクする
    revealTimeout = setTimeout(() => {
      box.style.filter = 'blur(6px)';
      box.textContent = '•'.repeat(box.dataset.real.length - 4) + box.dataset.real.slice(-4);
    }, 10000);
  }
</script>
</body>
</html>

ファイルを保存すれば、実装は完了です。

Step 4: スクリプトを設定し、有効化する

最後に、作成したスクリプトが正しく動作するための設定を行います。

1. スクリプトプロパティを設定する

スクリプトが「管理用スプレッドシート」の場所を知るための設定と、監査ログの改ざん防止用の秘密の文字列を設定します。

  1. スクリプトエディタの左側メニューから、歯車アイコンの「プロジェクトの設定」をクリックします。
  2. 「スクリプト プロパティ」のセクションで、「スクリプト プロパティを追加」をクリックします。
  3. 以下の2つのプロパティを追加します。
    • VAULT_SPREADSHEET_ID
      • 「管理用スプレッドシート」のURLからID部分をコピーして貼り付けます。
        (例: https://docs.google.com/spreadsheets/d/ココがID/edit)
    • AUDIT_CHAIN_KEY
      • 誰にも推測されないランダムな文字列を入力します。
        (例: Dk3$p!a9@zR7qF*c)

2. (任意)Googleグループ連携を有効にする

権限管理シートでGoogleグループによる権限管理を行いたい場合は、追加でAPI(アプリケーション・プログラミング・インターフェース)という、プログラム同士を連携させるための仕組みを有効にする必要があります。

  1. スクリプトエディタの左側メニュー「サービス」の横の「+」をクリックします。
  2. リストから「Admin SDK API」を選択し、「追加」をクリックします。

3. スクリプトをデプロイする

設定の総仕上げです。このスクリプトを「ウェブアプリ」としてデプロイ(公開)します。

  1. スクリプトエディタ右上の「デプロイ」ボタンをクリックし、「新しいデプロイ」を選択します。
  2. 「種類の選択」の横にある歯車アイコンをクリックし、「ウェブアプリ」を選択します。
  3. 以下の通りに設定します。この設定はセキュリティ上、非常に重要です。
    • 説明: ID・パスワード管理コンソール(v1)など
    • 次のユーザーとして実行: 自分(あなたのメールアドレス)
    • アクセスできるユーザー: (あなたのドメイン)内の全員
  4. 「デプロイ」ボタンをクリックします。
  5. 「デプロイ」ボタンをクリックすると、初回のみスクリプトの実行を承認する画面が表示されます。承認フローの詳しい手順については、こちらの記事をご確認ください。

これで全ての準備が整いました!

Step 5: 実際に使ってみよう

操作用スプレッドシートに戻り、ページを再読み込みしてください。メニューに「秘密情報」が追加されているはずです。

  1. 「秘密情報」>「コンソールを開く」をクリックすると、右側にサイドバーが表示されます。
  2. 権限管理シートと秘密情報シートに設定した内容に基づき、「ツール」「必要ロール」「資格情報名」と「用途」を入力し、「取得」ボタンをクリックします。
  3. 成功すると、ぼかしのかかったパスワード(下4桁のみ表示)が表示されます。
  4. 「10秒だけ表示」ボタンをクリックすると、パスワードの全文がクリアに表示され、10秒後に自動的に再びぼかしがかかった状態に戻ります。

同時に、監査ログシートを見てみましょう。あなたの操作がリアルタイムで記録されているはずです。ステータスがsuccessになっていれば成功、権限がない場合はdeniedと記録されます。

この仕組みのメリットと注意点

最後に、この仕組みのメリットと、運用する上での注意点をまとめます。

メリット

  • 無料: Googleアカウントがあれば追加費用なしで構築できます。
  • 高い安全性: パスワードをセルに直接表示せず、操作パネル(サイドバー)で一時的に表示するだけなので、画面キャプチャなどのリスクを低減します。
  • 柔軟な権限管理: ユーザー単位、Googleグループ単位で詳細なアクセスコントロールが可能です。
  • 完全な監査ログ: 「誰が、いつ、どの情報を、何の目的で」見たかが全て記録され、さらにハッシュ鎖技術によりログの改ざんも検知できます。

注意点

  • スクリプト編集権限の厳重な管理: この仕組みの最大の権限者は、このGASを編集できるユーザーです。スクリプト編集者は管理用スプレッドシートのIDやコードの全てを閲覧できるため、信頼できる管理者にのみ共同編集権限を付与してください。
  • 万能ではない: あくまでスプレッドシートをベースとした仕組みであり、多要素認証やIPアドレス制限など、専門のID管理ツールが持つ高度な機能はありません。企業の成長や扱う情報の重要度に応じて、将来的には専門ツールへの移行も視野に入れましょう。

まとめ

本記事では、GoogleスプレッドシートとGASを用いて、安全かつ効率的にID・パスワードを管理するシステムを構築する方法を解説しました。

この仕組みを導入することで、Excelやスプレッドシートでの単純なリスト管理から脱却し、「誰がどの情報にアクセスできるか」を明確にコントロールし、全ての操作履歴を記録するという、セキュアな情報管理の第一歩を踏み出すことができます。

まずは本記事を参考に、ご自身の環境で小さな範囲から試してみてはいかがでしょうか。きっと、日々のID・パスワード管理業務の安心感が格段に向上するはずです。

※Googleサービスは、Google LLC の商標であり、この記事はGoogleによって承認されたり、Google と提携したりするものではありません。