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列
    ツール名用途対象アカウント有効
    • ツール名: サービス名(例: crm, 会計ソフト)
    • 用途: そのツールの用途
    • 対象アカウント: アクセス権限を渡したいユーザーのメールアドレス
    • 有効: この設定を有効にするか (TRUE / FALSE)

    2. 監査ログシート

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

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

    A列B列C列D列E列F列
    タイムスタンプユーザーツール名用途理由ステータス

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

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

    秘密情報シート

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

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

    A列B列C列D列E列F列
    ツール名用途IDPW更新日備考
    • ツール名: サービス名(権限管理シートと対応)
    • 用途: そのツールの用途(権限管理シートと対応)
    • ID: 実際のID
    • PW: 実際のパスワード
    • 更新日: 更新した日
    • 備考: 備考

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

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

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

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

    1. 操作用スプレッドシートにスクリプトを貼り付ける

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

    const AUTH_TREE_KEY = 'AUTH_TREE_V2';
    
    //スプレッドシートを開いたときにカスタムメニューを追加
    function onOpen() {
      SpreadsheetApp.getUi()
        .createMenu('秘密情報')
        .addItem('コンソールを開く', 'showSidebar')
        .addToUi();
    }
    
    //サイドバーを表示
    function showSidebar() {
      CacheService.getScriptCache().remove(AUTH_TREE_KEY); //キャッシュを更新
      const tpl = HtmlService.createTemplateFromFile('sidebar');
      tpl.tree = getAuthTree(); //プルダウン用のツリー
      const html = tpl.evaluate().setTitle('PW情報コンソール');
      SpreadsheetApp.getUi().showSidebar(html);
    }
    
    function getAuthTree() {
      const cache = CacheService.getScriptCache();
      const hit = cache.get(AUTH_TREE_KEY);
      if (hit) return JSON.parse(hit);
    
      const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('権限管理');
      const lastRow = sh.getLastRow(), lastCol = sh.getLastColumn();
      //ヘッダーしかない場合を回避
      if (lastRow < 2) return { companies: [], usagesByTool: {} };
    
      //権限管理シートのデータ取得
      let vals = sh.getRange(1, 1, lastRow, lastCol).getDisplayValues();
      const header = vals.shift();
    
      const iT = header.indexOf('ツール名');
      const iU = header.indexOf('用途');
      const validIndex = header.indexOf('有効');
    
      //データを有効に絞る
      vals = vals.filter(elem => elem[validIndex]=="TRUE");
    
      // オブジェクトを集合として使う
      const companiesSet = {};
      const usagesByToolSet = {}; // { Tool: {usage:true,...} }
    
      // 有効なデータを繰り返す
      for (let i = 0; i < vals.length; i++) {
        const r = vals[i];
        const t = r[iT], u = r[iU];
        if (!t || !u) continue;
    
        companiesSet[t] = true;
    
        if (!usagesByToolSet[t]) usagesByToolSet[t] = {};
        usagesByToolSet[t][u] = true;
      }
    
      // 集合 → 配列へ
      const companies = Object.keys(companiesSet);
    
      const usagesByTool = {};
      for (const t in usagesByToolSet) {
        usagesByTool[t] = Object.keys(usagesByToolSet[t]);
      }
    
      const tree = {
        companies: companies,
        usagesByTool: usagesByTool
      };
    
      cache.put(AUTH_TREE_KEY, JSON.stringify(tree), 300); // 5分キャッシュ
      return tree;
    }
    
    //シートで設定された権限の確認と、PW受け取りを行うメイン関数
    function PwAcquisitionDecision(tool,usage,reason) {
      //ヘッダー名を変えたらここを変更する
      //※権限管理シート、PW管理SSのヘッダー名は揃える必要がある
      const tool_name = "ツール名";
      const usage_name = "用途";
      const account_name = "対象アカウント";
      const valid_name = "有効";
    
      //環境設定
      const date = Utilities.formatDate(new Date(),"Asia/Tokyo","yyyy/MM/dd HH:mm:ss");
      const ss = SpreadsheetApp.getActiveSpreadsheet();
      const ui = SpreadsheetApp.getUi();
      const user_account = Session.getActiveUser().getEmail(); //アカウント
    
      const authority_sheet = ss.getSheetByName("権限管理"); //権限管理シート
      const log_sheet = ss.getSheetByName("監査ログ"); //監査ログシート
    
      //監査ログシート更新用の設定
      const log_text = [date, user_account, tool, usage, reason];
    
      //稼働終了時の関数
      const finalize = (status, message) => {
        log_sheet.appendRow([...log_text, status]);
        if (message) ui.alert(message);
        return; // 早期終了用
      };
    
      //========ここから権限確認===========
      //権限管理シートの値を検索
      const authority_data = authority_sheet.getRange("A1:D").getDisplayValues(); //権限管理シートの値を取得
      const authority_header = authority_data.shift(); //ヘッダー取得
      
      //各項目の列番号
      const tool_index = authority_header.indexOf(tool_name);
      const usage_index = authority_header.indexOf(usage_name);
      const account_name_index = authority_header.indexOf(account_name);
      const valid_index = authority_header.indexOf(valid_name);
    
      //権限管理シートから一致する値を取得
      const matched_data = authority_data.filter(elem =>
        elem[tool_index] == tool &&
        elem[usage_index] == usage
      );
    
      //権限管理シートに一致する値が無ければ稼働終了
      if(matched_data.length == 0){
        finalize("該当データ無し","リクエスト内容が正しくありません");
        return;
      }
    
      //対象アカウントが無い場合も無効
      const match_account = matched_data.filter(elem =>
        elem[account_name_index] == user_account
      );
      if(match_account.length == 0){
        finalize("アカウント無効","このアカウントには許可されていません");
        return;
      }
    
      //無効になっている場合も終了
      if(match_account[0][valid_index]=="FALSE"){
        finalize("PW無効","パスワードが無効になっています。");
        return;
      } 
    
    
      //PWを受け渡すロボを稼働する
      const pass_obj = {
        [tool_name] : tool,
        [usage_name] : usage
      }
      const property = PropertiesService.getScriptProperties();
      const url = property.getProperty("PW_DELIVERY_URL");
      const payload = JSON.stringify(pass_obj);
      const options = {
        'method' : 'post',
        'contentType': 'application/json',
        'payload' : payload
      };
    
      const response = UrlFetchApp.fetch(url,options);
      const res_data = JSON.parse(response.getContentText());
      console.log(res_data);
      
      //エラーの場合は稼働終了
      if(res_data.error) return finalize("エラー",res_data.message);
    
      //ダイアログにパスワードを表示
      finalize("受渡し成功");
      return res_data; 
    }

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

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

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

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

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <base target="_top">
      <style>
        body { font:14px/1.6 sans-serif; padding:10px; }
        .field { margin:10px 0; }
        .label { display:block; margin-bottom:4px; }
        select, input { width:100%; padding:6px; }
        button { padding:8px; width:100%; }
        .small { width:auto; padding:6px 10px; }
        .muted { color:#666; margin-top:6px; }
        .note { border:1px solid #ccc; padding:8px; white-space:pre-wrap; }
        .section { margin-top:12px; padding-top:10px; border-top:1px solid #ccc; }
        .count { color:#666; margin-top:4px; }
      </style>
      </head>
    <body>
      <div class="field">
        <span class="label">ツール名</span>
        <select id="tool"></select>
      </div>
      <div class="field">
         <span class="label">用途</span>
        <select id="usage" disabled></select>
      </div>
      <div class="field">
        <span class="label">理由</span>
        <input id="reason" type="text" placeholder="障害対応のため など">
      </div>
    
      <div class="field">
        <button id="fetchBtn" disabled style="width:100%;">取得</button>
        <div id="status" class="muted"></div>
      </div>
    
      <!-- 結果 -->
      <div class="section" id="result" style="display:none;">
        <div class="field">
          <span class="label">ID</span>
          <input id="id" type="text" readonly>
        </div>
    
        <div class="field" id="pwRow">
          <span class="label">PW</span>
          <input id="pw" type="text" value="(非表示)" readonly>
          <button id="reveal" class="small" onclick="revealPw('pw','reveal','count','PW')">10秒間表示</button>
          <div id="count" class="count"></div>
        </div>
    
        <div class="field">
          <span class="label">備考</span>
          <div id="note" class="note"></div>
        </div>
    
        <div class="muted">※ PWは10秒だけ表示。</div>
      </div>
    
    <script>
        // サーバで埋め込んだツリーをそのまま使う
      const TREE = <?!= JSON.stringify(tree) ?>;
    
      const $ = id => document.getElementById(id);
      const toolSel = $('tool'), usageSel = $('usage'), reasonSel = $('reason');
      const fetchBtn = $('fetchBtn'), statusEl = $('status'), resultBox = $('result');;
      let DATA = null;
    
      // 表示直後にもう選べる
      fill(toolSel, TREE.companies, 'ツールを選択');
    
      toolSel.addEventListener('change', () => {
        reset(usageSel,'用途を選択');
        fetchBtn.disabled = true; resultBox.style.display = 'none';
        const t = toolSel.value;
        if (!t) return;
        fill(usageSel, (TREE.usagesByTool[t] || []), '用途を選択');
      });
    
      usageSel.addEventListener('change', () => {
        fetchBtn.disabled = !usageSel.value; resultBox.style.display='none';
      });
    
      // 取得→サーバ側 fetchPwForSidebar を呼ぶ(結果をサイドバーに描画)
      fetchBtn.addEventListener('click', () => {
        const t=toolSel.value, u=usageSel.value, r=reasonSel.value;
        statusEl.textContent='権限確認→PW取得中...'; resultBox.style.display='none';
    
        google.script.run
          .withSuccessHandler(obj => {
            DATA = obj || null;
    
            // 何も返ってこなかった(null/undefined/空/全キー空文字)の場合は非表示のまま
            const hasAny = DATA && Object.values(DATA).some(v => v != null && String(v).trim() !== '');
            if (!hasAny) {
              statusEl.textContent = '該当データがありませんでした';
              resultBox.style.display = 'none';   // ← サイドバーの「画像の部分」(結果表示)を出さない
              return;
            }
    
            $('id').value = DATA['ID'] || '';
            $('note').textContent = DATA['備考'] || '';
    
            // PW欄 初期化
            $('pw').value='(非表示)'; $('count').textContent=''; $('reveal').disabled=false;
    
            // PW の有無で行ごと表示/非表示
            const hasPw1 = !!(DATA['PW'] && String(DATA['PW']).trim());
            $('pwRow').style.display = hasPw1 ? '' : 'none';
    
            resultBox.style.display='block';
            statusEl.textContent='';
          })
          .withFailureHandler(showErr)
          .PwAcquisitionDecision(t,u,r);
      });
    
      // ------- UI helpers -------
      function fill(sel, list, placeholder) {
        const frag = document.createDocumentFragment();
        frag.appendChild(new Option('— '+placeholder+' —', ''));
        (list||[]).forEach(v => frag.appendChild(new Option(v, v)));
        sel.innerHTML=''; sel.appendChild(frag); sel.disabled=false;
      }
      function reset(sel, placeholder) {
        sel.innerHTML=''; sel.appendChild(new Option('— '+placeholder+' —', '')); sel.disabled=true;
      }
      function showErr(err) { statusEl.textContent = (err && err.message) ? err.message : String(err); }
    
      function openUrl() {
        const u = ($('url').value || '').trim();
        if (!u) return toast('URLがありません');
        try { window.open(u, '_blank'); } catch(_) {}
      }
      function toast(msg){
        const t=document.createElement('div');
        t.textContent=msg;
        t.style.cssText='position:fixed;right:12px;bottom:12px;background:#333;color:#fff;padding:6px 10px;border-radius:8px;opacity:.95;z-index:9999;';
        document.body.appendChild(t); setTimeout(()=>t.remove(),1500);
      }
      // 共通:PWを10秒だけ表示
      function revealPw(inputId, buttonId, countId, keyName) {
        const btn = $(buttonId);
        const pw  = $(inputId);
        const cnt = $(countId);
    
        if (!DATA || !DATA[keyName]) { toast(`${keyName} が取得できていません`); return; }
    
        btn.disabled = true;
        pw.value = DATA[keyName];
    
        let s = 10;
        (function tick(){
          cnt.textContent = `(${s}秒後に非表示)`;
          if (s-- <= 0) {
            pw.value = '(非表示)';
            cnt.textContent = '';
            // btn.disabled = false; // 再表示を許可したいなら外す
          } else {
            setTimeout(tick, 1000);
          }
        })();
      }
    
    </script>
    </body>
    </html>

    貼り付けたら、ファイルを保存してください。

    3. 管理用スプレッドシートにスクリプトを貼り付ける

    次は管理用スプレッドシートにコードを設定します。
    先程と同じようにスクリプトエディタを開いたら、元からあるコードを全て削除し、以下のスクリプトをそのまま貼り付けてください。

    function doPost(e){
      let passObj;
    
      try{
        const params = JSON.parse(e.postData.contents);
        passObj = PwDelivery(params);
    
      } catch (err) { //エラーがあればエラー値を返す
        passObj = { error: true, message: err.message };
    
      }finally{
        return ContentService
          .createTextOutput(JSON.stringify(passObj))
          .setMimeType(ContentService.MimeType.JSON);
      }
    }
    
    
    //PWを渡す関数本体
    function PwDelivery(request) {
      //ヘッダー名を変えたらここを変更する
      //更新日は使わないので、設定に含めない
      const tool_name = "ツール名";
      const usage_name = "用途";
      const id = "ID";
      const password = "PW";
      const note = "備考";
      
      //環境取得
      const ss = SpreadsheetApp.getActiveSpreadsheet();
      const sheet = ss.getSheetByName("秘密情報");
    
      //PWデータ取得
      const pw_list = sheet.getRange("A1:F").getDisplayValues();
      const header = pw_list.shift();
    
      //各項目の列番号
      const company_index = header.indexOf(tool_name);
      const usage_index = header.indexOf(usage_name);
      const id_index = header.indexOf(id);
      const password_index = header.indexOf(password);
      const note_index = header.indexOf(note);
    
      //一致データを探す
      const matched_data = pw_list.filter(elem =>
        elem[company_index] == request[tool_name] &&
        elem[usage_index] == request[usage_name] 
      );
    
      //一致データが無ければエラーを返す
      if(matched_data.length==0) throw new Error("該当PWがありません");
    
      //一致データがあれば、ID、PW、備考を返す
      return {
        [id] : matched_data[0][id_index],
        [password] : matched_data[0][password_index],
        [note] : matched_data[0][note_index]
      };
    }

    貼り付けたら、ファイルを保存してください。

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

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

    1. 管理用スプレッドシートのスクリプトをデプロイする

    管理用スプレッドシートに設定したスクリプトを「ウェブアプリ」としてデプロイ(公開)します。

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

    2. 操作用スプレッドシートのスクリプトプロパティを設定する

    スクリプトが「管理用スプレッドシート」からパスワードを取得するための設定を行います。

    1. スクリプトエディタの左側メニューから、歯車アイコンの「プロジェクトの設定」をクリックします。
    2. 「スクリプト プロパティ」のセクションで、「スクリプト プロパティを追加」をクリックします。
    3. 以下のようにプロパティを追加します。
      • プロパティ:PW_DELIVERY_URL
      • 値:管理用スプレッドシートのデプロイでコピーした、ウェブアプリのURL

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

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

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

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

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

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

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

    メリット

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

    注意点

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

    まとめ

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

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

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

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

    テンプレートDL用フォーム